const VERSION = require('../package.json').version; const md5 = require('blueimp-md5'); const marked = require('marked'); const autosize = require('autosize'); const timeAgo = require('./utils/timeago'); const detect = require('./utils/detect'); const Utils = require('./utils/htmlUtils'); const Emoji = require('./plugins/emojis'); const hanabi = require('hanabi'); const LINKREG = /^https?\:\/\//; const defaultComment = { comment: '', nick: 'Anonymous', mail: '', link: '', ua: navigator.userAgent, url: '' }; const locales = { 'zh-cn': { head: { nick: '昵称', mail: '邮箱', link: '网址(http://)', }, tips: { comments: '评论', sofa: '快来做第一个评论的人吧~', busy: '还在提交中,请稍候...', again: '这么简单也能错,也是没谁了.' }, ctrl: { reply: '回复', ok: '好的', sure: '确认', cancel: '取消', confirm: '确认', continue: '继续', more: '查看更多...', try: '再试试?', preview: '预览', emoji: '表情' }, error: { 99: '初始化失败,请检查init中的`el`元素.', 100: '初始化失败,请检查你的AppId和AppKey.', 401: '未经授权的操作,请检查你的AppId和AppKey.', 403: '访问被api域名白名单拒绝,请检查你的安全域名设置.', }, timeago: { seconds: '秒前', minutes: '分钟前', hours: '小时前', days: '天前', now: '刚刚' } }, en: { head: { nick: 'NickName', mail: 'E-Mail', link: 'Website(http://)', }, tips: { comments: 'Comments', sofa: 'No comments yet.', busy: 'Submit is busy, please wait...', again: 'Sorry, this is a wrong calculation.' }, ctrl: { reply: 'Reply', ok: 'Ok', sure: 'Sure', cancel: 'Cancel', confirm: 'Confirm', continue: 'Continue', more: 'Load More...', try: 'Once More?', preview: 'Preview', emoji: 'Emoji' }, error: { 99: 'Initialization failed, Please check the `el` element in the init method.', 100: 'Initialization failed, Please check your appId and appKey.', 401: 'Unauthorized operation, Please check your appId and appKey.', 403: 'Access denied by api domain white list, Please check your security domain.', }, timeago: { seconds: 'seconds ago', minutes: 'minutes ago', hours: 'hours ago', days: 'days ago', now: 'just now' } } } let _avatarSetting = { cdn: 'https://gravatar.loli.net/avatar/', ds: ['mp', 'identicon', 'monsterid', 'wavatar', 'robohash', 'retro', ''], params: '', hide: false }, META = ['nick', 'mail', 'link'], _store = Storage && localStorage && localStorage instanceof Storage && localStorage, _path = location.pathname.replace(/index\.html?$/, ''); function ValineFactory(option) { let root = this // Valine init !!option && root.init(option); return root; } /** * Valine Init * @param {Object} option */ ValineFactory.prototype.init = function (option) { if (typeof document === 'undefined') { console && console.warn('Sorry, Valine does not support Server-side rendering.') return; } let root = this; try { let { lang, langMode, avatar, avatarForce, avatar_cdn, notify, verify, visitor, pageSize, recordIP } = option; let ds = _avatarSetting['ds']; let force = avatarForce ? '&q=' + Math.random().toString(32).substring(2) : ''; lang && langMode && root.installLocale(lang, langMode); root.locale = root.locale || locales[lang || 'zh-cn']; root.notify = notify || false; root.verify = verify || false; if (recordIP) { let ipScript = Utils.create('script', 'src', '//api.ip.sb/jsonip?callback=getIP'); let s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ipScript, s); // 获取IP window.getIP = function (json) { defaultComment['ip'] = json.ip; } } _avatarSetting['params'] = `?d=${(ds.indexOf(avatar) > -1 ? avatar : 'mp')}&v=${VERSION}${force}`; _avatarSetting['hide'] = avatar === 'hide' ? true : false; _avatarSetting['cdn'] = LINKREG.test(avatar_cdn) ? avatar_cdn : _avatarSetting['cdn'] _path = option.path || _path; let size = Number(pageSize || 10); option.pageSize = !isNaN(size) ? (size < 1 ? 10 : size) : 10; marked.setOptions({ renderer: new marked.Renderer(), highlight: option.highlight === false ? null : hanabi, gfm: true, tables: true, breaks: true, pedantic: false, sanitize: false, smartLists: true, smartypants: true }); if (!AV) { setTimeout(() => { root.init(option) }, 20) return; } let id = option.app_id || option.appId; let key = option.app_key || option.appKey; if (!id || !key) throw 99; AV.applicationId && delete AV._config.applicationId || (AV.applicationId = null); AV.applicationKey && delete AV._config.applicationKey || (AV.applicationKey = null); AV.init({ appId: id, appKey: key }); // get comment count let els = Utils.findAll(document, '.valine-comment-count'); for (let i = 0, len = els.length; i < len; i++) { let el = els[i]; if (el) { let k = Utils.attr(el, 'data-xid'); if (k) { root.Q(k).count().then(n => { el.innerText = n }).catch(ex => { el.innerText = 0 }) } } } // Counter visitor && CounterFactory.add(AV.Object.extend('Counter')); let el = option.el || null; let _el = Utils.findAll(document, el); el = el instanceof HTMLElement ? el : (_el[_el.length - 1] || null); if (!el) return; root.el = el; root.el.classList.add('v'); _avatarSetting['hide'] && root.el.classList.add('hide-avatar'); option.meta = (option.guest_info || option.meta || META).filter(item => META.indexOf(item) > -1); let inputEl = (option.meta.length == 0 ? META : option.meta).map(item => { let _t = item == 'mail' ? 'email' : 'text'; return META.indexOf(item) > -1 ? `` : '' }); root.placeholder = option.placeholder || 'Just Go Go'; root.el.innerHTML = `
Code ${code}: ${msg}`) || console && console.error(`Code ${code}: ${msg}`) } else { root.el && root.nodata.show(`
${JSON.stringify(ex)}`) || console && console.error(JSON.stringify(ex)) } return; } /** * install Multi language support * @param {String} locale langName * @param {Object} mode langSource */ ValineFactory.prototype.installLocale = function (locale, mode) { let root = this; mode = mode || {}; if (locale) { // locales[locale] = JSON.stringify(Object.keys(locales['zh-cn']))==JSON.stringify(Object.keys(mode)) ? mode : undefined; locales[locale] = mode; root.locale = locales[locale] || locales['zh-cn']; } return root; } /** * * @param {String} path */ ValineFactory.prototype.setPath = function (path) { _path = path || _path; return this } /** * Bind Event */ ValineFactory.prototype.bind = function (option) { let root = this; // load emojis let _vemojis = Utils.find(root.el, '.vemojis'); let _vpreview = Utils.find(root.el, '.vpreview'); // emoji 操作 let _emojiCtrl = Utils.find(root.el, '.vemoji-btn'); // 评论内容预览 let _vpreviewCtrl = Utils.find(root.el, `.vpreview-btn`); let emojiData = Emoji.data; for (let key in emojiData) { if (emojiData.hasOwnProperty(key)) { (function (name, val) { let _i = Utils.create('i', { 'name': name, 'title': name }); _i.innerHTML = val; _vemojis.appendChild(_i); Utils.on('click', _i, (e) => { let _veditor = Utils.find(root.el, '.veditor'); _insertAtCaret(_veditor, val) syncContentEvt(_veditor) }); })(key, emojiData[key]) } } root.emoji = { show() { root.preview.hide(); Utils.attr(_emojiCtrl, 'v', 1); Utils.removeAttr(_vpreviewCtrl, 'v'); Utils.attr(_vemojis, 'style', 'display:block'); return root.emoji }, hide() { Utils.removeAttr(_emojiCtrl, 'v'); Utils.attr(_vemojis, 'style', 'display:hide'); return root.emoji } } root.preview = { show() { if (defaultComment['comment']) { root.emoji.hide(); Utils.attr(_vpreviewCtrl, 'v', 1); Utils.removeAttr(_emojiCtrl, 'v'); _vpreview.innerHTML = defaultComment['comment']; Utils.attr(_vpreview, 'style', 'display:block'); _activeOtherFn() } return root.preview }, hide() { Utils.removeAttr(_vpreviewCtrl, 'v'); Utils.attr(_vpreview, 'style', 'display:none'); return root.preview }, empty() { _vpreview.innerHtml = ''; return root.preview } } /** * XSS filter * @param {String} content Html String */ let xssFilter = (content) => { let vNode = Utils.create('div'); vNode.insertAdjacentHTML('afterbegin', content); let ns = Utils.findAll(vNode, "*"); let rejectNodes = ['INPUT', 'STYLE', 'SCRIPT', 'IFRAME', 'FRAME', 'AUDIO', 'VIDEO', 'EMBED', 'META', 'TITLE', 'LINK']; let __replaceVal = (node, attr) => { let val = Utils.attr(node, attr); val && Utils.attr(node, attr, val.replace(/(javascript|eval)/ig, '')); } Utils.each(ns, (idx, n) => { if (n.nodeType !== 1) return; if (rejectNodes.indexOf(n.nodeName) > -1) { // console.log(n.nodeName) if (n.nodeName === 'INPUT' && Utils.attr(n, 'type') === 'checkbox') Utils.attr(n, 'disabled', 'disabled'); else Utils.remove(n); } if (n.nodeName === 'A') __replaceVal(n, 'href') Utils.clearAttr(n) }) return vNode.innerHTML } /** * 评论框内容变化事件 * @param {HTMLElement} el */ let syncContentEvt = (_el) => { let _v = 'comment'; let _val = (_el.value || ''); _val = Emoji.parse(_val); _el.value = _val; let ret = xssFilter(marked(_val)); defaultComment[_v] = ret; _vpreview.innerHTML = ret; if (_val) autosize(_el); else autosize.destroy(_el) } // 显示/隐藏 Emojis Utils.on('click', _emojiCtrl, (e) => { let _vi = Utils.attr(_emojiCtrl, 'v'); if (_vi) { root.emoji.hide() } else { root.emoji.show(); } }); Utils.on('click', _vpreviewCtrl, function (e) { let _vi = Utils.attr(_vpreviewCtrl, 'v'); if (_vi) { root.preview.hide(); } else { root.preview.show(); } }); let meta = option.meta; let inputs = {}; // 同步操作 let mapping = { veditor: "comment" } for (let i = 0, len = meta.length; i < len; i++) { mapping[`v${meta[i]}`] = meta[i]; } for (let i in mapping) { if (mapping.hasOwnProperty(i)) { let _v = mapping[i]; let _el = Utils.find(root.el, `.${i}`); inputs[_v] = _el; _el && Utils.on('input change blur', _el, (e) => { if (_v === 'comment') { syncContentEvt(_el) } else { defaultComment[_v] = Utils.escape(_el.value.replace(/(^\s*)|(\s*$)/g, "")); } }); } } let _insertAtCaret = (field, val) => { if (document.selection) { //For browsers like Internet Explorer field.focus(); let sel = document.selection.createRange(); sel.text = val; field.focus(); } else if (field.selectionStart || field.selectionStart == '0') { //For browsers like Firefox and Webkit based let startPos = field.selectionStart; let endPos = field.selectionEnd; let scrollTop = field.scrollTop; field.value = field.value.substring(0, startPos) + val + field.value.substring(endPos, field.value.length); field.focus(); field.selectionStart = startPos + val.length; field.selectionEnd = startPos + val.length; field.scrollTop = scrollTop; } else { field.focus(); field.value += val; } } let createVquote = id => { let vcontent = Utils.find(root.el, ".vh[rootid='" + id + "']"); let vquote = Utils.find(vcontent, '.vquote'); if (!vquote) { vquote = Utils.create('div', 'class', 'vquote'); vcontent.appendChild(vquote); } return vquote } let query = (no = 1) => { let size = option.pageSize; let count = Number(Utils.find(root.el, '.vnum').innerText); root.loading.show(); let cq = root.Q(_path); cq.limit(size); cq.skip((no - 1) * size); cq.find().then(rets => { let len = rets.length; let rids = [] for (let i = 0; i < len; i++) { let ret = rets[i]; rids.push(ret.id) insertDom(ret, Utils.find(root.el, '.vlist'), !0) } // load children comment root.Q(_path, rids).then(ret => { let childs = ret && ret.results || [] for (let k = 0; k < childs.length; k++) { let child = childs[k]; insertDom(child, createVquote(child.get('rid'))) } }) let _vpage = Utils.find(root.el, '.vpage'); _vpage.innerHTML = size * no < count ? `` : ''; let _vmore = Utils.find(_vpage, '.vmore'); if (_vmore) { Utils.on('click', _vmore, (e) => { _vpage.innerHTML = ''; query(++no); }) } root.loading.hide(); }).catch(ex => { root.loading.hide().ErrorHandler(ex) }) } root.Q(_path).count().then(num => { if (num > 0) { Utils.attr(Utils.find(root.el, '.vinfo'), 'style', 'display:block;'); Utils.find(root.el, '.vcount').innerHTML = `${num} ${root.locale['tips']['comments']}`; query(); } else { root.loading.hide(); } }).catch(ex => { root.ErrorHandler(ex) }); let insertDom = (rt, node, mt) => { let _vcard = Utils.create('div', { 'class': 'vcard', 'id': rt.id }); let _img = _avatarSetting['hide'] ? '' : ``; let ua = rt.get('ua') || ''; let uaMeta = ''; if (ua) { ua = detect(ua); let browser = `${ua.browser} ${ua.version}`; let os = `${ua.os} ${ua.osVersion}`; uaMeta = `${browser} ${os}`; } let _nick = ''; let _t = rt.get('link') || ''; _nick = _t ? `${rt.get("nick")}` : `${rt.get('nick')}`; _vcard.innerHTML = `${_img}
', `
${atData['at']} , `);
}
for (let i in defaultComment) {
if (defaultComment.hasOwnProperty(i)) {
let _v = defaultComment[i];
comment.set(i, _v);
}
}
comment.setACL(getAcl());
comment.save().then(ret => {
defaultComment['nick'] != 'Anonymous' && _store && _store.setItem('ValineCache', JSON.stringify({
nick: defaultComment['nick'],
link: defaultComment['link'],
mail: defaultComment['mail']
}));
let _count = Utils.find(root.el, '.vnum');
let num = 1;
try {
if (atData['rid']) {
let vquote = Utils.find(root.el, '.vquote[rid="' + atData['rid'] + '"]') || createVquote(atData['rid']);
insertDom(ret, vquote, !0)
} else {
if (_count) {
num = Number(_count.innerText) + 1;
_count.innerText = num;
} else {
Utils.find(root.el, '.vcount').innerHTML = '1 ' + root.locale['tips']['comments']
}
insertDom(ret, Utils.find(root.el, '.vlist'));
}
defaultComment['mail'] && signUp({
username: defaultComment['nick'],
mail: defaultComment['mail']
});
atData['at'] && atData['rmail'] && root.notify && mailEvt({
username: atData['at'].replace('@', ''),
mail: atData['rmail']
});
Utils.removeAttr(submitBtn, 'disabled');
root.loading.hide();
reset();
} catch (ex) {
root.ErrorHandler(ex);
}
}).catch(ex => {
root.ErrorHandler(ex);
})
}
let verifyEvt = (fn) => {
let x = Math.floor((Math.random() * 10) + 1);
let y = Math.floor((Math.random() * 10) + 1);
let z = Math.floor((Math.random() * 10) + 1);
let opt = ['+', '-', 'x'];
let o1 = opt[Math.floor(Math.random() * 3)];
let o2 = opt[Math.floor(Math.random() * 3)];
let expre = `${x}${o1}${y}${o2}${z}`;
let subject = `${expre} = `;
root.alert.show({
type: 1,
text: subject,
ctxt: root.locale['ctrl']['cancel'],
otxt: root.locale['ctrl']['ok'],
cb() {
let code = +Utils.find(root.el, '.vcode').value;
let ret = (new Function(`return ${expre.replace(/x/g, '*')}`))();
if (ret === code) {
fn && fn();
} else {
root.alert.show({
type: 1,
text: `(T_T)${root.locale['tips']['again']}`,
ctxt: root.locale['ctrl']['cancel'],
otxt: root.locale['ctrl']['try'],
cb() {
verifyEvt(fn);
return;
}
})
}
}
})
}
let signUp = (o) => {
let u = new AV.User();
u.setUsername(o.username);
u.setPassword(o.mail);
u.setEmail(o.mail);
u.setACL(getAcl());
return u.signUp();
}
let mailEvt = (o) => {
AV.User.requestPasswordReset(o.mail).then(ret => {}).catch(e => {
if (e.code == 1) {
root.alert.show({
type: 0,
text: `ヾ(o・ω・)ノ At太频繁啦,提醒功能暂时宕机。
${e.error}`,
ctxt: root.locale['ctrl']['ok']
})
} else {
signUp(o).then(ret => {
mailEvt(o);
}).catch(x => {
//err(x)
})
}
})
}
Utils.on('click', submitBtn, submitEvt);
Utils.on('keydown', document, function (e) {
e = event || e;
let keyCode = e.keyCode || e.which || e.charCode;
let ctrlKey = e.ctrlKey || e.metaKey;
// Shortcut key
ctrlKey && keyCode === 13 && submitEvt()
// tab key
if (keyCode === 9) {
let focus = document.activeElement.id || ''
if (focus == 'veditor') {
e.preventDefault();
let _veditor = Utils.find(root.el, '.veditor');
_insertAtCaret(_veditor, ' ');
}
}
})
}
function Valine(options) {
return new ValineFactory(options)
}
module.exports = Valine;
module.exports.default = Valine;