@jx3box/jx3box-editor 3.0.2 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/Article.vue +64 -15
- package/src/Markdown.vue +297 -62
- package/src/assets/js/xss.js +35 -7
- package/src/views/article.vue +14 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jx3box/jx3box-editor",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "JX3BOX Article & Editor",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"js-base64": "^3.6.1",
|
|
33
33
|
"katex": "^0.16.4",
|
|
34
34
|
"lodash": "^4.17.21",
|
|
35
|
+
"lucide": "^0.577.0",
|
|
35
36
|
"photoswipe": "^4.1.2",
|
|
36
37
|
"sanitize-html": "^2.17.0",
|
|
37
38
|
"sortablejs": "^1.15.0",
|
package/src/Article.vue
CHANGED
|
@@ -8,12 +8,19 @@
|
|
|
8
8
|
v-for="(text, i) in data"
|
|
9
9
|
:key="i"
|
|
10
10
|
v-html="text"
|
|
11
|
-
:class="{ on: i == page - 1 || all == true }"
|
|
11
|
+
:class="{ on: i == page - 1 || all == true, 'markdown-body': isMarkdownMode }"
|
|
12
12
|
:id="'c-article-part' + ~~(i + 1)"
|
|
13
13
|
></div>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
<div
|
|
16
|
+
<div
|
|
17
|
+
id="c-article"
|
|
18
|
+
class="c-article"
|
|
19
|
+
ref="article"
|
|
20
|
+
v-else-if="data && data.length"
|
|
21
|
+
v-html="data[0]"
|
|
22
|
+
:class="{ 'markdown-body': isMarkdownMode }"
|
|
23
|
+
></div>
|
|
17
24
|
|
|
18
25
|
<el-button class="c-article-all" type="primary" v-if="!all && hasPages" @click="showAll">加载全部</el-button>
|
|
19
26
|
|
|
@@ -48,6 +55,8 @@
|
|
|
48
55
|
|
|
49
56
|
<script>
|
|
50
57
|
import $ from "jquery";
|
|
58
|
+
import Vditor from "vditor";
|
|
59
|
+
import "github-markdown-css/github-markdown-light.css";
|
|
51
60
|
|
|
52
61
|
// XSS
|
|
53
62
|
import execFilterXSS from "./assets/js/xss";
|
|
@@ -85,8 +94,7 @@ import renderJx3Element from "./assets/js/jx3_element";
|
|
|
85
94
|
export default {
|
|
86
95
|
name: "Article",
|
|
87
96
|
props: {
|
|
88
|
-
|
|
89
|
-
post_mode : {
|
|
97
|
+
post_mode: {
|
|
90
98
|
type: String,
|
|
91
99
|
default: "tinymce",
|
|
92
100
|
},
|
|
@@ -134,6 +142,7 @@ export default {
|
|
|
134
142
|
page: 1,
|
|
135
143
|
data: [],
|
|
136
144
|
mode: "",
|
|
145
|
+
renderVersion: 0,
|
|
137
146
|
|
|
138
147
|
// 画廊
|
|
139
148
|
gallery_index: null,
|
|
@@ -180,14 +189,17 @@ export default {
|
|
|
180
189
|
},
|
|
181
190
|
computed: {
|
|
182
191
|
total: function () {
|
|
183
|
-
return this.
|
|
192
|
+
return this.data.length;
|
|
184
193
|
},
|
|
185
194
|
hasPages: function () {
|
|
186
|
-
return this.
|
|
195
|
+
return this.data.length > 1;
|
|
187
196
|
},
|
|
188
197
|
origin: function () {
|
|
189
198
|
return this.content;
|
|
190
199
|
},
|
|
200
|
+
isMarkdownMode: function () {
|
|
201
|
+
return ["markdown", "md", "vditor"].includes(String(this.post_mode || "").toLowerCase());
|
|
202
|
+
},
|
|
191
203
|
chunks: function () {
|
|
192
204
|
return this.pageable ? execSplitPages(this.origin) : [this.origin];
|
|
193
205
|
},
|
|
@@ -195,9 +207,7 @@ export default {
|
|
|
195
207
|
methods: {
|
|
196
208
|
getHeaderHeight: function () {
|
|
197
209
|
// 页面上可能没有这些元素,取存在的第一个:.c-header 优先,其次 .c-breadcrumb
|
|
198
|
-
const el =
|
|
199
|
-
document.querySelector(".c-header") ||
|
|
200
|
-
document.querySelector(".c-breadcrumb");
|
|
210
|
+
const el = document.querySelector(".c-header") || document.querySelector(".c-breadcrumb");
|
|
201
211
|
if (!el) return 0;
|
|
202
212
|
const rect = el.getBoundingClientRect && el.getBoundingClientRect();
|
|
203
213
|
const h = (rect && rect.height) || el.offsetHeight || 0;
|
|
@@ -232,6 +242,31 @@ export default {
|
|
|
232
242
|
return "";
|
|
233
243
|
}
|
|
234
244
|
},
|
|
245
|
+
renderMarkdownChunk: async function (chunk) {
|
|
246
|
+
const temp = document.createElement("div");
|
|
247
|
+
|
|
248
|
+
await Vditor.preview(temp, chunk || "", {
|
|
249
|
+
mode: "light",
|
|
250
|
+
lang: "zh_CN",
|
|
251
|
+
hljs: {
|
|
252
|
+
enable: true,
|
|
253
|
+
lineNumber: false,
|
|
254
|
+
style: "github",
|
|
255
|
+
},
|
|
256
|
+
markdown: {
|
|
257
|
+
sanitize: true,
|
|
258
|
+
},
|
|
259
|
+
icon: "ant",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return temp.innerHTML;
|
|
263
|
+
},
|
|
264
|
+
renderMarkdown: async function () {
|
|
265
|
+
const html = await this.renderMarkdownChunk(this.origin);
|
|
266
|
+
const chunks = this.pageable ? execSplitPages(html) : [html];
|
|
267
|
+
|
|
268
|
+
return chunks.map((chunk) => this.doReg(chunk));
|
|
269
|
+
},
|
|
235
270
|
doDOM: function ($root) {
|
|
236
271
|
// 折叠块
|
|
237
272
|
renderFoldBlock($root);
|
|
@@ -305,16 +340,27 @@ export default {
|
|
|
305
340
|
this.doDir();
|
|
306
341
|
});
|
|
307
342
|
},
|
|
308
|
-
render: function () {
|
|
343
|
+
render: async function () {
|
|
344
|
+
const version = ++this.renderVersion;
|
|
309
345
|
let result = [];
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
result.
|
|
346
|
+
|
|
347
|
+
if (this.isMarkdownMode) {
|
|
348
|
+
result = await this.renderMarkdown();
|
|
349
|
+
} else {
|
|
350
|
+
for (let chunk of this.chunks) {
|
|
351
|
+
let _chunk = this.doReg(chunk);
|
|
352
|
+
result.push(_chunk);
|
|
353
|
+
}
|
|
313
354
|
}
|
|
355
|
+
|
|
356
|
+
if (version !== this.renderVersion) return;
|
|
314
357
|
this.data = result;
|
|
315
358
|
},
|
|
316
|
-
run: function () {
|
|
317
|
-
this.
|
|
359
|
+
run: async function () {
|
|
360
|
+
this.page = 1;
|
|
361
|
+
this.all = false;
|
|
362
|
+
await this.render();
|
|
363
|
+
if (!this.data.length) return;
|
|
318
364
|
|
|
319
365
|
// 等待html加载完毕后
|
|
320
366
|
this.$nextTick(() => {
|
|
@@ -334,6 +380,9 @@ export default {
|
|
|
334
380
|
content: function () {
|
|
335
381
|
this.run();
|
|
336
382
|
},
|
|
383
|
+
post_mode: function () {
|
|
384
|
+
this.run();
|
|
385
|
+
},
|
|
337
386
|
},
|
|
338
387
|
mounted: function () {
|
|
339
388
|
const params = new URLSearchParams(location.search);
|
package/src/Markdown.vue
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="c-editor-markdown">
|
|
2
|
+
<div class="c-editor-markdown" :class="{ 'c-editor-markdown--preview': previewVisible }">
|
|
3
3
|
<slot name="prepend"></slot>
|
|
4
4
|
|
|
5
|
-
<div class="c-editor-markdown__toolbar">
|
|
6
|
-
<div class="c-editor-markdown__toolbar-left">
|
|
7
|
-
<span class="c-editor-markdown__toolbar-label">编辑模式</span>
|
|
8
|
-
<el-radio-group v-model="editorMode" size="small">
|
|
9
|
-
<el-radio-button v-for="item in modeOptions" :key="item.value" :value="item.value">
|
|
10
|
-
{{ item.label }}
|
|
11
|
-
</el-radio-button>
|
|
12
|
-
</el-radio-group>
|
|
13
|
-
</div>
|
|
14
|
-
<div class="c-editor-markdown__toolbar-tip">GitHub 样式预览,支持 Ctrl+V 粘贴图片上传</div>
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
5
|
<slot></slot>
|
|
18
6
|
|
|
19
7
|
<div ref="editorHost" class="c-editor-markdown__host"></div>
|
|
@@ -25,17 +13,81 @@
|
|
|
25
13
|
<script>
|
|
26
14
|
import axios from "axios";
|
|
27
15
|
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
16
|
+
import {
|
|
17
|
+
Bold,
|
|
18
|
+
Code2,
|
|
19
|
+
Expand,
|
|
20
|
+
Eye,
|
|
21
|
+
EyeOff,
|
|
22
|
+
Heading,
|
|
23
|
+
Italic,
|
|
24
|
+
Link2,
|
|
25
|
+
List,
|
|
26
|
+
ListChecks,
|
|
27
|
+
ListOrdered,
|
|
28
|
+
Quote,
|
|
29
|
+
Redo2,
|
|
30
|
+
SquareCode,
|
|
31
|
+
Strikethrough,
|
|
32
|
+
TableProperties,
|
|
33
|
+
Undo2,
|
|
34
|
+
} from "lucide";
|
|
28
35
|
import Vditor from "vditor";
|
|
29
36
|
import "vditor/dist/index.css";
|
|
30
|
-
import "github-markdown-css/github-markdown.css";
|
|
37
|
+
import "github-markdown-css/github-markdown-light.css";
|
|
31
38
|
|
|
32
39
|
const { __cms } = JX3BOX;
|
|
33
40
|
const UPLOAD_API = `${__cms}api/cms/upload`;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
const TOOLBAR_ICON_SIZE = 16;
|
|
42
|
+
const STATIC_TOOLBAR_ICONS = {
|
|
43
|
+
headings: Heading,
|
|
44
|
+
bold: Bold,
|
|
45
|
+
italic: Italic,
|
|
46
|
+
strike: Strikethrough,
|
|
47
|
+
link: Link2,
|
|
48
|
+
list: List,
|
|
49
|
+
"ordered-list": ListOrdered,
|
|
50
|
+
check: ListChecks,
|
|
51
|
+
quote: Quote,
|
|
52
|
+
code: Code2,
|
|
53
|
+
"inline-code": SquareCode,
|
|
54
|
+
table: TableProperties,
|
|
55
|
+
undo: Undo2,
|
|
56
|
+
redo: Redo2,
|
|
57
|
+
fullscreen: Expand,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function renderLucideIcon(iconNode) {
|
|
61
|
+
if (!iconNode) return "";
|
|
62
|
+
|
|
63
|
+
const attrs = {
|
|
64
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
65
|
+
width: TOOLBAR_ICON_SIZE,
|
|
66
|
+
height: TOOLBAR_ICON_SIZE,
|
|
67
|
+
viewBox: "0 0 24 24",
|
|
68
|
+
fill: "none",
|
|
69
|
+
stroke: "currentColor",
|
|
70
|
+
"stroke-width": "2",
|
|
71
|
+
"stroke-linecap": "round",
|
|
72
|
+
"stroke-linejoin": "round",
|
|
73
|
+
class: "c-editor-markdown__icon",
|
|
74
|
+
"aria-hidden": "true",
|
|
75
|
+
};
|
|
76
|
+
const svgAttrs = Object.entries(attrs)
|
|
77
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
78
|
+
.join(" ");
|
|
79
|
+
const content = iconNode
|
|
80
|
+
.map(([tag, tagAttrs]) => {
|
|
81
|
+
const elementAttrs = Object.entries(tagAttrs)
|
|
82
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
83
|
+
.join(" ");
|
|
84
|
+
|
|
85
|
+
return `<${tag} ${elementAttrs}></${tag}>`;
|
|
86
|
+
})
|
|
87
|
+
.join("");
|
|
88
|
+
|
|
89
|
+
return `<svg ${svgAttrs}>${content}</svg>`;
|
|
90
|
+
}
|
|
39
91
|
|
|
40
92
|
export default {
|
|
41
93
|
name: "Markdown",
|
|
@@ -74,10 +126,14 @@ export default {
|
|
|
74
126
|
data: this.modelValue ?? this.content ?? "",
|
|
75
127
|
editor: null,
|
|
76
128
|
editorReady: false,
|
|
77
|
-
editorMode: "
|
|
78
|
-
|
|
129
|
+
editorMode: "sv",
|
|
130
|
+
previewVisible: false,
|
|
79
131
|
isRebuildingEditor: false,
|
|
80
132
|
isUploadingImage: false,
|
|
133
|
+
counterElement: null,
|
|
134
|
+
counterClickListener: null,
|
|
135
|
+
counterClickTimestamps: [],
|
|
136
|
+
pendingTip: "",
|
|
81
137
|
};
|
|
82
138
|
},
|
|
83
139
|
watch: {
|
|
@@ -105,13 +161,6 @@ export default {
|
|
|
105
161
|
this.applyEditableState();
|
|
106
162
|
},
|
|
107
163
|
},
|
|
108
|
-
editorMode(nextMode, prevMode) {
|
|
109
|
-
if (!this.editorReady || this.isRebuildingEditor || nextMode === prevMode) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
this.rebuildEditor(nextMode);
|
|
114
|
-
},
|
|
115
164
|
},
|
|
116
165
|
mounted() {
|
|
117
166
|
this.initEditor();
|
|
@@ -120,11 +169,28 @@ export default {
|
|
|
120
169
|
this.destroyEditor();
|
|
121
170
|
},
|
|
122
171
|
methods: {
|
|
123
|
-
getPreviewMode(
|
|
124
|
-
return
|
|
172
|
+
getPreviewMode() {
|
|
173
|
+
return this.previewVisible ? "both" : "editor";
|
|
174
|
+
},
|
|
175
|
+
getPreviewTip() {
|
|
176
|
+
return this.previewVisible ? "返回编辑" : "查看预览";
|
|
177
|
+
},
|
|
178
|
+
getToolbarIconMap() {
|
|
179
|
+
return {
|
|
180
|
+
...STATIC_TOOLBAR_ICONS,
|
|
181
|
+
"toggle-preview": this.previewVisible ? EyeOff : Eye,
|
|
182
|
+
};
|
|
125
183
|
},
|
|
126
184
|
getToolbar() {
|
|
127
185
|
return [
|
|
186
|
+
{
|
|
187
|
+
name: "toggle-preview",
|
|
188
|
+
icon: renderLucideIcon(Eye),
|
|
189
|
+
tip: this.getPreviewTip(),
|
|
190
|
+
tipPosition: "ne",
|
|
191
|
+
click: () => this.togglePreview(),
|
|
192
|
+
},
|
|
193
|
+
"|",
|
|
128
194
|
"headings",
|
|
129
195
|
"bold",
|
|
130
196
|
"italic",
|
|
@@ -171,7 +237,7 @@ export default {
|
|
|
171
237
|
markdown: {
|
|
172
238
|
sanitize: true,
|
|
173
239
|
},
|
|
174
|
-
mode: this.getPreviewMode(
|
|
240
|
+
mode: this.getPreviewMode(),
|
|
175
241
|
},
|
|
176
242
|
customWysiwygToolbar() {},
|
|
177
243
|
toolbar: this.getToolbar(),
|
|
@@ -190,6 +256,10 @@ export default {
|
|
|
190
256
|
this.editorReady = true;
|
|
191
257
|
this.syncEditorValue(this.data);
|
|
192
258
|
this.applyEditableState();
|
|
259
|
+
this.applyPreviewState();
|
|
260
|
+
this.syncToolbarState();
|
|
261
|
+
this.bindCounterShortcut();
|
|
262
|
+
this.showPendingTip();
|
|
193
263
|
},
|
|
194
264
|
input: (value) => {
|
|
195
265
|
this.handleEditorInput(value);
|
|
@@ -204,6 +274,7 @@ export default {
|
|
|
204
274
|
this.editor = new Vditor(host, this.buildEditorOptions(initialValue));
|
|
205
275
|
},
|
|
206
276
|
destroyEditor() {
|
|
277
|
+
this.unbindCounterShortcut();
|
|
207
278
|
if (!this.editor) return;
|
|
208
279
|
|
|
209
280
|
this.editor.destroy();
|
|
@@ -212,9 +283,11 @@ export default {
|
|
|
212
283
|
},
|
|
213
284
|
rebuildEditor() {
|
|
214
285
|
const currentValue = this.editor?.getValue?.() ?? this.data ?? "";
|
|
286
|
+
|
|
215
287
|
this.data = currentValue;
|
|
216
288
|
this.isRebuildingEditor = true;
|
|
217
289
|
this.destroyEditor();
|
|
290
|
+
|
|
218
291
|
this.$nextTick(() => {
|
|
219
292
|
this.initEditor(currentValue);
|
|
220
293
|
this.isRebuildingEditor = false;
|
|
@@ -226,6 +299,112 @@ export default {
|
|
|
226
299
|
const nextValue = value ?? "";
|
|
227
300
|
if (this.editor.getValue() === nextValue) return;
|
|
228
301
|
this.editor.setValue(nextValue);
|
|
302
|
+
if (this.previewVisible) {
|
|
303
|
+
this.editor.renderPreview();
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
getToolbarButton(name) {
|
|
307
|
+
return this.$refs.editorHost?.querySelector?.(`.vditor-toolbar button[data-type="${name}"]`) || null;
|
|
308
|
+
},
|
|
309
|
+
syncToolbarState() {
|
|
310
|
+
const iconMap = this.getToolbarIconMap();
|
|
311
|
+
|
|
312
|
+
Object.entries(iconMap).forEach(([name, iconNode]) => {
|
|
313
|
+
const button = this.getToolbarButton(name);
|
|
314
|
+
if (!button) return;
|
|
315
|
+
|
|
316
|
+
button.innerHTML = renderLucideIcon(iconNode);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const previewButton = this.getToolbarButton("toggle-preview");
|
|
320
|
+
if (previewButton) {
|
|
321
|
+
previewButton.setAttribute("aria-label", this.getPreviewTip());
|
|
322
|
+
previewButton.classList.toggle("vditor-menu--current", this.previewVisible);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.$refs.editorHost?.querySelectorAll?.(".vditor-toolbar button[data-type]").forEach((button) => {
|
|
326
|
+
const isVisibleInPreview = ["toggle-preview", "fullscreen"].includes(button.dataset.type || "");
|
|
327
|
+
|
|
328
|
+
button.parentElement.style.display = this.previewVisible && !isVisibleInPreview ? "none" : "";
|
|
329
|
+
});
|
|
330
|
+
this.$refs.editorHost?.querySelectorAll?.(".vditor-toolbar .vditor-toolbar__divider").forEach((divider) => {
|
|
331
|
+
divider.style.display = this.previewVisible ? "none" : "";
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
syncEditorPanels() {
|
|
335
|
+
const host = this.$refs.editorHost;
|
|
336
|
+
const preview = host?.querySelector?.(".vditor-preview");
|
|
337
|
+
const sv = host?.querySelector?.(".vditor-sv");
|
|
338
|
+
const irWrapper = host?.querySelector?.(".vditor-ir");
|
|
339
|
+
const wysiwygWrapper = host?.querySelector?.(".vditor-wysiwyg");
|
|
340
|
+
|
|
341
|
+
if (preview) {
|
|
342
|
+
preview.style.display = this.previewVisible ? "block" : "none";
|
|
343
|
+
}
|
|
344
|
+
if (sv) {
|
|
345
|
+
sv.style.display = this.editorMode === "sv" && !this.previewVisible ? "block" : "none";
|
|
346
|
+
}
|
|
347
|
+
if (irWrapper) {
|
|
348
|
+
irWrapper.style.display = "none";
|
|
349
|
+
}
|
|
350
|
+
if (wysiwygWrapper) {
|
|
351
|
+
wysiwygWrapper.style.display = this.editorMode === "wysiwyg" && !this.previewVisible ? "block" : "none";
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
applyPreviewState() {
|
|
355
|
+
if (!this.editorReady || !this.editor) return;
|
|
356
|
+
|
|
357
|
+
this.syncEditorPanels();
|
|
358
|
+
if (this.previewVisible) {
|
|
359
|
+
this.editor.renderPreview();
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
togglePreview() {
|
|
363
|
+
this.previewVisible = !this.previewVisible;
|
|
364
|
+
|
|
365
|
+
if (!this.editorReady || !this.editor) return;
|
|
366
|
+
|
|
367
|
+
this.applyPreviewState();
|
|
368
|
+
this.syncToolbarState();
|
|
369
|
+
},
|
|
370
|
+
toggleSecretEditorMode() {
|
|
371
|
+
if (this.isRebuildingEditor) return;
|
|
372
|
+
|
|
373
|
+
this.editorMode = this.editorMode === "wysiwyg" ? "sv" : "wysiwyg";
|
|
374
|
+
this.pendingTip = this.editorMode === "wysiwyg" ? "已切换到所见即所得模式" : "已切换到 Markdown 源码模式";
|
|
375
|
+
this.rebuildEditor();
|
|
376
|
+
},
|
|
377
|
+
bindCounterShortcut() {
|
|
378
|
+
const counter = this.$refs.editorHost?.querySelector?.(".vditor-counter");
|
|
379
|
+
if (!counter || counter === this.counterElement) return;
|
|
380
|
+
|
|
381
|
+
this.unbindCounterShortcut();
|
|
382
|
+
this.counterClickListener = this.counterClickListener || (() => this.handleCounterClick());
|
|
383
|
+
this.counterElement = counter;
|
|
384
|
+
this.counterElement.addEventListener("click", this.counterClickListener);
|
|
385
|
+
},
|
|
386
|
+
unbindCounterShortcut() {
|
|
387
|
+
if (!this.counterElement) return;
|
|
388
|
+
|
|
389
|
+
this.counterElement.removeEventListener("click", this.counterClickListener);
|
|
390
|
+
this.counterElement = null;
|
|
391
|
+
},
|
|
392
|
+
handleCounterClick() {
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
|
|
395
|
+
this.counterClickTimestamps = this.counterClickTimestamps.filter((time) => now - time <= 2000);
|
|
396
|
+
this.counterClickTimestamps.push(now);
|
|
397
|
+
|
|
398
|
+
if (this.counterClickTimestamps.length < 6) return;
|
|
399
|
+
|
|
400
|
+
this.counterClickTimestamps = [];
|
|
401
|
+
this.toggleSecretEditorMode();
|
|
402
|
+
},
|
|
403
|
+
showPendingTip() {
|
|
404
|
+
if (!this.pendingTip || !this.editor?.tip) return;
|
|
405
|
+
|
|
406
|
+
this.editor.tip(this.pendingTip, 1500);
|
|
407
|
+
this.pendingTip = "";
|
|
229
408
|
},
|
|
230
409
|
applyEditableState() {
|
|
231
410
|
if (!this.editorReady || !this.editor) return;
|
|
@@ -329,28 +508,6 @@ export default {
|
|
|
329
508
|
flex-direction: column;
|
|
330
509
|
gap: 12px;
|
|
331
510
|
|
|
332
|
-
&__toolbar {
|
|
333
|
-
display: flex;
|
|
334
|
-
align-items: center;
|
|
335
|
-
justify-content: space-between;
|
|
336
|
-
gap: 12px;
|
|
337
|
-
flex-wrap: wrap;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
&__toolbar-left {
|
|
341
|
-
display: flex;
|
|
342
|
-
align-items: center;
|
|
343
|
-
gap: 12px;
|
|
344
|
-
flex-wrap: wrap;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
&__toolbar-label,
|
|
348
|
-
&__toolbar-tip {
|
|
349
|
-
font-size: 13px;
|
|
350
|
-
color: #57606a;
|
|
351
|
-
line-height: 1.4;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
511
|
&__host {
|
|
355
512
|
overflow: visible;
|
|
356
513
|
border: 1px solid #dcdfe6;
|
|
@@ -358,8 +515,10 @@ export default {
|
|
|
358
515
|
background-color: #ffffff;
|
|
359
516
|
}
|
|
360
517
|
|
|
361
|
-
|
|
362
|
-
|
|
518
|
+
&__icon {
|
|
519
|
+
display: block;
|
|
520
|
+
width: 16px;
|
|
521
|
+
height: 16px;
|
|
363
522
|
}
|
|
364
523
|
|
|
365
524
|
.vditor {
|
|
@@ -369,10 +528,74 @@ export default {
|
|
|
369
528
|
}
|
|
370
529
|
|
|
371
530
|
.vditor-toolbar {
|
|
531
|
+
display: flex;
|
|
532
|
+
align-items: center;
|
|
372
533
|
padding: 10px 12px;
|
|
373
534
|
background: #f6f8fa;
|
|
374
535
|
border-bottom: 1px solid #d8dee4;
|
|
375
536
|
overflow: visible;
|
|
537
|
+
|
|
538
|
+
.vditor-toolbar__item {
|
|
539
|
+
margin-right: 2px;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.vditor-toolbar__divider {
|
|
543
|
+
margin: 0 4px;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.vditor-toolbar__item > .vditor-tooltipped {
|
|
547
|
+
display: inline-flex;
|
|
548
|
+
align-items: center;
|
|
549
|
+
justify-content: center;
|
|
550
|
+
width: 30px;
|
|
551
|
+
height: 30px;
|
|
552
|
+
padding: 0;
|
|
553
|
+
color: #4b5563;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.vditor-toolbar__item > .vditor-tooltipped svg {
|
|
557
|
+
fill: none !important;
|
|
558
|
+
stroke: currentColor !important;
|
|
559
|
+
stroke-width: 2 !important;
|
|
560
|
+
stroke-linecap: round;
|
|
561
|
+
stroke-linejoin: round;
|
|
562
|
+
width: 16px;
|
|
563
|
+
height: 16px;
|
|
564
|
+
overflow: visible;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.vditor-toolbar__item > .vditor-tooltipped svg * {
|
|
568
|
+
fill: none !important;
|
|
569
|
+
stroke: currentColor !important;
|
|
570
|
+
stroke-width: 2 !important;
|
|
571
|
+
stroke-linecap: round;
|
|
572
|
+
stroke-linejoin: round;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.vditor-panel button {
|
|
576
|
+
width: auto;
|
|
577
|
+
height: auto;
|
|
578
|
+
padding: 4px 8px;
|
|
579
|
+
color: inherit;
|
|
580
|
+
display: block;
|
|
581
|
+
line-height: 1.5;
|
|
582
|
+
text-align: left;
|
|
583
|
+
white-space: nowrap;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
&--preview {
|
|
588
|
+
.vditor-sv,
|
|
589
|
+
.vditor-ir,
|
|
590
|
+
.vditor-wysiwyg,
|
|
591
|
+
.vditor-counter {
|
|
592
|
+
display: none !important;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.vditor-preview {
|
|
596
|
+
display: block !important;
|
|
597
|
+
width: 100%;
|
|
598
|
+
}
|
|
376
599
|
}
|
|
377
600
|
|
|
378
601
|
.vditor-reset {
|
|
@@ -400,9 +623,20 @@ export default {
|
|
|
400
623
|
}
|
|
401
624
|
|
|
402
625
|
.vditor-counter {
|
|
403
|
-
padding: 8px
|
|
404
|
-
border-top:
|
|
405
|
-
background:
|
|
626
|
+
padding: 3px 8px;
|
|
627
|
+
border-top: 0;
|
|
628
|
+
background: rgba(27, 31, 35, 0.05);
|
|
629
|
+
border-radius: 3px;
|
|
630
|
+
line-height: 1.4;
|
|
631
|
+
margin: 0;
|
|
632
|
+
position: absolute;
|
|
633
|
+
right: 12px;
|
|
634
|
+
|
|
635
|
+
&::before,
|
|
636
|
+
&::after {
|
|
637
|
+
display: none !important;
|
|
638
|
+
content: none !important;
|
|
639
|
+
}
|
|
406
640
|
}
|
|
407
641
|
|
|
408
642
|
.vditor-preview__action {
|
|
@@ -414,11 +648,12 @@ export default {
|
|
|
414
648
|
z-index: 30;
|
|
415
649
|
}
|
|
416
650
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
651
|
+
.vditor-menu--current {
|
|
652
|
+
color: #0969da;
|
|
653
|
+
background: #eaf2ff;
|
|
654
|
+
}
|
|
421
655
|
|
|
656
|
+
@media (max-width: 768px) {
|
|
422
657
|
.vditor-reset.markdown-body,
|
|
423
658
|
.vditor-sv__editor,
|
|
424
659
|
.vditor-ir__marker,
|
package/src/assets/js/xss.js
CHANGED
|
@@ -7,12 +7,28 @@ const FORBID = new Set(["script", "object", "embed", "applet", "base", "meta", "
|
|
|
7
7
|
|
|
8
8
|
const EXTRA_TAGS = [
|
|
9
9
|
"img",
|
|
10
|
-
"h1",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
10
|
+
"h1",
|
|
11
|
+
"h2",
|
|
12
|
+
"h3",
|
|
13
|
+
"h4",
|
|
14
|
+
"h5",
|
|
15
|
+
"h6",
|
|
16
|
+
"table",
|
|
17
|
+
"thead",
|
|
18
|
+
"tbody",
|
|
19
|
+
"tr",
|
|
20
|
+
"th",
|
|
21
|
+
"td",
|
|
22
|
+
"blockquote",
|
|
23
|
+
"pre",
|
|
24
|
+
"code",
|
|
25
|
+
"hr",
|
|
26
|
+
"video",
|
|
27
|
+
"source",
|
|
28
|
+
"iframe",
|
|
29
|
+
"style",
|
|
30
|
+
"colgroup",
|
|
31
|
+
"col",
|
|
16
32
|
];
|
|
17
33
|
|
|
18
34
|
// 必须顶层的 at-rule(你说不需要动画,但 keyframes 也可能被编辑器/作者写进来,留着更稳)
|
|
@@ -116,6 +132,7 @@ export default function sanitizeRichText(html) {
|
|
|
116
132
|
.filter((t) => !FORBID.has(t));
|
|
117
133
|
|
|
118
134
|
return sanitizeHtml(html, {
|
|
135
|
+
allowVulnerableTags: true,
|
|
119
136
|
disallowedTagsMode: "discard",
|
|
120
137
|
allowedTags,
|
|
121
138
|
|
|
@@ -125,7 +142,18 @@ export default function sanitizeRichText(html) {
|
|
|
125
142
|
img: ["src", "alt", "title", "width", "height", "class", "style", "loading", "decoding"],
|
|
126
143
|
video: ["controls", "width", "height", "class", "style"],
|
|
127
144
|
source: ["src", "type"],
|
|
128
|
-
iframe: [
|
|
145
|
+
iframe: [
|
|
146
|
+
"src",
|
|
147
|
+
"width",
|
|
148
|
+
"height",
|
|
149
|
+
"frameborder",
|
|
150
|
+
"scrolling",
|
|
151
|
+
"allowfullscreen",
|
|
152
|
+
"sandbox",
|
|
153
|
+
"referrerpolicy",
|
|
154
|
+
"class",
|
|
155
|
+
"style",
|
|
156
|
+
],
|
|
129
157
|
td: ["colspan", "rowspan", "align", "valign", "class", "style"],
|
|
130
158
|
th: ["colspan", "rowspan", "align", "valign", "class", "style"],
|
|
131
159
|
col: ["span", "width", "class", "style"],
|
package/src/views/article.vue
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<el-segmented v-model="group" :options="groupOptions" />
|
|
5
5
|
</div>
|
|
6
6
|
<div class="container">
|
|
7
|
-
<Article class="article" :content="content"></Article>
|
|
7
|
+
<Article class="article" :content="content" :post_mode="postMode"></Article>
|
|
8
8
|
<div id="directory"></div>
|
|
9
9
|
</div>
|
|
10
10
|
</template>
|
|
@@ -29,6 +29,11 @@ export default {
|
|
|
29
29
|
],
|
|
30
30
|
};
|
|
31
31
|
},
|
|
32
|
+
computed: {
|
|
33
|
+
postMode() {
|
|
34
|
+
return this.group === "markdown" ? "markdown" : "tinymce";
|
|
35
|
+
},
|
|
36
|
+
},
|
|
32
37
|
watch: {
|
|
33
38
|
group: {
|
|
34
39
|
immediate: true,
|
|
@@ -53,20 +58,19 @@ export default {
|
|
|
53
58
|
.flex(x);
|
|
54
59
|
margin-bottom: 1rem;
|
|
55
60
|
}
|
|
56
|
-
.container{
|
|
57
|
-
#directory{
|
|
61
|
+
.container {
|
|
62
|
+
#directory {
|
|
58
63
|
.w(200px);
|
|
59
64
|
position: fixed;
|
|
60
|
-
right:20px;
|
|
61
|
-
top:20px;
|
|
65
|
+
right: 20px;
|
|
66
|
+
top: 20px;
|
|
62
67
|
}
|
|
63
|
-
.article{
|
|
68
|
+
.article {
|
|
64
69
|
margin-right: 320px;
|
|
65
70
|
}
|
|
66
|
-
|
|
67
71
|
}
|
|
68
|
-
body{
|
|
69
|
-
min-height:2000px;
|
|
70
|
-
padding:0 20px;
|
|
72
|
+
body {
|
|
73
|
+
min-height: 2000px;
|
|
74
|
+
padding: 0 20px;
|
|
71
75
|
}
|
|
72
76
|
</style>
|