xiaojiji
Version:
A simple comment system based on sqlite.
1,073 lines (1,008 loc) • 39.2 kB
JavaScript
const VERSION = require('../package.json').version;
const md5 = require('blueimp-md5');
import { marked } from 'marked';
const autosize = require('autosize');
const timeAgo = require('./utils/timeago');
const detect = require('./utils/detect');
const Utils = require('./utils/domUtils');
const Emoji = require('./plugins/emojis');
const hanabi = require('hanabi');
const uuid = require('uuid')
import AV from './services/api';
import qs from 'querystring'
const defaultComment = {
comment: 'test comment',
nick: '书虫',
mail: 'guohb@163.com',
link: 'http://127.0.0.1:8088/',
ua: navigator.userAgent,
url: '',
objectId: '',
insertedAt: ''
};
const blogComment = {
QQAvatar: '',
comment: '',
createdAt: '',
insertedAt:'',
link: '',
mail: '',
nick: '',
objectId: '',
ua:'',
updatedAt:'',
url: '',
pid: '',
rid: ''
}
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;
function ValineFactory(option) {
let root = this;
root.init(option);
// Valine init
return root;
}
/**
* Valine Init
*/
ValineFactory.prototype.init = function (option) {
let root = this;
root['config'] = option
if (typeof document === 'undefined') {
console && console.warn('Sorry, Valine does not support Server-side rendering.')
return;
}
!!option && root._init();
return root;
}
ValineFactory.prototype._init = function(){
let root = this;
try {
let {
lang,
langMode,
avatar,
avatarForce,
avatar_cdn,
notify,
verify,
visitor,
path = location.pathname,
pageSize,
recordIP,
clazzName = 'Comment'
} = root.config;
root['config']['path'] = path.replace(/index\.html?$/, '');
root['config']['clazzName'] = clazzName;
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;
_avatarSetting['params'] = `?d=${(ds.indexOf(avatar) > -1 ? avatar : 'mp')}&v=${VERSION}${force}`;
_avatarSetting['hide'] = avatar === 'hide' ? true : false;
_avatarSetting['cdn'] = /^https?\:\/\//.test(avatar_cdn) ? avatar_cdn : _avatarSetting['cdn']
let size = Number(pageSize || 10);
root.config.pageSize = !isNaN(size) ? (size < 1 ? 10 : size) : 10;
marked.setOptions({
renderer: new marked.Renderer(),
highlight: root.config.highlight === false ? null : hanabi,
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: true
});
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;
}
}
// get comment count
let els = Utils.findAll(document, '.valine-comment-count');
Utils.each(els, (idx, el) => {
if (el) {
let k = Utils.attr(el, 'data-xid');
if (k) {
root.Q(k)
.then(response => {
el.innerText = response.data.data.length
})
.catch(ex => {
el.innerText = 0
})
// root.Q(k).count().then(n => {
// el.innerText = n
// }).catch(ex => {
// el.innerText = 0
// })
}
}
})
// Counter
// visitor && CounterFactory.add(AV.Object.extend('Counter'),root.config.path);
let el = root.config.el || null;
let _el = Utils.findAll(document, el);
el = el instanceof HTMLElement ? el : (_el[_el.length - 1] || null);
if (!el) return;
root.el = el;
try{root.el.classList.add('v');}catch(ex){root.el.setAttribute('class',root.el.getAttribute('class')+' v')}
_avatarSetting['hide'] && root.el.classList.add('hide-avatar');
root.config.meta = (root.config.guest_info || root.config.meta || META).filter(item => META.indexOf(item) > -1);
let inputEl = (root.config.meta.length == 0 ? META : root.config.meta).map(item => {
let _t = item == 'mail' ? 'email' : 'text';
return META.indexOf(item) > -1 ? `<input name="${item}" placeholder="${root.locale['head'][item]}" class="v${item} vinput" type="${_t}">` : ''
});
root.placeholder = root.config.placeholder || 'Just Go Go';
root.el.innerHTML = `<div class="vwrap"><div class="${`vheader item${inputEl.length}`}">${inputEl.join('')}</div><div class="vedit"><textarea id="veditor" class="veditor vinput" placeholder="${root.placeholder}"></textarea><div class="vctrl"><span class="vemoji-btn">${root.locale['ctrl']['emoji']}</span> | <span class="vpreview-btn">${root.locale['ctrl']['preview']}</span></div><div class="vemojis" style="display:none;"></div><div class="vinput vpreview" style="display:none;"></div></div><div class="vcontrol"><div class="col col-20" title="Markdown is supported"><a href="https://segmentfault.com/markdown" target="_blank"><svg class="markdown" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg></a></div><div class="col col-80 text-right"><button type="button" title="Cmd|Ctrl+Enter" class="vsubmit vbtn">${root.locale['ctrl']['reply']}</button></div></div><div style="display:none;" class="vmark"></div></div><div class="vinfo" style="display:none;"><div class="vcount col"></div></div><div class="vlist"></div><div class="vempty" style="display:none;"></div><div class="vpage txt-center"></div><div class="info"><div class="power txt-right">Powered By <a href="https://valine.js.org" target="_blank">Valine</a><br>v${VERSION}</div></div>`;
// Empty Data
let vempty = Utils.find(root.el, '.vempty');
root.nodata = {
show(txt) {
vempty.innerHTML = txt || root.locale['tips']['sofa'];
Utils.attr(vempty, 'style', 'display:block;');
return root;
},
hide() {
Utils.attr(vempty, 'style', 'display:none;');
return root;
}
}
// loading
let _spinner = Utils.create('div', 'class', 'vloading');
// loading control
let _vlist = Utils.find(root.el, '.vlist');
root.loading = {
show(mt) {
let _vlis = Utils.findAll(_vlist, '.vcard');
if (mt) _vlist.insertBefore(_spinner, _vlis[0]);
else _vlist.appendChild(_spinner);
root.nodata.hide();
return root;
},
hide() {
let _loading = Utils.find(_vlist, '.vloading');
if (_loading) Utils.remove(_loading);
Utils.findAll(_vlist, '.vcard').length === 0 && root.nodata.show()
return root;
}
};
// alert
let _mark = Utils.find(root.el, '.vmark');
root.alert = {
/**
* {
* type:0/1,
* text:'',
* ctxt:'',
* otxt:'',
* cb:fn
* }
*
* @param {Object} o
*/
show(o) {
_mark.innerHTML = `<div class="valert txt-center"><div class="vtext">${o && o.text || 1}</div><div class="vbtns"></div></div>`;
let _vbtns = Utils.find(_mark, '.vbtns');
let _cBtn = `<button class="vcancel vbtn">${ o && o.ctxt || root.locale['ctrl']['cancel'] }</button>`;
let _oBtn = `<button class="vsure vbtn">${ o && o.otxt || root.locale['ctrl']['sure'] }</button>`;
_vbtns.innerHTML = `${_cBtn}${o && o.type && _oBtn}`;
Utils.on('click', Utils.find(_mark, '.vcancel'), (e) => {
root.alert.hide();
})
Utils.attr(_mark, 'style', 'display:block;');
if (o && o.type) {
let _ok = Utils.find(_mark, '.vsure');
Utils.on('click', _ok, (e) => {
root.alert.hide();
o.cb && o.cb();
});
}
return root;
},
hide() {
Utils.attr(_mark, 'style', 'display:none;');
return root;
}
}
// Bind Event
root.bind();
} catch (ex) {
root.ErrorHandler(ex,'init')
}
}
// 新建Counter对象
// let createCounter = function (Counter, o) {
// let newCounter = new Counter();
// let acl = new AV.ACL();
// acl.setPublicReadAccess(true);
// acl.setPublicWriteAccess(true);
// newCounter.setACL(acl);
// newCounter.set('url', o.url)
// newCounter.set('xid', o.xid)
// newCounter.set('title', o.title)
// newCounter.set('time', 1)
// newCounter.save().then(ret => {
// Utils.find(o.el, '.leancloud-visitors-count').innerText = 1
// }).catch(ex => {
// console.log(ex)
// });
// }
let CounterFactory = {
// add(Counter,currPath) {
// let root = this
// let lvs = Utils.findAll(document, '.leancloud_visitors,.leancloud-visitors');
// if (lvs.length) {
// let lv = lvs[0];
// let url = Utils.attr(lv, 'id');
// let title = Utils.attr(lv, 'data-flag-title');
// let xid = encodeURI(url);
// let o = {
// el: lv,
// url: url,
// xid: xid,
// title: title
// }
// // 判断是否需要+1
// // if (decodeURI(url) === decodeURI(currPath)) {
// // let query = new AV.Query(Counter);
// // query.equalTo('url', url);
// // query.find().then(ret => {
// // if (ret.length > 0) {
// // let v = ret[0];
// // v.increment("time");
// // v.save().then(rt => {
// // Utils.find(lv, '.leancloud-visitors-count').innerText = rt.get('time')
// // }).catch(ex => {
// // console.log(ex)
// // });
// // } else {
// // createCounter(Counter, o)
// // }
// // }).catch(ex => {
// // ex.code == 101 && createCounter(Counter, o)
// // })
// // } else CounterFactory.show(Counter, lvs)
// }
// },
// show(Counter, lvs) {
// let COUNT_CONTAINER_REF = '.leancloud-visitors-count';
//
// // 重置所有计数
// Utils.each(lvs, (idx, el) => {
// let cel = Utils.find(el, COUNT_CONTAINER_REF);
// if (cel) cel.innerText = 0
// })
// let urls = [];
// for (let i in lvs) {
// if (lvs.hasOwnProperty(i)) urls.push(Utils.attr(lvs[i], 'id'))
// }
// if (urls.length) {
// let query = new AV.Query(Counter);
// query.containedIn('url', urls);
// query.find().then(ret => {
// if (ret.length > 0) {
// Utils.each(ret, (idx, item) => {
// let url = item.get('url');
// let time = item.get('time');
// let els = Utils.findAll(document, `.leancloud_visitors[id="${url}"],.leancloud-visitors[id="${url}"]`);
// Utils.each(els, (idx, el) => {
// let cel = Utils.find(el, COUNT_CONTAINER_REF);
// if (cel) cel.innerText = time
// })
// });
// }
// }).catch(ex => {
// console.error(ex)
// })
// }
// }
}
/**
* LeanCloud SDK Query Util
* @param {String} url
* @param {String} id
*/
ValineFactory.prototype.Q = function (k) {
let root = this;
let len = arguments.length;
if (len == 1) {
if (k === '/') {
return AV.get('/')
} else {
return AV.get('queryComments/' + encodeURIComponent(k))
}
} else {
// let ids = JSON.stringify(arguments[1]).replace(/(\[|\])/g, '');
// let cql = `select * from ${root['config']['clazzName']} where rid in (${ids}) order by -createdAt,-createdAt`;
// return AV.Query.doCloudQuery(cql)
return AV.get(arguments[0]+arguments[1])
}
}
ValineFactory.prototype.PP = function (api, body) {
let root = this;
return AV.post(api, body)
// let len = arguments.length;
// if (len == 1) {
// // let notExist = new AV.Query(root['config']['clazzName']);
// // notExist.doesNotExist('rid');
// // let isEmpty = new AV.Query(root['config']['clazzName']);
// // isEmpty.equalTo('rid', '');
// // let q = AV.Query.or(notExist, isEmpty);
// // if (k === '*') q.exists('url');
// // else q.equalTo('url', decodeURI(k));
// // q.addDescending('createdAt');
// // q.addDescending('insertedAt');
// // return q;
// // return AV.get('queryComments/'+decodeURI(k))
// return AV.get('queryComments/%2F2022%2F04%2F17%2FScrum-Master%2F')
// } else {
// let ids = JSON.stringify(arguments[1]).replace(/(\[|\])/g, '');
// // let cql = `select * from ${root['config']['clazzName']} where rid in (${ids}) order by -createdAt,-createdAt`;
// // return AV.Query.doCloudQuery(cql)
// return AV.get('queryChildComments/'+ids)
// }
}
ValineFactory.prototype.ErrorHandler = function (ex,origin) {
console.log(origin)
console.error(ex)
console.error(ex.code,ex.message)
let root = this;
root.el && root.loading.hide().nodata.hide()
if (({}).toString.call(ex) === "[object Error]") {
let code = ex.code || '',
t = root.locale['error'][code],
msg = t || ex.message || ex.error || '';
if (code == 101) root.nodata.show()
else root.el && root.nodata.show(`<pre style="text-align:left;">Code ${code}: ${msg}</pre>`) ||
console && console.error(`Code ${code}: ${msg}`)
} else {
root.el && root.nodata.show(`<pre style="text-align:left;">${JSON.stringify(ex)}</pre>`) ||
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) {
this.config.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 _veditor = Utils.find(root.el, '.veditor');
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) => {
_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) {
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
*/
function 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 = root.config.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, "")).substring(0,20);
});
}
}
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 = root.config.pageSize;
let count = Number(Utils.find(root.el, '.vnum').innerText);
root.loading.show();
// let cql = AV.get('queryComments111'+root.config.path)
// 最多获取 10 条结果
// cq.limit(size);
// cq.skip((no - 1) * size);
// cq.find().then(rets => {
root.Q(root.config.path)
.then(res => {
let len = res.data.data.length;
let rids = []
for (let i = 0; i < len; i++) {
let ret = res.data.data[i];
rids.push(ret.objectId)
if (ret.rid === "" || ret.rid === null) {
insertDom(ret, Utils.find(root.el, '.vlist'), !0)
}
}
// load children comment
// AV.get("queryChildComments/"+ rids).then(ret => {
rids.forEach(e =>{
console.log("queryChildComments " + e )
root.Q("queryChildComments/", e).then(ret => {
let childs = ret && ret.data.data || []
for (let k = 0; k < childs.length; k++) {
let child = childs[k];
insertDom(child, createVquote(child.rid))
}
})
})
let _vpage = Utils.find(root.el, '.vpage');
_vpage.innerHTML = size * no < count ? `<button type="button" class="vmore vbtn">${root.locale['ctrl']['more']}</button>` : '';
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,'query')
})
}
root.Q(root.config.path).then(rets => {
if (rets.data.data.length > 0) {
Utils.attr(Utils.find(root.el, '.vinfo'), 'style', 'display:block;');
Utils.find(root.el, '.vcount').innerHTML = `<span class="vnum">${rets.data.data.length}</span> ${root.locale['tips']['comments']}`;
query();
} else {
root.loading.hide();
}
}).catch(ex => {
root.ErrorHandler(ex,'count')
});
let insertDom = (rt, node, mt) => {
console.log(rt.comment + " insert " + rt.insertedAt + ", update " + rt.updatedAt + ", created " + rt.createdAt)
console.log()
let _vcard = Utils.create('div', {
'class': 'vcard',
'id': rt.objectId
});
let _img = _avatarSetting['hide'] ? '' : `<img class="vimg" src="../assets/images/avatar.jpg">`;
let ua = rt.ua || '';
let uaMeta = '';
if (ua) {
ua = detect(ua);
let browser = `<span class="vsys">${ua.browser} ${ua.version}</span>`;
let os = `<span class="vsys">${ua.os} ${ua.osVersion}</span>`;
uaMeta = `${browser} ${os}`;
}
if(root.config.path === '*') uaMeta = `<a href="${rt.url}" class="vsys">${rt.url}</a>`
let _nick = '';
let _t = rt.link?(/^https?\:\/\//.test(rt.link) ? rt.link : 'http://'+rt.link) : '';
_nick = _t ? `<a class="vnick" rel="nofollow" href="${_t}" target="_blank" >${rt.nick}</a>` : `<span class="vnick">${rt.nick}</span>`;
_vcard.innerHTML = `${_img}
<div class="vh" rootid=${/*rt.rid || */rt.objectId}>
<div class="vhead">${_nick} ${uaMeta}</div>
<div class="vmeta">
<span class="vtime">${timeAgo(new Date(rt.updatedAt) || new Date(rt.createdAt),root.locale)}</span>
<span class="vat">${root.locale['ctrl']['reply']}</span>
</div>
<div class="vcontent">
${xssFilter(rt.comment)}
</div>
</div>`;
let _vat = Utils.find(_vcard, '.vat');
let _as = Utils.findAll(_vcard, 'a');
for (let i = 0, len = _as.length; i < len; i++) {
let _a = _as[i];
if (_a && (Utils.attr(_a, 'class') || '').indexOf('at') == -1) {
Utils.attr(_a, {
'target': '_blank',
'rel': 'nofollow'
});
}
}
let _vlis = Utils.findAll(node, '.vcard');
if (mt) node.appendChild(_vcard);
else node.insertBefore(_vcard, _vlis[0]);
let _vcontent = Utils.find(_vcard, '.vcontent');
if (_vcontent) expandEvt(_vcontent);
if (_vat) bindAtEvt(_vat, rt);
_activeOtherFn()
}
let _activeOtherFn = () => {
setTimeout(function () {
try {
// let MathJax = MathJax || '';
typeof MathJax !== 'undefined' && MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
if (typeof hljs !== 'undefined') {
Utils.each(Utils.findAll('pre code'), function (i, block) {
hljs.highlightBlock(block);
})
Utils.each(Utils.findAll('code.hljs'), function (i, block) {
hljs.lineNumbersBlock(block);
});
}
} catch (ex) {}
}, 200)
}
// expand event
let expandEvt = (el) => {
setTimeout(function () {
if (el.offsetHeight > 180) {
el.classList.add('expand');
Utils.on('click', el, e => {
Utils.attr(el, 'class', 'vcontent');
})
}
})
}
let atData = {}
// at event
let bindAtEvt = (el, rt) => {
Utils.on('click', el, (e) => {
let at = `@${Utils.escape(rt.nick)}`;
atData = {
'at': Utils.escape(at) + ' ',
'rid': /*rt.rid ||*/ rt.objectId,
'pid': rt.objectId,
'rmail': rt.mail,
}
console.log(atData)
Utils.attr(inputs['comment'], 'placeholder', at);
inputs['comment'].focus();
})
}
// cache
let getCache = () => {
let s = _store && _store.ValineCache;
if (s) {
s = JSON.parse(s);
let m = meta;
for (let i in m) {
let k = m[i];
Utils.find(root.el, `.v${k}`).value = Utils.unescape(s[k]);
defaultComment[k] = s[k];
}
}
}
getCache();
// reset form
let reset = () => {
defaultComment['comment'] = "";
inputs['comment'].value = "";
syncContentEvt(inputs['comment'])
Utils.attr(inputs['comment'], 'placeholder', root.placeholder);
atData = {};
root.preview.empty().hide();
}
// submitsubmit
let submitBtn = Utils.find(root.el, '.vsubmit');
let submitEvt = (e) => {
if (Utils.attr(submitBtn, 'disabled')) {
root.alert.show({
type: 0,
text: `${root.locale['tips']['busy']}ヾ(๑╹◡╹)ノ"`,
ctxt: root.locale['ctrl']['ok']
})
return;
}
if (defaultComment['nick'].length < 3) {
inputs['nick'].focus();
return;
}
if (defaultComment['mail'].length < 6 || defaultComment['mail'].indexOf('@') < 1 || defaultComment['mail'].indexOf('.') < 3) {
inputs['mail'].focus();
return;
}
if (defaultComment['comment'] == '') {
inputs['comment'].focus();
return;
}
defaultComment['nick'] = defaultComment['nick'] || 'Anonymous';
// return;
if (root.notify || root.verify) {
verifyEvt(commitEvt)
} else {
commitEvt();
}
}
let commitEvt = () => {
Utils.attr(submitBtn, 'disabled', !0);
root.loading.show(!0);
// 声明类型
// let Ct = AV.Object.extend(root.config.clazzName || 'Comment');
// 新建对象
// let comment = new Ct();
defaultComment['url'] = decodeURI(root.config.path);
defaultComment['insertedAt'] = new Date();
defaultComment['objectId'] = uuid.v1()
if (atData['rid']) {
let pid = atData['pid'] || atData['rid'];
blogComment['rid'] = atData['rid'];
blogComment['pid'] = pid;
defaultComment['comment'] = defaultComment['comment'].replace('<p>', `<p><a class="at" href="#${pid}">${atData['at']}</a> , `);
}
for (let i in defaultComment) {
if (defaultComment.hasOwnProperty(i)) {
let _v = defaultComment[i];
blogComment[i] = _v;
}
}
root.PP("addComment", qs.stringify(blogComment)).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(blogComment, vquote, !0)
} else {
if (_count) {
num = Number(_count.innerText) + 1;
_count.innerText = num;
} else {
Utils.find(root.el, '.vcount').innerHTML = '<span class="num">1</span> ' + root.locale['tips']['comments']
}
insertDom(blogComment, Utils.find(root.el, '.vlist'));
root.config.pageSize++
}
Utils.removeAttr(submitBtn, 'disabled');
root.loading.hide();
reset();
} catch (ex) {
root.ErrorHandler(ex,'save');
}
}).catch(ex => {
root.ErrorHandler(ex,'commitEvt');
})
}
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} = <input class='vcode vinput' >`;
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;
}
})
}
}
})
}
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();
_insertAtCaret(_veditor, ' ');
}
}
});
Utils.on('paste',document,(e)=>{
let clipboardData = "clipboardData" in e ? e.clipboardData : (e.originalEvent && e.originalEvent.clipboardData || window.clipboardData)
let items = clipboardData && clipboardData.items;
let files = [];
if (items && items.length>0) {
// 检索剪切板items
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
files.push(items[i].getAsFile());
break;
}
}
if(files.length) {
for(let idx in files){
let file = files[idx],
uploadText = `![Uploading ${file['name']}]()`;
_insertAtCaret(_veditor, uploadText);
file && uploadImage(file,function(err,ret){
if(!err && ret) _veditor.value = _veditor.value.replace(uploadText,`\r\n![${file['name']}](${ret['data']})`)
})
}
}
}
})
let uploadImage = (file,callback)=>{
let formData = new FormData();
formData.append('file', file);
let xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
try {
let json = JSON.parse(xhr.responseText);
callback && callback(null,json)
} catch (err) {
callback && callback(err)
}
} else {
callback && callback(xhr.status)
}
}
xhr.onerror = function(e){
console.log(e)
}
// xhr.open('POST', 'https://sm.ms/api/v2/upload?inajax=1',true);
// xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.open('POST','https://imgkr.com/api/files/upload',true);
xhr.send(formData);
}
}
function Valine(options) {
return new ValineFactory(options)
}
module.exports = Valine;
module.exports.default = Valine;