@fastkit/vui 0.7.59 → 0.7.65
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 +616 -39
- package/dist/vui.cjs.prod.js +616 -39
- package/dist/vui.css +219 -0
- package/dist/vui.d.ts +141 -4
- package/dist/vui.min.css +1 -1
- package/dist/vui.min.css.map +1 -1
- package/dist/vui.mjs +606 -39
- 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/.DS_Store +0 -0
- package/src/components/VWysiwygEditor/VWysiwygEditor.scss +4 -0
- package/src/components/VWysiwygEditor/VWysiwygEditor.tsx +65 -22
- 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 +2 -0
- package/src/components/VWysiwygEditor/extensions/linter/.DS_Store +0 -0
- package/src/components/VWysiwygEditor/extensions/linter/Linter.scss +140 -0
- package/src/components/VWysiwygEditor/extensions/linter/Linter.tsx +214 -0
- package/src/components/VWysiwygEditor/extensions/linter/LinterPlugin.ts +65 -0
- package/src/components/VWysiwygEditor/extensions/linter/index.ts +5 -0
- package/src/components/VWysiwygEditor/extensions/linter/plugins/BadWords.tsx +50 -0
- package/src/components/VWysiwygEditor/extensions/linter/plugins/HeadingLevel.ts +39 -0
- package/src/components/VWysiwygEditor/extensions/linter/plugins/Punctuation.ts +49 -0
- package/src/components/VWysiwygEditor/extensions/linter/plugins/index.ts +3 -0
- package/src/components/VWysiwygEditor/extensions/linter/utils.ts +28 -0
- package/src/components/VWysiwygEditor/index.ts +1 -0
- package/src/components/VWysiwygEditor/schemes.ts +190 -4
- 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/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.65",
|
|
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.65",
|
|
36
|
+
"@fastkit/icon-font": "0.7.65",
|
|
37
|
+
"@fastkit/tiny-logger": "0.7.65",
|
|
38
|
+
"@fastkit/vite-kit": "0.7.65",
|
|
39
|
+
"@fastkit/vue-kit": "0.7.65",
|
|
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
|
});
|
|
Binary file
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import './VWysiwygEditor.scss';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
PropType,
|
|
5
|
+
computed,
|
|
6
|
+
ref,
|
|
7
|
+
watch,
|
|
8
|
+
onBeforeUnmount,
|
|
9
|
+
VNodeChild,
|
|
10
|
+
} from 'vue';
|
|
3
11
|
import {
|
|
4
12
|
createFormControlProps,
|
|
5
13
|
FormControlSlots,
|
|
@@ -34,9 +42,13 @@ import {
|
|
|
34
42
|
import StarterKit from '@tiptap/starter-kit';
|
|
35
43
|
import { VButton, VButtonGroup } from '../VButton';
|
|
36
44
|
import {
|
|
45
|
+
RawWysiwygExtension,
|
|
46
|
+
resolveRawWysiwygExtensions,
|
|
37
47
|
RawWysiwygEditorTool,
|
|
38
48
|
resolveRawWysiwygEditorTools,
|
|
39
49
|
WysiwygEditorContext,
|
|
50
|
+
// EditorEventsOptions,
|
|
51
|
+
WysiwygEditorInitializeContext,
|
|
40
52
|
} from './schemes';
|
|
41
53
|
|
|
42
54
|
export const VWysiwygEditor = defineComponent({
|
|
@@ -49,6 +61,10 @@ export const VWysiwygEditor = defineComponent({
|
|
|
49
61
|
...createControlFieldProviderProps(),
|
|
50
62
|
...createControlProps(),
|
|
51
63
|
...defineSlotsProps<FormControlSlots & InputBoxSlots>(),
|
|
64
|
+
extensions: {
|
|
65
|
+
type: Array as PropType<RawWysiwygExtension[]>,
|
|
66
|
+
default: () => [],
|
|
67
|
+
},
|
|
52
68
|
tools: {
|
|
53
69
|
type: Array as PropType<RawWysiwygEditorTool[]>,
|
|
54
70
|
default: () => [],
|
|
@@ -78,6 +94,23 @@ export const VWysiwygEditor = defineComponent({
|
|
|
78
94
|
validationValue: () => textRef.value,
|
|
79
95
|
});
|
|
80
96
|
|
|
97
|
+
const initializeCtx = new WysiwygEditorInitializeContext(() => vui, {
|
|
98
|
+
onCreate: ({ editor }) => {
|
|
99
|
+
textRef.value = editor.getText();
|
|
100
|
+
updateEditable();
|
|
101
|
+
},
|
|
102
|
+
onFocus: ({ event }) => {
|
|
103
|
+
inputControl.focusHandler(event);
|
|
104
|
+
},
|
|
105
|
+
onBlur: ({ event }) => {
|
|
106
|
+
inputControl.blurHandler(event);
|
|
107
|
+
},
|
|
108
|
+
onUpdate: ({ editor }) => {
|
|
109
|
+
inputControl.value = editor.getHTML();
|
|
110
|
+
textRef.value = editor.getText();
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
81
114
|
const extensions = [
|
|
82
115
|
StarterKit.configure({
|
|
83
116
|
bold: false,
|
|
@@ -86,6 +119,10 @@ export const VWysiwygEditor = defineComponent({
|
|
|
86
119
|
history: false,
|
|
87
120
|
italic: false,
|
|
88
121
|
}),
|
|
122
|
+
// Linter.configure({
|
|
123
|
+
// plugins: [BadWords(['abc', 'evidently']), Punctuation, HeadingLevel],
|
|
124
|
+
// }),
|
|
125
|
+
...resolveRawWysiwygExtensions(props.extensions, initializeCtx),
|
|
89
126
|
...wysiwygSettings.value.extensions,
|
|
90
127
|
];
|
|
91
128
|
|
|
@@ -110,21 +147,13 @@ export const VWysiwygEditor = defineComponent({
|
|
|
110
147
|
},
|
|
111
148
|
);
|
|
112
149
|
|
|
150
|
+
initializeCtx.on('create', ({ editor }) => {
|
|
151
|
+
textRef.value = editor.getText();
|
|
152
|
+
updateEditable();
|
|
153
|
+
});
|
|
154
|
+
|
|
113
155
|
const editor = useEditor({
|
|
114
|
-
|
|
115
|
-
textRef.value = editor.getText();
|
|
116
|
-
updateEditable();
|
|
117
|
-
},
|
|
118
|
-
onFocus: (ctx) => {
|
|
119
|
-
inputControl.focusHandler(ctx.event);
|
|
120
|
-
},
|
|
121
|
-
onBlur: (ctx) => {
|
|
122
|
-
inputControl.blurHandler(ctx.event);
|
|
123
|
-
},
|
|
124
|
-
onUpdate: (ev) => {
|
|
125
|
-
inputControl.value = ev.editor.getHTML();
|
|
126
|
-
textRef.value = ev.editor.getText();
|
|
127
|
-
},
|
|
156
|
+
...initializeCtx.editorOptions(),
|
|
128
157
|
autofocus: props.autofocus,
|
|
129
158
|
content: props.modelValue,
|
|
130
159
|
extensions,
|
|
@@ -177,9 +206,12 @@ export const VWysiwygEditor = defineComponent({
|
|
|
177
206
|
|
|
178
207
|
return (
|
|
179
208
|
<VButtonGroup
|
|
180
|
-
class={
|
|
181
|
-
'v-wysiwyg-
|
|
182
|
-
|
|
209
|
+
class={[
|
|
210
|
+
'v-wysiwyg-editor__toolbar',
|
|
211
|
+
{
|
|
212
|
+
'v-wysiwyg-editor__floating-toolbar': props.floatingToolbar,
|
|
213
|
+
},
|
|
214
|
+
]}
|
|
183
215
|
variant={variant}>
|
|
184
216
|
{tools.map(({ key, icon, onClick, disabled, active }) => {
|
|
185
217
|
const isActive =
|
|
@@ -188,16 +220,22 @@ export const VWysiwygEditor = defineComponent({
|
|
|
188
220
|
!inputControl.canOperation ||
|
|
189
221
|
(!!_editor && resolveContextableValue(context, disabled));
|
|
190
222
|
let iconName: IconName | undefined;
|
|
223
|
+
let child: VNodeChild;
|
|
191
224
|
|
|
192
225
|
if (typeof icon === 'function') {
|
|
193
226
|
if (_editor) {
|
|
194
|
-
|
|
227
|
+
const _child = icon(context);
|
|
228
|
+
if (typeof _child === 'function') {
|
|
229
|
+
child = _child();
|
|
230
|
+
} else {
|
|
231
|
+
iconName = child as any;
|
|
232
|
+
}
|
|
195
233
|
}
|
|
196
234
|
} else {
|
|
197
235
|
iconName = icon;
|
|
198
236
|
}
|
|
199
237
|
|
|
200
|
-
if (!iconName) return;
|
|
238
|
+
if (!iconName && child == null) return;
|
|
201
239
|
|
|
202
240
|
return (
|
|
203
241
|
<VButton
|
|
@@ -206,14 +244,19 @@ export const VWysiwygEditor = defineComponent({
|
|
|
206
244
|
icon={iconName}
|
|
207
245
|
onClick={(ev) => onClick(context, ev)}
|
|
208
246
|
color={isActive ? toolButtonActiveColor : undefined}
|
|
209
|
-
disabled={isDisabled}
|
|
210
|
-
|
|
247
|
+
disabled={isDisabled}>
|
|
248
|
+
{child}
|
|
249
|
+
</VButton>
|
|
211
250
|
);
|
|
212
251
|
})}
|
|
213
252
|
</VButtonGroup>
|
|
214
253
|
);
|
|
215
254
|
};
|
|
216
255
|
|
|
256
|
+
onBeforeUnmount(() => {
|
|
257
|
+
editor.value && editor.value.destroy();
|
|
258
|
+
});
|
|
259
|
+
|
|
217
260
|
return {
|
|
218
261
|
editor,
|
|
219
262
|
...inputControl.expose(),
|
|
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
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
.v-linter {
|
|
2
|
+
&__problem {
|
|
3
|
+
margin-bottom: -1px;
|
|
4
|
+
cursor: help;
|
|
5
|
+
background: var(--deep-color);
|
|
6
|
+
border-bottom: 1px solid var(--main-color);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
&__issue-icon {
|
|
10
|
+
position: absolute;
|
|
11
|
+
right: 2px;
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
width: 1.2em;
|
|
16
|
+
height: 1.2em;
|
|
17
|
+
margin-top: 0.2em;
|
|
18
|
+
// padding-left: 0.5px;
|
|
19
|
+
font-family: times, georgia, serif;
|
|
20
|
+
font-size: 15px;
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
line-height: 1;
|
|
23
|
+
color: var(--text-color);
|
|
24
|
+
text-align: center;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
background: var(--main-color);
|
|
27
|
+
border-radius: 100px;
|
|
28
|
+
|
|
29
|
+
&::before {
|
|
30
|
+
content: '!';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&__issue-menu-wrapper {
|
|
35
|
+
position: relative;
|
|
36
|
+
display: inline-block;
|
|
37
|
+
width: 0px;
|
|
38
|
+
height: 1em;
|
|
39
|
+
overflow: visible;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&__issue-menu {
|
|
43
|
+
font-size: 87.5%;
|
|
44
|
+
// font-size: 12px;
|
|
45
|
+
line-height: 1.5;
|
|
46
|
+
|
|
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
|
+
}
|
|
72
|
+
}
|
|
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
|
+
|
|
128
|
+
&__fix {
|
|
129
|
+
display: inline;
|
|
130
|
+
margin-top: 8px;
|
|
131
|
+
color: var(--palette-link);
|
|
132
|
+
text-decoration: underline;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
background: transparent;
|
|
135
|
+
border: 0;
|
|
136
|
+
outline: 0;
|
|
137
|
+
appearance: none;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -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
|
+
});
|