diff --git a/lib/migrator.js b/lib/migrator.js
index f42722e..28b3b89 100644
--- a/lib/migrator.js
+++ b/lib/migrator.js
@@ -1,17 +1,16 @@
'use strict';
-const posts = require.main.require('./src/posts');
-
const MarkdownIt = require('markdown-it');
const markdown = new MarkdownIt();
const { QuillDeltaToHtmlConverter } = require('quill-delta-to-html');
+const { TableParser } = require('quill-v1-table/TableDeltaToHtml');
const isHtml = require('is-html');
-const winston = require.main.require('winston');
-
const Migrator = module.exports;
+Migrator.resolveNbb = id => require.main.require(id);
+
Migrator.detect = (postObj) => {
const isHtml = Migrator.isHtml(postObj);
@@ -37,21 +36,90 @@ Migrator.isHtml = postObj => isHtml(postObj.content);
Migrator.isMarkdown = postObj => !Migrator.isHTML(postObj);
-Migrator.toHtml = (content) => {
- try {
- content = JSON.parse(content);
- const converter = new QuillDeltaToHtmlConverter(content.ops, {});
-
- // Quill plugin should fire a hook here, passing converter.renderCustomWith
- // Emoji plugin should take that method and register a listener.
- // Also toHtml is probably going to end up being asynchronous, then... awaited?
- converter.renderCustomWith((customOp) => {
- if (customOp.insert.type === 'emoji') {
- return `
`;
+function registerEmojiOnConverter(converter) {
+ converter.renderCustomWith((customOp) => {
+ if (customOp.insert.type === 'emoji') {
+ return `
`;
+ }
+ });
+}
+
+function segmentDeltaOps(ops) {
+ const segments = [];
+ let i = 0;
+ while (i < ops.length) {
+ const op = ops[i];
+ const next = ops[i + 1];
+ const opTd = !!(op.attributes && op.attributes.td);
+ const nextTd = !!(next && next.attributes && next.attributes.td);
+
+ if (opTd || nextTd) {
+ const tableOps = [];
+ while (i < ops.length) {
+ const o = ops[i];
+ const n = ops[i + 1];
+ const oTd = !!(o.attributes && o.attributes.td);
+ const nTd = !!(n && n.attributes && n.attributes.td);
+ if (oTd || nTd) {
+ tableOps.push(o);
+ i += 1;
+ } else {
+ break;
+ }
+ }
+ segments.push({ type: 'table', ops: tableOps });
+ } else {
+ const textOps = [];
+ while (i < ops.length) {
+ const o = ops[i];
+ const n = ops[i + 1];
+ const oTd = !!(o.attributes && o.attributes.td);
+ const nTd = !!(n && n.attributes && n.attributes.td);
+ if (oTd || nTd) {
+ break;
+ }
+ textOps.push(o);
+ i += 1;
}
- });
+ segments.push({ type: 'text', ops: textOps });
+ }
+ }
+ return segments;
+}
- return posts.sanitize(converter.convert());
+Migrator.convertMixedDeltaToHtml = (delta) => {
+ const {ops} = delta;
+ if (!Array.isArray(ops)) {
+ return '';
+ }
+ const parts = [];
+ const segments = segmentDeltaOps(ops);
+ for (let s = 0; s < segments.length; s += 1) {
+ const seg = segments[s];
+ if (seg.type === 'table') {
+ const parser = new TableParser(seg.ops);
+ parser.parse();
+ parts.push(parser.toHTML());
+ } else if (seg.ops.length > 0) {
+ const converter = new QuillDeltaToHtmlConverter(seg.ops, {});
+ registerEmojiOnConverter(converter);
+ parts.push(converter.convert());
+ }
+ }
+ return parts.join('');
+};
+
+Migrator.toHtml = (content) => {
+ const posts = Migrator.resolveNbb('./src/posts');
+ const winston = Migrator.resolveNbb('winston');
+ try {
+ const delta = JSON.parse(content);
+ if (!delta || !Array.isArray(delta.ops)) {
+ winston.verbose('[plugin/composer-quill (toHtml)] Input not in expected format, skipping.');
+ return false;
+ }
+ const html = Migrator.convertMixedDeltaToHtml(delta);
+ return posts.sanitize(html);
} catch (e) {
// Do nothing
winston.verbose('[plugin/composer-quill (toHtml)] Input not in expected format, skipping.');
diff --git a/package.json b/package.json
index 8ed5769..4f38a15 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nodebb-plugin-composer-quill",
- "version": "4.1.0",
+ "version": "4.2.0",
"description": "Quill Composer for NodeBB",
"main": "library.js",
"repository": {
@@ -32,6 +32,7 @@
"node-quill-converter": "^0.3.2",
"quill": "^1.3.7",
"quill-delta-to-html": "^0.12.0",
+ "quill-v1-table": "^1.7.4",
"quill-magic-url": "^4.0.0",
"quill-markdown-shortcuts": "^0.0.10",
"screenfull": "^5.0.0"
diff --git a/plugin.json b/plugin.json
index 2ac8777..db872c1 100644
--- a/plugin.json
+++ b/plugin.json
@@ -25,6 +25,7 @@
"quill.js": "./node_modules/quill/dist/quill.js",
"quill-magic-url.js": "./node_modules/quill-magic-url/dist/index.js",
"quill-markdown-shortcuts.js": "./node_modules/quill-markdown-shortcuts/dist/markdownShortcuts.js",
+ "quill-table.js": "./node_modules/quill-v1-table/dist/quill1-table.js",
"quill-emoji.js": "./static/lib/emoji.js",
"composer.js": "../nodebb-plugin-composer-default/static/lib/composer.js",
"composer/categoryList.js": "../nodebb-plugin-composer-default/static/lib/composer/categoryList.js",
@@ -46,4 +47,4 @@
"./node_modules/screenfull/dist/screenfull.js"
],
"templates": "static/templates"
-}
+}
\ No newline at end of file
diff --git a/screenshots/table in editor.png b/screenshots/table in editor.png
new file mode 100644
index 0000000..a917bfd
Binary files /dev/null and b/screenshots/table in editor.png differ
diff --git a/screenshots/table in post.png b/screenshots/table in post.png
new file mode 100644
index 0000000..5ded1d0
Binary files /dev/null and b/screenshots/table in post.png differ
diff --git a/static/lib/quill-nbb.js b/static/lib/quill-nbb.js
index 157eccb..c20c02e 100644
--- a/static/lib/quill-nbb.js
+++ b/static/lib/quill-nbb.js
@@ -241,20 +241,24 @@ $(window).on('action:chat.loaded', (evt, containerEl) => {
window.quill.init = function (targetEl, data, callback) {
require([
- 'quill', 'quill-magic-url', 'quill-emoji', 'quill-markdown-shortcuts',
+ 'quill', 'quill-magic-url', 'quill-emoji', 'quill-markdown-shortcuts', 'quill-table',
'composer/autocomplete', 'composer/drafts',
- ], (Quill, MagicUrl, Emoji, MarkdownShortcuts, autocomplete, drafts) => {
+ ], (Quill, MagicUrl, Emoji, MarkdownShortcuts, TableModule, autocomplete, drafts) => {
const textDirection = $('html').attr('data-dir');
const textareaEl = targetEl.siblings('textarea');
- window.quill.configureToolbar(targetEl, data).then(({ toolbar }) => {
+ window.quill.configureToolbar(targetEl, data, TableModule).then(({ toolbar }) => {
// Quill...
Quill.register('modules/magicUrl', MagicUrl.default);
Quill.register('modules/markdownShortcuts', MarkdownShortcuts);
+ Quill.register('modules/table', TableModule);
const quill = new Quill(targetEl.get(0), {
theme: data.theme || 'snow',
modules: {
toolbar,
+ table: {
+ cellSelectionOnClick: false,
+ },
magicUrl: {
normalizeUrlOptions: {
sortQueryParameters: false,
@@ -383,7 +387,7 @@ window.quill.init = function (targetEl, data, callback) {
return window.quill;
};
-window.quill.configureToolbar = async (targetEl, data) => {
+window.quill.configureToolbar = async (targetEl, data, TableModule) => {
const textareaEl = targetEl.siblings('textarea');
const [formatting, hooks] = await new Promise((resolve) => {
require(['composer/formatting', 'hooks'], (...libs) => resolve(libs));
@@ -395,6 +399,26 @@ window.quill.configureToolbar = async (targetEl, data) => {
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['link', 'blockquote', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }],
+ [{ table: TableModule.tableOptions() }, {
+ table: [
+ 'insert',
+ 'remove-table',
+ 'split-cell',
+ 'merge-selection',
+ 'append-row-above',
+ 'append-row-below',
+ 'append-col-before',
+ 'append-col-after',
+ 'remove-col',
+ 'remove-row',
+ 'remove-cell',
+ 'remove-selection',
+ 'hide-border',
+ 'show-border',
+ 'undo',
+ 'redo',
+ ],
+ }],
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ align: [] }],
diff --git a/static/scss/post.scss b/static/scss/post.scss
index 0583550..dd99f72 100644
--- a/static/scss/post.scss
+++ b/static/scss/post.scss
@@ -16,4 +16,100 @@
.ql-font-monospace {
font-family: Monaco, "Courier New", monospace;
}
+
+ /* 表格视觉变量:默认浅色;仅当祖先带 .dark 或 .dark 时切换深色描边(不与系统 prefers-color-scheme 联动) */
+ table {
+ /* 原 0.14 在白底上极易被看成「无边框」;0.18 仍为浅边,可读性更好;可用变量改回 0.14 */
+ --ql-table-border: rgba(23, 26, 29, 0.18);
+ --ql-table-bg: transparent;
+ --ql-table-cell-bg: transparent;
+ --ql-table-selected-bg: rgba(59, 130, 246, 0.14);
+ --ql-table-font-size: 16px;
+ --ql-table-line-height: 1.7;
+ --ql-table-padding: 8px;
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: fixed;
+ /* 勿与 border-radius 同用:在 Chromium/WebKit 下常导致 collapse 表格的格线被裁掉或完全不画 */
+ overflow: visible;
+ white-space: nowrap;
+ font-size: var(--ql-table-font-size);
+ line-height: var(--ql-table-line-height);
+ background-color: var(--ql-table-bg);
+ border-radius: 8px;
+ /* 外轮廓:collapse 下仅 td 边线在部分浏览器不可靠,表格外框保证浅色模式下可见 */
+ border: 1px solid var(--ql-table-border);
+ }
+
+ /* @media (prefers-color-scheme: dark) {
+ table {
+ --ql-table-border: rgba(255, 255, 255, 0.12);
+ --ql-table-selected-bg: rgba(96, 165, 250, 0.18);
+ }
+ } */
+
+ .dark table,
+ .dark table {
+ --ql-table-border: rgba(255, 255, 255, 0.12);
+ --ql-table-selected-bg: rgba(96, 165, 250, 0.18);
+ }
+
+ table td {
+ border: 1px solid var(--ql-table-border);
+ padding: var(--ql-table-padding);
+ min-height: calc(var(--ql-table-padding) * 2 + var(--ql-table-line-height) * 1em);
+ vertical-align: top;
+ white-space: pre-wrap; /* https://github.com/quilljs/quill/issues/1760 */
+ font-size: inherit;
+ line-height: inherit;
+ background-color: var(--ql-table-cell-bg);
+ }
+
+ .ql-editor__table--hideBorder td {
+ border: none !important;
+ }
+
+ .ql-editor__table--hideBorder {
+ border: none !important;
+ }
+
+ table td[rowspan="2"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 2 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td[rowspan="3"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 3 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td[rowspan="4"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 4 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td[rowspan="5"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 5 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td[rowspan="6"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 6 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td[rowspan="7"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 7 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td[rowspan="8"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 8 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td[rowspan="9"] {
+ min-height: calc(var(--ql-table-padding) * 2 + 9 * var(--ql-table-line-height) * 1em);
+ }
+
+ table td.ql-cell-selected {
+ background-color: var(--ql-table-selected-bg);
+ }
+
+ table td[merge_id] {
+ display: none;
+ }
}
\ No newline at end of file