feat(comments): full 1:1 zibll comment kernel for posts, forum, shop
Extract shared comment helpers (content/item/form/geo/actions), port GIF [g=] smilies + FixedInput/moveForm UX, wire close_comments/city/quick settings, REST comment get, and align product review chrome with zibll.
This commit is contained in:
1 parent
9edd39c33d
commit
276a2891ba
116 files changed
+2658
-1848
No files matched your search
+134
-415
@@ -1,422 +1,141 @@
|
||||
/**
|
||||
* AKSRT Comment Tools
|
||||
*
|
||||
* Handles comment toolbar actions (emoji, code, bold, italic, quote, link),
|
||||
* Ctrl+Enter submit, and quick quote from selection.
|
||||
*
|
||||
* @package AKSRT
|
||||
* Comment expand tools — zibll-style shortcode inserts ([g=] [code] [img=] quick).
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var COMMENT_TOOLS = {
|
||||
emojiPicker: null,
|
||||
quickReplyPanel: null,
|
||||
quickQuoteEnabled: false,
|
||||
quickReplyTemplates: [
|
||||
'感谢分享,这篇内容对我很有帮助!',
|
||||
'已收藏,后面会按这个思路继续研究。',
|
||||
'这个细节讲得很清楚,学习了。',
|
||||
'我这边也复现成功了,补充一点经验:',
|
||||
'先留个脚印,等我测试后再回来反馈结果。'
|
||||
],
|
||||
|
||||
init: function () {
|
||||
this.bindToolbar();
|
||||
this.bindCtrlEnter();
|
||||
this.bindQuickQuote();
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert text at cursor position in textarea
|
||||
*/
|
||||
insertAtCursor: function (textarea, before, after) {
|
||||
if (!textarea) return;
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var text = textarea.value;
|
||||
var selected = text.substring(start, end);
|
||||
var replacement = before + (selected || '') + (after || '');
|
||||
textarea.value = text.substring(0, start) + replacement + text.substring(end);
|
||||
textarea.focus();
|
||||
var newPos = start + before.length + selected.length;
|
||||
textarea.setSelectionRange(newPos, newPos);
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrap selected text with markers
|
||||
*/
|
||||
wrapSelection: function (textarea, marker) {
|
||||
this.insertAtCursor(textarea, marker, marker);
|
||||
},
|
||||
|
||||
uploadImage: function (btn, textarea) {
|
||||
if (!window.aksrtData || !aksrtData.commentUploadEnabled || !aksrtData.commentUploadUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/jpeg,image/png,image/gif,image/webp';
|
||||
input.style.display = 'none';
|
||||
document.body.appendChild(input);
|
||||
|
||||
var self = this;
|
||||
input.addEventListener('change', function () {
|
||||
var file = input.files && input.files[0];
|
||||
if (!file) {
|
||||
input.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
var maxSize = parseInt(aksrtData.commentUploadMaxSize || 0, 10) || (2 * 1024 * 1024);
|
||||
if (file.size > maxSize) {
|
||||
window.alert('图片大小不能超过 2MB');
|
||||
input.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
var originalHtml = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('comment_image', file);
|
||||
|
||||
fetch(aksrtData.commentUploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-WP-Nonce': aksrtData.restNonce || ''
|
||||
}
|
||||
})
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (payload) {
|
||||
if (!payload || payload.success !== true || !payload.url) {
|
||||
throw new Error((payload && payload.message) || '上传失败');
|
||||
}
|
||||
self.insertAtCursor(textarea, '[img]' + payload.url + '[/img]', '');
|
||||
})
|
||||
.catch(function (error) {
|
||||
window.alert(error && error.message ? error.message : '上传失败');
|
||||
})
|
||||
.finally(function () {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
input.remove();
|
||||
});
|
||||
});
|
||||
|
||||
input.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind toolbar button actions
|
||||
*/
|
||||
bindToolbar: function () {
|
||||
var self = this;
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-toolbar-action]');
|
||||
if (!btn) return;
|
||||
|
||||
var action = btn.getAttribute('data-toolbar-action');
|
||||
var form = btn.closest('.comment-form');
|
||||
var textarea = form ? form.querySelector('textarea') : document.getElementById('comment');
|
||||
if (!textarea) return;
|
||||
|
||||
switch (action) {
|
||||
case 'bold':
|
||||
self.wrapSelection(textarea, '**');
|
||||
break;
|
||||
case 'italic':
|
||||
self.wrapSelection(textarea, '*');
|
||||
break;
|
||||
case 'code':
|
||||
var selected = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
if (selected.indexOf('\n') >= 0) {
|
||||
self.insertAtCursor(textarea, '```\n', '\n```');
|
||||
} else {
|
||||
self.wrapSelection(textarea, '`');
|
||||
}
|
||||
break;
|
||||
case 'quote':
|
||||
self.insertAtCursor(textarea, '> ', '\n');
|
||||
break;
|
||||
case 'link':
|
||||
self.askUrl(btn, '链接地址', function (url) {
|
||||
if (url) {
|
||||
var linkText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd) || '链接文字';
|
||||
self.insertAtCursor(textarea, '[' + linkText + '](' + url + ')');
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'image':
|
||||
self.askUrl(btn, '图片地址', function (url) {
|
||||
if (url) {
|
||||
self.insertAtCursor(textarea, '[img]' + url + '[/img]');
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'upload-image':
|
||||
self.uploadImage(btn, textarea);
|
||||
break;
|
||||
case 'emoji':
|
||||
self.toggleEmojiPicker(btn, textarea);
|
||||
break;
|
||||
case 'quick-reply':
|
||||
self.toggleQuickReply(btn, textarea);
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Inline URL input — replaces native prompt() with a small input below the button.
|
||||
*/
|
||||
askUrl: function (anchor, placeholder, callback) {
|
||||
var existing = document.querySelector('.aksrt-inline-url-input');
|
||||
if (existing) existing.remove();
|
||||
|
||||
var input = document.createElement('input');
|
||||
input.type = 'url';
|
||||
input.className = 'aksrt-inline-url-input';
|
||||
input.placeholder = placeholder || 'https://';
|
||||
input.value = 'https://';
|
||||
input.style.cssText = 'display:block;width:100%;max-width:320px;margin:4px 0;padding:6px 10px;border:1px solid #dce3ec;border-radius:8px;font-size:13px;';
|
||||
|
||||
var parent = anchor.parentElement;
|
||||
parent.insertBefore(input, anchor.nextSibling);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
function done() {
|
||||
var val = input.value.trim();
|
||||
input.remove();
|
||||
callback(val);
|
||||
}
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); done(); }
|
||||
if (e.key === 'Escape') { input.remove(); callback(''); }
|
||||
});
|
||||
input.addEventListener('blur', function () { done(); });
|
||||
},
|
||||
|
||||
/**
|
||||
* Simple emoji picker
|
||||
*/
|
||||
toggleEmojiPicker: function (btn, textarea) {
|
||||
var self = this;
|
||||
this.removeQuickReplyPanel();
|
||||
if (this.emojiPicker) {
|
||||
this.emojiPicker.remove();
|
||||
this.emojiPicker = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var emojis = [
|
||||
{ key: 'grinning', emoji: '😀' },
|
||||
{ key: 'joy', emoji: '😂' },
|
||||
{ key: 'rofl', emoji: '🤣' },
|
||||
{ key: 'blush', emoji: '😊' },
|
||||
{ key: 'heart-eyes', emoji: '😍' },
|
||||
{ key: 'smiling-hearts', emoji: '🥰' },
|
||||
{ key: 'sunglasses', emoji: '😎' },
|
||||
{ key: 'thinking', emoji: '🤔' },
|
||||
{ key: 'cry', emoji: '😢' },
|
||||
{ key: 'sob', emoji: '😭' },
|
||||
{ key: 'angry', emoji: '😡' },
|
||||
{ key: 'pleading', emoji: '🥺' },
|
||||
{ key: 'scream', emoji: '😱' },
|
||||
{ key: 'rolling-eyes', emoji: '🙄' },
|
||||
{ key: 'sleeping', emoji: '😴' },
|
||||
{ key: 'nauseated', emoji: '🤮' },
|
||||
{ key: 'thumbs-up', emoji: '👍' },
|
||||
{ key: 'thumbs-down', emoji: '👎' },
|
||||
{ key: 'clap', emoji: '👏' },
|
||||
{ key: 'pray', emoji: '🙏' },
|
||||
{ key: 'muscle', emoji: '💪' },
|
||||
{ key: 'heart', emoji: '❤️' },
|
||||
{ key: 'fire', emoji: '🔥' },
|
||||
{ key: 'star', emoji: '⭐' },
|
||||
{ key: 'tada', emoji: '🎉' },
|
||||
{ key: 'confetti', emoji: '🎊' },
|
||||
{ key: 'check', emoji: '✅' },
|
||||
{ key: 'cross', emoji: '❌' },
|
||||
{ key: 'warning', emoji: '⚠️' },
|
||||
{ key: 'hundred', emoji: '💯' },
|
||||
{ key: 'ok', emoji: '🆗' },
|
||||
{ key: 'new', emoji: '🆕' }
|
||||
];
|
||||
|
||||
var picker = document.createElement('div');
|
||||
picker.className = 'aksrt-emoji-picker';
|
||||
picker.style.cssText = 'position:absolute;bottom:100%;left:0;background:#fff;border:1px solid #e8e8e8;border-radius:8px;padding:8px;display:grid;grid-template-columns:repeat(8,1fr);gap:4px;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,0.1);';
|
||||
|
||||
emojis.forEach(function (entry) {
|
||||
var item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.style.cssText = 'border:none;background:none;cursor:pointer;font-size:18px;padding:4px;border-radius:4px;transition:background 0.15s;';
|
||||
item.textContent = entry.emoji;
|
||||
item.setAttribute('title', entry.key);
|
||||
item.addEventListener('mouseenter', function () { item.style.background = '#f5f5f5'; });
|
||||
item.addEventListener('mouseleave', function () { item.style.background = 'none'; });
|
||||
item.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
self.insertAtCursor(textarea, '[g=' + entry.key + ']', '');
|
||||
picker.remove();
|
||||
self.emojiPicker = null;
|
||||
});
|
||||
picker.appendChild(item);
|
||||
});
|
||||
|
||||
btn.style.position = 'relative';
|
||||
btn.appendChild(picker);
|
||||
this.emojiPicker = picker;
|
||||
|
||||
// Close on outside click
|
||||
var closeHandler = function (e) {
|
||||
if (!picker.contains(e.target) && e.target !== btn) {
|
||||
picker.remove();
|
||||
self.emojiPicker = null;
|
||||
document.removeEventListener('click', closeHandler);
|
||||
}
|
||||
};
|
||||
setTimeout(function () { document.addEventListener('click', closeHandler); }, 0);
|
||||
},
|
||||
|
||||
removeQuickReplyPanel: function () {
|
||||
if (this.quickReplyPanel) {
|
||||
this.quickReplyPanel.remove();
|
||||
this.quickReplyPanel = null;
|
||||
}
|
||||
},
|
||||
|
||||
toggleQuickReply: function (btn, textarea) {
|
||||
var self = this;
|
||||
if (this.quickReplyPanel) {
|
||||
this.removeQuickReplyPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.emojiPicker) {
|
||||
this.emojiPicker.remove();
|
||||
this.emojiPicker = null;
|
||||
}
|
||||
|
||||
var panel = document.createElement('div');
|
||||
panel.className = 'comment-quick-reply';
|
||||
|
||||
var title = document.createElement('div');
|
||||
title.className = 'comment-quick-reply__title';
|
||||
title.textContent = '快捷回复';
|
||||
panel.appendChild(title);
|
||||
|
||||
var templates = document.createElement('div');
|
||||
templates.className = 'comment-quick-reply__templates';
|
||||
|
||||
this.quickReplyTemplates.forEach(function (text) {
|
||||
var item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'comment-quick-reply__template';
|
||||
item.textContent = text;
|
||||
item.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
var needsLineBreak = textarea.value && !/\n\s*$/.test(textarea.value);
|
||||
var prefix = needsLineBreak ? '\n' : '';
|
||||
self.insertAtCursor(textarea, prefix + text, '');
|
||||
self.removeQuickReplyPanel();
|
||||
});
|
||||
templates.appendChild(item);
|
||||
});
|
||||
|
||||
panel.appendChild(templates);
|
||||
|
||||
var toolbar = btn.closest('.comment-toolbar');
|
||||
if (toolbar && toolbar.parentNode) {
|
||||
toolbar.parentNode.insertBefore(panel, toolbar.nextSibling);
|
||||
} else {
|
||||
btn.parentNode.appendChild(panel);
|
||||
}
|
||||
|
||||
this.quickReplyPanel = panel;
|
||||
|
||||
var closeHandler = function (e) {
|
||||
if (!panel.contains(e.target) && e.target !== btn) {
|
||||
self.removeQuickReplyPanel();
|
||||
document.removeEventListener('click', closeHandler);
|
||||
}
|
||||
};
|
||||
setTimeout(function () { document.addEventListener('click', closeHandler); }, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Ctrl+Enter to submit comment
|
||||
*/
|
||||
bindCtrlEnter: function () {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
var textarea = e.target;
|
||||
if (textarea.tagName !== 'TEXTAREA') return;
|
||||
var form = textarea.closest('form');
|
||||
if (!form) return;
|
||||
var submitBtn = form.querySelector('[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Quick quote: select text on page, show "quote in comment" button
|
||||
*/
|
||||
bindQuickQuote: function () {
|
||||
if (!this.quickQuoteEnabled) return;
|
||||
var self = this;
|
||||
var quoteBtn = null;
|
||||
|
||||
document.addEventListener('mouseup', function () {
|
||||
setTimeout(function () {
|
||||
if (quoteBtn) {
|
||||
quoteBtn.remove();
|
||||
quoteBtn = null;
|
||||
}
|
||||
|
||||
var selection = window.getSelection();
|
||||
var text = selection.toString().trim();
|
||||
if (text.length < 5 || text.length > 500) return;
|
||||
|
||||
var range = selection.getRangeAt(0);
|
||||
var rect = range.getBoundingClientRect();
|
||||
|
||||
quoteBtn = document.createElement('button');
|
||||
quoteBtn.type = 'button';
|
||||
quoteBtn.textContent = '引用评论';
|
||||
quoteBtn.style.cssText = 'position:fixed;top:' + (rect.top - 36) + 'px;left:' + (rect.left + rect.width / 2) + 'px;transform:translateX(-50%);padding:4px 12px;background:#1890ff;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.15);';
|
||||
quoteBtn.addEventListener('click', function () {
|
||||
var textarea = document.getElementById('comment');
|
||||
if (textarea) {
|
||||
var quoted = text.split('\n').map(function (line) { return '> ' + line; }).join('\n') + '\n\n';
|
||||
self.insertAtCursor(textarea, quoted, '');
|
||||
textarea.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
textarea.focus();
|
||||
}
|
||||
quoteBtn.remove();
|
||||
quoteBtn = null;
|
||||
});
|
||||
document.body.appendChild(quoteBtn);
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () { COMMENT_TOOLS.init(); });
|
||||
} else {
|
||||
COMMENT_TOOLS.init();
|
||||
function notify(msg) {
|
||||
if (window.aksrt && typeof window.aksrt.toast === 'function') {
|
||||
window.aksrt.toast(msg, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function findTextarea(from) {
|
||||
var form = from && from.closest ? from.closest('form') : null;
|
||||
if (form) {
|
||||
return form.querySelector('textarea.grin, textarea#comment, textarea[name="comment"]');
|
||||
}
|
||||
return document.querySelector('#comment, textarea.grin');
|
||||
}
|
||||
|
||||
function insertAtCursor(textarea, value) {
|
||||
if (!textarea) return;
|
||||
var start = textarea.selectionStart || 0;
|
||||
var end = textarea.selectionEnd || 0;
|
||||
var val = textarea.value || '';
|
||||
textarea.value = val.slice(0, start) + value + val.slice(end);
|
||||
var pos = start + value.length;
|
||||
textarea.selectionStart = textarea.selectionEnd = pos;
|
||||
textarea.focus();
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
function restUpload(file) {
|
||||
var d = window.aksrtData || {};
|
||||
var url = d.commentUploadUrl || ((d.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '') + '/comment/upload-image');
|
||||
var fd = new FormData();
|
||||
fd.append('image', file);
|
||||
fd.append('file', file);
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'X-WP-Nonce': d.restNonce || d.nonce || '' },
|
||||
credentials: 'same-origin',
|
||||
body: fd
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var sm = e.target && e.target.closest ? e.target.closest('[data-smilie], a.smilie-icon') : null;
|
||||
if (sm) {
|
||||
e.preventDefault();
|
||||
var key = sm.getAttribute('data-smilie');
|
||||
if (!key && sm.querySelector) {
|
||||
var img = sm.querySelector('[data-smilie]');
|
||||
if (img) key = img.getAttribute('data-smilie');
|
||||
}
|
||||
if (!key) return;
|
||||
insertAtCursor(findTextarea(sm), '[g=' + key + ']');
|
||||
var wrap = sm.closest('.dropup, .input-expand-wrap');
|
||||
if (wrap) wrap.classList.remove('open');
|
||||
return;
|
||||
}
|
||||
|
||||
var codeBtn = e.target && e.target.closest ? e.target.closest('.input-code-submit') : null;
|
||||
if (codeBtn) {
|
||||
e.preventDefault();
|
||||
var box = codeBtn.closest('.dropdown-code, .input-expand-code');
|
||||
var ta = box ? box.querySelector('textarea') : null;
|
||||
var code = ta ? ta.value : '';
|
||||
if (!code || code.length < 2) {
|
||||
notify('请输入代码');
|
||||
return;
|
||||
}
|
||||
insertAtCursor(findTextarea(codeBtn), '[code]\n' + code + '\n[/code]\n');
|
||||
if (ta) ta.value = '';
|
||||
var w = codeBtn.closest('.dropup, .input-expand-wrap');
|
||||
if (w) w.classList.remove('open');
|
||||
return;
|
||||
}
|
||||
|
||||
var imgBtn = e.target && e.target.closest ? e.target.closest('.input-image-submit') : null;
|
||||
if (imgBtn) {
|
||||
e.preventDefault();
|
||||
var ibox = imgBtn.closest('.dropdown-image, .input-expand-image');
|
||||
var input = ibox ? ibox.querySelector('.input-image-url') : null;
|
||||
var url = input ? (input.value || '').trim() : '';
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
notify('请输入有效图片地址');
|
||||
return;
|
||||
}
|
||||
insertAtCursor(findTextarea(imgBtn), '[img=' + url + ']');
|
||||
if (input) input.value = '';
|
||||
var iw = imgBtn.closest('.dropup, .input-expand-wrap');
|
||||
if (iw) iw.classList.remove('open');
|
||||
return;
|
||||
}
|
||||
|
||||
var upBtn = e.target && e.target.closest ? e.target.closest('.input-image-upload') : null;
|
||||
if (upBtn) {
|
||||
e.preventDefault();
|
||||
var fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/jpeg,image/png,image/gif,image/webp';
|
||||
fileInput.addEventListener('change', function () {
|
||||
var file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
restUpload(file)
|
||||
.then(function (json) {
|
||||
var u = (json && (json.url || json.source_url || (json.data && (json.data.url || json.data.source_url)))) || '';
|
||||
if (!u) {
|
||||
notify((json && json.message) || '上传失败');
|
||||
return;
|
||||
}
|
||||
insertAtCursor(findTextarea(upBtn), '[img=' + u + ']');
|
||||
})
|
||||
.catch(function () { notify('上传失败'); });
|
||||
});
|
||||
fileInput.click();
|
||||
return;
|
||||
}
|
||||
|
||||
var q = e.target && e.target.closest ? e.target.closest('.quick-reply-item') : null;
|
||||
if (q) {
|
||||
e.preventDefault();
|
||||
var phrase = q.getAttribute('data-text') || q.textContent || '';
|
||||
insertAtCursor(findTextarea(q), phrase);
|
||||
var qw = q.closest('.dropup, .input-expand-wrap');
|
||||
if (qw) qw.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (!(e.ctrlKey || e.metaKey) || e.key !== 'Enter') return;
|
||||
var ta = e.target;
|
||||
if (!ta || ta.id !== 'comment') return;
|
||||
var form = ta.closest('#commentform');
|
||||
if (!form) return;
|
||||
e.preventDefault();
|
||||
var submit = form.querySelector('#submit, [type="submit"]');
|
||||
if (submit) submit.click();
|
||||
});
|
||||
})();
|
||||
Reference in new issue
Block a user