@fastkit/vui 0.7.61 → 0.7.66
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/dist/tool/index.js +1 -0
- package/dist/tool/vite-plugin.d.ts.map +1 -1
- package/dist/vui.cjs.js +443 -228
- package/dist/vui.cjs.prod.js +443 -228
- package/dist/vui.css +166 -12
- package/dist/vui.d.ts +103 -54
- package/dist/vui.min.css +1 -1
- package/dist/vui.min.css.map +1 -1
- package/dist/vui.mjs +437 -225
- package/package.json +7 -6
- package/src/components/VButton/VButtonGroup.scss +4 -0
- package/src/components/VButton/VButtonGroup.tsx +12 -1
- package/src/components/VWysiwygEditor/VWysiwygEditor.scss +4 -0
- package/src/components/VWysiwygEditor/VWysiwygEditor.tsx +19 -9
- package/src/components/VWysiwygEditor/extensions/.DS_Store +0 -0
- package/src/components/VWysiwygEditor/extensions/color.ts +72 -0
- package/src/components/VWysiwygEditor/extensions/index.ts +1 -0
- package/src/components/VWysiwygEditor/extensions/linter/.DS_Store +0 -0
- package/src/components/VWysiwygEditor/extensions/linter/Linter.scss +80 -15
- package/src/components/VWysiwygEditor/extensions/linter/Linter.tsx +214 -0
- package/src/components/VWysiwygEditor/extensions/linter/LinterPlugin.ts +32 -11
- package/src/components/VWysiwygEditor/extensions/linter/index.ts +3 -3
- package/src/components/VWysiwygEditor/extensions/linter/plugins/BadWords.tsx +50 -0
- package/src/components/VWysiwygEditor/extensions/linter/plugins/HeadingLevel.ts +9 -3
- package/src/components/VWysiwygEditor/extensions/linter/plugins/Punctuation.ts +9 -3
- package/src/components/VWysiwygEditor/extensions/linter/plugins/index.ts +3 -3
- package/src/components/VWysiwygEditor/extensions/linter/utils.ts +28 -0
- package/src/components/VWysiwygEditor/schemes.ts +87 -5
- package/src/components/VWysiwygEditor/tools/color.scss +140 -0
- package/src/components/VWysiwygEditor/tools/color.tsx +90 -0
- package/src/components/VWysiwygEditor/tools/index.ts +1 -1
- package/src/service.tsx +5 -0
- package/src/tool/vite-plugin.ts +1 -0
- package/src/components/VWysiwygEditor/extensions/linter/Linter.ts +0 -262
- package/src/components/VWysiwygEditor/extensions/linter/plugins/BadWords.ts +0 -34
- package/src/components/VWysiwygEditor/tools/text-color.ts +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastkit/vui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.66",
|
|
4
4
|
"description": "@fastkit/vui",
|
|
5
5
|
"buildOptions": {
|
|
6
6
|
"name": "Vui",
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
"vue": "^3.2.26"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@fastkit/color-scheme": "0.7.
|
|
36
|
-
"@fastkit/icon-font": "0.7.
|
|
37
|
-
"@fastkit/tiny-logger": "0.7.
|
|
38
|
-
"@fastkit/vite-kit": "0.7.
|
|
39
|
-
"@fastkit/vue-kit": "0.7.
|
|
35
|
+
"@fastkit/color-scheme": "0.7.66",
|
|
36
|
+
"@fastkit/icon-font": "0.7.66",
|
|
37
|
+
"@fastkit/tiny-logger": "0.7.66",
|
|
38
|
+
"@fastkit/vite-kit": "0.7.66",
|
|
39
|
+
"@fastkit/vue-kit": "0.7.66",
|
|
40
40
|
"@tiptap/extension-link": "^2.0.0-beta.36",
|
|
41
41
|
"@tiptap/extension-underline": "^2.0.0-beta.23",
|
|
42
|
+
"@tiptap/extension-text-style": "^2.0.0-beta.23",
|
|
42
43
|
"@tiptap/starter-kit": "^2.0.0-beta.183",
|
|
43
44
|
"@tiptap/vue-3": "^2.0.0-beta.90",
|
|
44
45
|
"eta": "^1.12.3",
|
|
@@ -25,6 +25,7 @@ export const VButtonGroup = defineComponent({
|
|
|
25
25
|
},
|
|
26
26
|
setup(props, ctx) {
|
|
27
27
|
return () => {
|
|
28
|
+
let hasIcon = false;
|
|
28
29
|
let buttonLength = 0;
|
|
29
30
|
|
|
30
31
|
let tmp = renderSlotOrEmpty(ctx.slots) || [];
|
|
@@ -43,6 +44,10 @@ export const VButtonGroup = defineComponent({
|
|
|
43
44
|
|
|
44
45
|
buttonLength++;
|
|
45
46
|
|
|
47
|
+
if (!hasIcon && node.props && !!node.props.icon) {
|
|
48
|
+
hasIcon = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
return {
|
|
47
52
|
isButton: true,
|
|
48
53
|
node,
|
|
@@ -67,6 +72,7 @@ export const VButtonGroup = defineComponent({
|
|
|
67
72
|
class: [
|
|
68
73
|
'v-button-group__item',
|
|
69
74
|
{
|
|
75
|
+
'v-button--icon': hasIcon,
|
|
70
76
|
'v-button-group__item--has-left': hasLeft,
|
|
71
77
|
'v-button-group__item--has-right': hasRight,
|
|
72
78
|
},
|
|
@@ -75,7 +81,12 @@ export const VButtonGroup = defineComponent({
|
|
|
75
81
|
}
|
|
76
82
|
return child.node;
|
|
77
83
|
});
|
|
78
|
-
return
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
class={['v-button-group', { 'v-button-group--has-icon': hasIcon }]}>
|
|
87
|
+
{$children}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
79
90
|
};
|
|
80
91
|
},
|
|
81
92
|
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ref,
|
|
7
7
|
watch,
|
|
8
8
|
onBeforeUnmount,
|
|
9
|
+
VNodeChild,
|
|
9
10
|
} from 'vue';
|
|
10
11
|
import {
|
|
11
12
|
createFormControlProps,
|
|
@@ -49,7 +50,6 @@ import {
|
|
|
49
50
|
// EditorEventsOptions,
|
|
50
51
|
WysiwygEditorInitializeContext,
|
|
51
52
|
} from './schemes';
|
|
52
|
-
// import { Linter, BadWords, Punctuation, HeadingLevel } from './extensions';
|
|
53
53
|
|
|
54
54
|
export const VWysiwygEditor = defineComponent({
|
|
55
55
|
name: 'VWysiwygEditor',
|
|
@@ -94,7 +94,7 @@ export const VWysiwygEditor = defineComponent({
|
|
|
94
94
|
validationValue: () => textRef.value,
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
const initializeCtx = new WysiwygEditorInitializeContext({
|
|
97
|
+
const initializeCtx = new WysiwygEditorInitializeContext(() => vui, {
|
|
98
98
|
onCreate: ({ editor }) => {
|
|
99
99
|
textRef.value = editor.getText();
|
|
100
100
|
updateEditable();
|
|
@@ -206,9 +206,12 @@ export const VWysiwygEditor = defineComponent({
|
|
|
206
206
|
|
|
207
207
|
return (
|
|
208
208
|
<VButtonGroup
|
|
209
|
-
class={
|
|
210
|
-
'v-wysiwyg-
|
|
211
|
-
|
|
209
|
+
class={[
|
|
210
|
+
'v-wysiwyg-editor__toolbar',
|
|
211
|
+
{
|
|
212
|
+
'v-wysiwyg-editor__floating-toolbar': props.floatingToolbar,
|
|
213
|
+
},
|
|
214
|
+
]}
|
|
212
215
|
variant={variant}>
|
|
213
216
|
{tools.map(({ key, icon, onClick, disabled, active }) => {
|
|
214
217
|
const isActive =
|
|
@@ -217,16 +220,22 @@ export const VWysiwygEditor = defineComponent({
|
|
|
217
220
|
!inputControl.canOperation ||
|
|
218
221
|
(!!_editor && resolveContextableValue(context, disabled));
|
|
219
222
|
let iconName: IconName | undefined;
|
|
223
|
+
let child: VNodeChild;
|
|
220
224
|
|
|
221
225
|
if (typeof icon === 'function') {
|
|
222
226
|
if (_editor) {
|
|
223
|
-
|
|
227
|
+
const _child = icon(context);
|
|
228
|
+
if (typeof _child === 'function') {
|
|
229
|
+
child = _child();
|
|
230
|
+
} else {
|
|
231
|
+
iconName = child as any;
|
|
232
|
+
}
|
|
224
233
|
}
|
|
225
234
|
} else {
|
|
226
235
|
iconName = icon;
|
|
227
236
|
}
|
|
228
237
|
|
|
229
|
-
if (!iconName) return;
|
|
238
|
+
if (!iconName && child == null) return;
|
|
230
239
|
|
|
231
240
|
return (
|
|
232
241
|
<VButton
|
|
@@ -235,8 +244,9 @@ export const VWysiwygEditor = defineComponent({
|
|
|
235
244
|
icon={iconName}
|
|
236
245
|
onClick={(ev) => onClick(context, ev)}
|
|
237
246
|
color={isActive ? toolButtonActiveColor : undefined}
|
|
238
|
-
disabled={isDisabled}
|
|
239
|
-
|
|
247
|
+
disabled={isDisabled}>
|
|
248
|
+
{child}
|
|
249
|
+
</VButton>
|
|
240
250
|
);
|
|
241
251
|
})}
|
|
242
252
|
</VButtonGroup>
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import '@tiptap/extension-text-style';
|
|
3
|
+
|
|
4
|
+
export type WysiwygColorOptions = {
|
|
5
|
+
types: string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
declare module '@tiptap/core' {
|
|
9
|
+
interface Commands<ReturnType> {
|
|
10
|
+
color: {
|
|
11
|
+
/**
|
|
12
|
+
* Set the text color
|
|
13
|
+
*/
|
|
14
|
+
setColor: (color: string) => ReturnType;
|
|
15
|
+
/**
|
|
16
|
+
* Unset the text color
|
|
17
|
+
*/
|
|
18
|
+
unsetColor: () => ReturnType;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const WysiwygColorExtension = Extension.create<WysiwygColorOptions>({
|
|
24
|
+
name: 'color',
|
|
25
|
+
|
|
26
|
+
addOptions() {
|
|
27
|
+
return {
|
|
28
|
+
types: ['textStyle'],
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
addGlobalAttributes() {
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
types: this.options.types,
|
|
36
|
+
attributes: {
|
|
37
|
+
color: {
|
|
38
|
+
default: null,
|
|
39
|
+
parseHTML: (element) => element.style.color.replace(/['"]+/g, ''),
|
|
40
|
+
renderHTML: (attributes) => {
|
|
41
|
+
if (!attributes.color) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
style: `color: ${attributes.color}`,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
addCommands() {
|
|
56
|
+
return {
|
|
57
|
+
setColor:
|
|
58
|
+
(color) =>
|
|
59
|
+
({ chain }) => {
|
|
60
|
+
return chain().setMark('textStyle', { color }).run();
|
|
61
|
+
},
|
|
62
|
+
unsetColor:
|
|
63
|
+
() =>
|
|
64
|
+
({ chain }) => {
|
|
65
|
+
return chain()
|
|
66
|
+
.setMark('textStyle', { color: null })
|
|
67
|
+
.removeEmptyTextStyle()
|
|
68
|
+
.run();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
Binary file
|
|
@@ -40,26 +40,91 @@
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
&__issue-menu {
|
|
43
|
-
position: absolute;
|
|
44
|
-
top: calc(100% + 8px);
|
|
45
|
-
left: 0;
|
|
46
|
-
min-width: 200px;
|
|
47
|
-
max-width: calc(100vw - 40px);
|
|
48
|
-
padding: 8px;
|
|
49
|
-
// font-size: 12px;
|
|
50
43
|
font-size: 87.5%;
|
|
44
|
+
// font-size: 12px;
|
|
51
45
|
line-height: 1.5;
|
|
52
|
-
pointer-events: none;
|
|
53
|
-
background: #fff;
|
|
54
|
-
border-radius: 4px;
|
|
55
|
-
opacity: 0;
|
|
56
|
-
transition: opacity 0.15s;
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
.v-dialog__body {
|
|
48
|
+
padding: 0;
|
|
49
|
+
font-size: inherit;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&__message {
|
|
53
|
+
padding: 8px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&__fixers {
|
|
57
|
+
position: relative;
|
|
58
|
+
padding: 4px 0;
|
|
59
|
+
// padding: 8px;
|
|
60
|
+
// border-top: solid 1px var(--divider-color);
|
|
61
|
+
&::before {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: 0;
|
|
64
|
+
right: 0;
|
|
65
|
+
left: 0;
|
|
66
|
+
display: block;
|
|
67
|
+
height: 1px;
|
|
68
|
+
content: '';
|
|
69
|
+
background: currentColor;
|
|
70
|
+
opacity: 0.5;
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
&__fixer {
|
|
75
|
+
&__button {
|
|
76
|
+
position: relative;
|
|
77
|
+
display: block;
|
|
78
|
+
width: 100%;
|
|
79
|
+
padding: 8px;
|
|
80
|
+
font: inherit;
|
|
81
|
+
font-size: 12px;
|
|
82
|
+
// margin-top: 8px;
|
|
83
|
+
// color: var(--palette-link);
|
|
84
|
+
text-align: left;
|
|
85
|
+
// text-decoration: underline;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
background: transparent;
|
|
88
|
+
border: 0;
|
|
89
|
+
outline: 0;
|
|
90
|
+
appearance: none;
|
|
91
|
+
|
|
92
|
+
&::before {
|
|
93
|
+
position: absolute;
|
|
94
|
+
top: 0;
|
|
95
|
+
right: 0;
|
|
96
|
+
bottom: 0;
|
|
97
|
+
left: 0;
|
|
98
|
+
display: block;
|
|
99
|
+
content: '';
|
|
100
|
+
background: currentColor;
|
|
101
|
+
opacity: 0;
|
|
102
|
+
transition: 0.15s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&:hover,
|
|
106
|
+
&:focus {
|
|
107
|
+
&::before {
|
|
108
|
+
opacity: 0.1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&__message {
|
|
113
|
+
position: relative;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// pointer-events: none;
|
|
118
|
+
// background: #fff;
|
|
119
|
+
// border-radius: 4px;
|
|
120
|
+
// opacity: 0;
|
|
121
|
+
// transition: opacity 0.15s;
|
|
122
|
+
|
|
123
|
+
// &--active {
|
|
124
|
+
// pointer-events: auto;
|
|
125
|
+
// opacity: 1;
|
|
126
|
+
// }
|
|
127
|
+
|
|
63
128
|
&__fix {
|
|
64
129
|
display: inline;
|
|
65
130
|
margin-top: 8px;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import './Linter.scss';
|
|
2
|
+
|
|
3
|
+
import { Extension } from '@tiptap/core';
|
|
4
|
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
5
|
+
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
|
6
|
+
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
|
7
|
+
import {
|
|
8
|
+
WysiwygLinterPlugin,
|
|
9
|
+
WysiwygLinterResult as Issue,
|
|
10
|
+
WysiwygLinterFixMessage,
|
|
11
|
+
} from './LinterPlugin';
|
|
12
|
+
import { EditorView } from 'prosemirror-view';
|
|
13
|
+
import { createWysiwygExtension } from '../../schemes';
|
|
14
|
+
|
|
15
|
+
const PROBLEM_CLASS_NAME = 'v-linter__problem';
|
|
16
|
+
const ISSUE_ICON_CLASS_NAME = 'v-linter__issue-icon';
|
|
17
|
+
const ISSUE_DATASET_NAME = 'data-issue-id';
|
|
18
|
+
|
|
19
|
+
function resolveWysiwygLinterFixMessage(message: WysiwygLinterFixMessage) {
|
|
20
|
+
return typeof message === 'function' ? message() : message;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderIcon(view: EditorView<any>, issue: Issue) {
|
|
24
|
+
const { level = 'warning' } = issue;
|
|
25
|
+
const icon = document.createElement('div');
|
|
26
|
+
const message = resolveWysiwygLinterFixMessage(issue.message);
|
|
27
|
+
|
|
28
|
+
icon.className = `${ISSUE_ICON_CLASS_NAME} ${level}-scope`;
|
|
29
|
+
icon.setAttribute(ISSUE_DATASET_NAME, issue.id);
|
|
30
|
+
|
|
31
|
+
if (typeof message === 'string') {
|
|
32
|
+
icon.title = message;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return icon;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runAllLinterPlugins(
|
|
39
|
+
doc: ProsemirrorNode,
|
|
40
|
+
plugins: Array<typeof WysiwygLinterPlugin>,
|
|
41
|
+
) {
|
|
42
|
+
const decorations: [any?] = [];
|
|
43
|
+
|
|
44
|
+
const issues = plugins
|
|
45
|
+
.map((RegisteredLinterPlugin) => {
|
|
46
|
+
return new RegisteredLinterPlugin(doc).scan().getResults();
|
|
47
|
+
})
|
|
48
|
+
.flat();
|
|
49
|
+
|
|
50
|
+
issues.forEach((issue) => {
|
|
51
|
+
const { level = 'warning', icon } = issue;
|
|
52
|
+
const message = resolveWysiwygLinterFixMessage(issue.message);
|
|
53
|
+
decorations.push(
|
|
54
|
+
Decoration.inline(issue.from, issue.to, {
|
|
55
|
+
class: `${PROBLEM_CLASS_NAME} ${level}-scope`,
|
|
56
|
+
'data-issue-id': issue.id,
|
|
57
|
+
title: typeof message === 'string' ? message : undefined,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
if (icon) {
|
|
61
|
+
decorations.push(
|
|
62
|
+
Decoration.widget(issue.from, (view) => renderIcon(view, issue)),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const decorationSet = DecorationSet.create(doc, decorations);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
issues,
|
|
71
|
+
decorationSet,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WysiwygLinterOptions {
|
|
76
|
+
plugins: Array<typeof WysiwygLinterPlugin>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface WysiwygLinterStorage {
|
|
80
|
+
issues: Issue[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const WysiwygLinter = createWysiwygExtension((ctx) => {
|
|
84
|
+
function showMenu(view: EditorView<any>, issue: Issue, event: Event) {
|
|
85
|
+
const message = resolveWysiwygLinterFixMessage(issue.message);
|
|
86
|
+
const variant = ctx.vui.setting('containedVariant');
|
|
87
|
+
const scope = ctx.vui.setting(`${issue.level}Scope`);
|
|
88
|
+
|
|
89
|
+
return ctx.vui.menu({
|
|
90
|
+
activator: event,
|
|
91
|
+
content: (stack) => (
|
|
92
|
+
<div class="v-linter__issue-menu__body">
|
|
93
|
+
<div
|
|
94
|
+
class={[
|
|
95
|
+
'v-linter__issue-menu__message',
|
|
96
|
+
`${scope}-scope ${variant}`,
|
|
97
|
+
]}>
|
|
98
|
+
{message}
|
|
99
|
+
</div>
|
|
100
|
+
{issue.fix.length && (
|
|
101
|
+
<div class="v-linter__issue-menu__fixers">
|
|
102
|
+
{issue.fix.map((fixer, index) => (
|
|
103
|
+
<div key={index} class="v-linter__issue-menu__fixer">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="v-linter__issue-menu__fixer__button"
|
|
107
|
+
onClick={() => {
|
|
108
|
+
stack.close();
|
|
109
|
+
fixer.handler(view, issue);
|
|
110
|
+
}}>
|
|
111
|
+
<span class="v-linter__issue-menu__fixer__button__message">
|
|
112
|
+
{resolveWysiwygLinterFixMessage(fixer.message)}
|
|
113
|
+
</span>
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Extension.create<WysiwygLinterOptions, WysiwygLinterStorage>({
|
|
125
|
+
name: 'linter',
|
|
126
|
+
|
|
127
|
+
addOptions() {
|
|
128
|
+
return {
|
|
129
|
+
plugins: [],
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
addStorage() {
|
|
133
|
+
return {
|
|
134
|
+
issues: [],
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
addProseMirrorPlugins() {
|
|
138
|
+
const { plugins } = this.options;
|
|
139
|
+
const { storage } = this;
|
|
140
|
+
|
|
141
|
+
const runAll = (doc: ProsemirrorNode) => {
|
|
142
|
+
const { issues, decorationSet } = runAllLinterPlugins(doc, plugins);
|
|
143
|
+
storage.issues = issues;
|
|
144
|
+
return decorationSet;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const getIssue = (id: string) =>
|
|
148
|
+
storage.issues.find((issue) => issue.id === id);
|
|
149
|
+
|
|
150
|
+
const getIssueElementByEvent = (
|
|
151
|
+
source: EventTarget | null,
|
|
152
|
+
):
|
|
153
|
+
| {
|
|
154
|
+
issue: Issue;
|
|
155
|
+
}
|
|
156
|
+
| undefined => {
|
|
157
|
+
if (!source || !(source instanceof HTMLElement)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const issueId = source.getAttribute(ISSUE_DATASET_NAME);
|
|
161
|
+
const issue = issueId && getIssue(issueId);
|
|
162
|
+
if (!issue) return;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
issue,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
new Plugin({
|
|
171
|
+
key: new PluginKey('linter'),
|
|
172
|
+
state: {
|
|
173
|
+
init(_, { doc }) {
|
|
174
|
+
return runAll(doc);
|
|
175
|
+
},
|
|
176
|
+
apply(transaction, oldState) {
|
|
177
|
+
return transaction.docChanged
|
|
178
|
+
? runAll(transaction.doc)
|
|
179
|
+
: oldState;
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
props: {
|
|
183
|
+
decorations(state) {
|
|
184
|
+
return this.getState(state);
|
|
185
|
+
},
|
|
186
|
+
handleClick(view, _, event) {
|
|
187
|
+
const info = getIssueElementByEvent(event.target);
|
|
188
|
+
if (!info) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { issue } = info;
|
|
193
|
+
const { from, to } = issue;
|
|
194
|
+
|
|
195
|
+
const focus = () =>
|
|
196
|
+
view.dispatch(
|
|
197
|
+
view.state.tr
|
|
198
|
+
.setSelection(
|
|
199
|
+
TextSelection.create(view.state.doc, from, to),
|
|
200
|
+
)
|
|
201
|
+
.scrollIntoView(),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
focus();
|
|
205
|
+
|
|
206
|
+
showMenu(view, issue, event);
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
];
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -1,36 +1,57 @@
|
|
|
1
|
+
import { VNodeChild } from 'vue';
|
|
1
2
|
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
|
2
3
|
import { EditorView } from 'prosemirror-view';
|
|
4
|
+
import { cheepUUID } from './utils';
|
|
3
5
|
|
|
4
|
-
export type
|
|
6
|
+
export type WysiwygLinterFixFn = (
|
|
7
|
+
view: EditorView,
|
|
8
|
+
issue: WysiwygLinterResult,
|
|
9
|
+
) => any;
|
|
5
10
|
|
|
6
|
-
export type
|
|
11
|
+
export type WysiwygLinterFixMessage = string | (() => VNodeChild);
|
|
7
12
|
|
|
8
|
-
export interface
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
export interface WysiwygLinterFixer {
|
|
14
|
+
message: WysiwygLinterFixMessage;
|
|
15
|
+
handler: WysiwygLinterFixFn;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type WysiwygLinterResultLevel = 'warning' | 'error';
|
|
19
|
+
|
|
20
|
+
// fixers
|
|
21
|
+
export interface WysiwygLinterResult {
|
|
22
|
+
id: string;
|
|
23
|
+
icon: boolean;
|
|
24
|
+
level: WysiwygLinterResultLevel;
|
|
25
|
+
message: WysiwygLinterFixMessage;
|
|
11
26
|
from: number;
|
|
12
27
|
to: number;
|
|
13
|
-
fix
|
|
14
|
-
fixMessage?: string;
|
|
28
|
+
fix: WysiwygLinterFixer[];
|
|
15
29
|
}
|
|
16
30
|
|
|
17
|
-
export interface RawLinterResult
|
|
18
|
-
level
|
|
31
|
+
export interface RawLinterResult
|
|
32
|
+
extends Omit<WysiwygLinterResult, 'level' | 'fix' | 'id' | 'icon'> {
|
|
33
|
+
level?: WysiwygLinterResultLevel;
|
|
34
|
+
icon?: boolean;
|
|
35
|
+
fix?: WysiwygLinterFixer | WysiwygLinterFixer[];
|
|
19
36
|
}
|
|
20
37
|
|
|
21
|
-
export class
|
|
38
|
+
export class WysiwygLinterPlugin {
|
|
22
39
|
protected doc: ProsemirrorNode;
|
|
23
40
|
|
|
24
|
-
private results: Array<
|
|
41
|
+
private results: Array<WysiwygLinterResult> = [];
|
|
25
42
|
|
|
26
43
|
constructor(doc: ProsemirrorNode) {
|
|
27
44
|
this.doc = doc;
|
|
28
45
|
}
|
|
29
46
|
|
|
30
47
|
record(result: RawLinterResult) {
|
|
48
|
+
const { fix = [], icon = false } = result;
|
|
31
49
|
this.results.push({
|
|
32
50
|
level: 'error',
|
|
51
|
+
id: cheepUUID(),
|
|
33
52
|
...result,
|
|
53
|
+
fix: Array.isArray(fix) ? fix : [fix],
|
|
54
|
+
icon,
|
|
34
55
|
});
|
|
35
56
|
}
|
|
36
57
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export type {
|
|
1
|
+
export { WysiwygLinter } from './Linter';
|
|
2
|
+
export { WysiwygLinterPlugin } from './LinterPlugin';
|
|
3
|
+
export type { WysiwygLinterOptions } from './Linter';
|
|
4
4
|
|
|
5
5
|
export * from './plugins';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WysiwygLinterPlugin } from '../LinterPlugin';
|
|
2
|
+
|
|
3
|
+
export function WysiwygLinterBadWords(
|
|
4
|
+
words: string[],
|
|
5
|
+
): typeof WysiwygLinterPlugin {
|
|
6
|
+
const regex = new RegExp(`\\b(${words.join('|')})\\b`);
|
|
7
|
+
|
|
8
|
+
return class WysiwygLinterBadWords extends WysiwygLinterPlugin {
|
|
9
|
+
scan() {
|
|
10
|
+
this.doc.descendants((node: any, position: number) => {
|
|
11
|
+
if (!node.isText) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const matches = regex.exec(node.text);
|
|
16
|
+
|
|
17
|
+
if (matches) {
|
|
18
|
+
const fixValue = matches[0] + '!!!!!';
|
|
19
|
+
|
|
20
|
+
this.record({
|
|
21
|
+
level: 'warning',
|
|
22
|
+
message: `Try not to say '${matches[0]}'`,
|
|
23
|
+
from: position + matches.index,
|
|
24
|
+
to: position + matches.index + matches[0].length,
|
|
25
|
+
fix: [
|
|
26
|
+
{
|
|
27
|
+
message: () => (
|
|
28
|
+
<span>
|
|
29
|
+
<code>{fixValue}</code>に修正する。
|
|
30
|
+
</span>
|
|
31
|
+
),
|
|
32
|
+
handler: () => {
|
|
33
|
+
console.log('hoge');
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
message: 'どうにかする',
|
|
38
|
+
handler: () => {
|
|
39
|
+
console.log('hoge');
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|