@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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { VNodeChild } from 'vue';
|
|
2
|
+
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
|
3
|
+
import { EditorView } from 'prosemirror-view';
|
|
4
|
+
import { cheepUUID } from './utils';
|
|
5
|
+
|
|
6
|
+
export type WysiwygLinterFixFn = (
|
|
7
|
+
view: EditorView,
|
|
8
|
+
issue: WysiwygLinterResult,
|
|
9
|
+
) => any;
|
|
10
|
+
|
|
11
|
+
export type WysiwygLinterFixMessage = string | (() => VNodeChild);
|
|
12
|
+
|
|
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;
|
|
26
|
+
from: number;
|
|
27
|
+
to: number;
|
|
28
|
+
fix: WysiwygLinterFixer[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RawLinterResult
|
|
32
|
+
extends Omit<WysiwygLinterResult, 'level' | 'fix' | 'id' | 'icon'> {
|
|
33
|
+
level?: WysiwygLinterResultLevel;
|
|
34
|
+
icon?: boolean;
|
|
35
|
+
fix?: WysiwygLinterFixer | WysiwygLinterFixer[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class WysiwygLinterPlugin {
|
|
39
|
+
protected doc: ProsemirrorNode;
|
|
40
|
+
|
|
41
|
+
private results: Array<WysiwygLinterResult> = [];
|
|
42
|
+
|
|
43
|
+
constructor(doc: ProsemirrorNode) {
|
|
44
|
+
this.doc = doc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
record(result: RawLinterResult) {
|
|
48
|
+
const { fix = [], icon = false } = result;
|
|
49
|
+
this.results.push({
|
|
50
|
+
level: 'error',
|
|
51
|
+
id: cheepUUID(),
|
|
52
|
+
...result,
|
|
53
|
+
fix: Array.isArray(fix) ? fix : [fix],
|
|
54
|
+
icon,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
scan() {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getResults() {
|
|
63
|
+
return this.results;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EditorView } from 'prosemirror-view';
|
|
2
|
+
import {
|
|
3
|
+
WysiwygLinterPlugin,
|
|
4
|
+
WysiwygLinterResult as Issue,
|
|
5
|
+
} from '../LinterPlugin';
|
|
6
|
+
|
|
7
|
+
export class WysiwygLinterHeadingLevel extends WysiwygLinterPlugin {
|
|
8
|
+
fixHeader(level: number) {
|
|
9
|
+
return function ({ state, dispatch }: EditorView, issue: Issue) {
|
|
10
|
+
dispatch(state.tr.setNodeMarkup(issue.from - 1, undefined, { level }));
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
scan() {
|
|
15
|
+
let lastHeadLevel: number | null = null;
|
|
16
|
+
|
|
17
|
+
this.doc.descendants((node, position) => {
|
|
18
|
+
if (node.type.name === 'heading') {
|
|
19
|
+
// Check whether heading levels fit under the current level
|
|
20
|
+
const { level } = node.attrs;
|
|
21
|
+
|
|
22
|
+
if (lastHeadLevel != null && level > lastHeadLevel + 1) {
|
|
23
|
+
this.record({
|
|
24
|
+
message: `Heading too small (${level} under ${lastHeadLevel})`,
|
|
25
|
+
from: position + 1,
|
|
26
|
+
to: position + 1 + node.content.size,
|
|
27
|
+
fix: {
|
|
28
|
+
message: '修正する',
|
|
29
|
+
handler: this.fixHeader(lastHeadLevel + 1),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
lastHeadLevel = level;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EditorView } from 'prosemirror-view';
|
|
2
|
+
import {
|
|
3
|
+
WysiwygLinterPlugin,
|
|
4
|
+
WysiwygLinterResult as Issue,
|
|
5
|
+
} from '../LinterPlugin';
|
|
6
|
+
|
|
7
|
+
export class WysiwygLinterPunctuation extends WysiwygLinterPlugin {
|
|
8
|
+
public regex = / ([,.!?:]) ?/g;
|
|
9
|
+
|
|
10
|
+
fix(replacement: any) {
|
|
11
|
+
return function ({ state, dispatch }: EditorView, issue: Issue) {
|
|
12
|
+
dispatch(
|
|
13
|
+
state.tr.replaceWith(
|
|
14
|
+
issue.from,
|
|
15
|
+
issue.to,
|
|
16
|
+
state.schema.text(replacement),
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
scan() {
|
|
23
|
+
this.doc.descendants((node, position) => {
|
|
24
|
+
if (!node.isText) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!node.text) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const matches = this.regex.exec(node.text);
|
|
33
|
+
|
|
34
|
+
if (matches) {
|
|
35
|
+
this.record({
|
|
36
|
+
message: 'Suspicious spacing around punctuation',
|
|
37
|
+
from: position + matches.index,
|
|
38
|
+
to: position + matches.index + matches[0].length,
|
|
39
|
+
fix: {
|
|
40
|
+
message: 'Fix it!!!',
|
|
41
|
+
handler: this.fix(`${matches[1]} `),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
let IDX = 256,
|
|
2
|
+
BUFFER: any;
|
|
3
|
+
const HEX: any = [];
|
|
4
|
+
while (IDX--) HEX[IDX] = (IDX + 256).toString(16).substring(1);
|
|
5
|
+
|
|
6
|
+
export function cheepUUID() {
|
|
7
|
+
let i = 0,
|
|
8
|
+
num,
|
|
9
|
+
out = '';
|
|
10
|
+
|
|
11
|
+
if (!BUFFER || IDX + 16 > 256) {
|
|
12
|
+
BUFFER = Array((i = 256));
|
|
13
|
+
while (i--) BUFFER[i] = (256 * Math.random()) | 0;
|
|
14
|
+
i = IDX = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (; i < 16; i++) {
|
|
18
|
+
num = BUFFER[IDX + i];
|
|
19
|
+
if (i == 6) out += HEX[(num & 15) | 64];
|
|
20
|
+
else if (i == 8) out += HEX[(num & 63) | 128];
|
|
21
|
+
else out += HEX[num];
|
|
22
|
+
|
|
23
|
+
if (i & 1 && i > 1 && i < 11) out += '-';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
IDX++;
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
@@ -1,15 +1,201 @@
|
|
|
1
1
|
import { type VuiService } from '../../service';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Editor,
|
|
4
|
+
type Extensions,
|
|
5
|
+
type Node,
|
|
6
|
+
type Extension,
|
|
7
|
+
type Mark,
|
|
8
|
+
type AnyExtension,
|
|
9
|
+
type EditorOptions,
|
|
10
|
+
} from '@tiptap/vue-3';
|
|
3
11
|
import { type IconName } from '../VIcon';
|
|
12
|
+
import { VNodeChild } from 'vue';
|
|
13
|
+
|
|
14
|
+
const EDITOR_EVENTS = [
|
|
15
|
+
'beforeCreate',
|
|
16
|
+
'create',
|
|
17
|
+
'update',
|
|
18
|
+
'selectionUpdate',
|
|
19
|
+
'transaction',
|
|
20
|
+
'focus',
|
|
21
|
+
'blur',
|
|
22
|
+
'destroy',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
type PrefixedEventName<S extends string> = `on${Capitalize<S>}`;
|
|
26
|
+
|
|
27
|
+
const prefixedEventName = <S extends string>(
|
|
28
|
+
source: S,
|
|
29
|
+
): PrefixedEventName<S> => {
|
|
30
|
+
return `on${source.charAt(0).toUpperCase()}${source.slice(1)}` as any;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type WysiwygEditorEvent = typeof EDITOR_EVENTS[number];
|
|
34
|
+
|
|
35
|
+
export type WysiwygEditorPrefixedEvent = PrefixedEventName<WysiwygEditorEvent>;
|
|
36
|
+
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
38
|
+
export interface WysiwygEditorEventsOptions
|
|
39
|
+
extends Partial<Pick<EditorOptions, WysiwygEditorPrefixedEvent>> {}
|
|
40
|
+
|
|
41
|
+
export type WysiwygEditorEventsBucket = {
|
|
42
|
+
[EV in WysiwygEditorEvent]: NonNullable<
|
|
43
|
+
WysiwygEditorEventsOptions[PrefixedEventName<EV>]
|
|
44
|
+
>[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export class WysiwygEditorInitializeContext {
|
|
48
|
+
readonly listeners: WysiwygEditorEventsBucket = {} as any;
|
|
49
|
+
private readonly _vui: () => VuiService;
|
|
50
|
+
|
|
51
|
+
get vui() {
|
|
52
|
+
return this._vui();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
vuiGetter: () => VuiService,
|
|
57
|
+
opts: WysiwygEditorEventsOptions = {},
|
|
58
|
+
) {
|
|
59
|
+
this._vui = vuiGetter;
|
|
60
|
+
|
|
61
|
+
EDITOR_EVENTS.forEach((event) => {
|
|
62
|
+
this.listeners[event] = [];
|
|
63
|
+
const prefixed = prefixedEventName(event);
|
|
64
|
+
const fn = opts[prefixed];
|
|
65
|
+
fn && this.listeners[event].push(fn as any);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
on<EV extends WysiwygEditorEvent>(
|
|
70
|
+
ev: EV,
|
|
71
|
+
handler: NonNullable<WysiwygEditorEventsOptions[PrefixedEventName<EV>]>,
|
|
72
|
+
) {
|
|
73
|
+
this.listeners[ev].push(handler);
|
|
74
|
+
return () => this.off(ev, handler);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
off<EV extends WysiwygEditorEvent>(
|
|
78
|
+
ev: EV,
|
|
79
|
+
handler: NonNullable<WysiwygEditorEventsOptions[PrefixedEventName<EV>]>,
|
|
80
|
+
) {
|
|
81
|
+
this.listeners[ev] = this.listeners[ev].filter(
|
|
82
|
+
(_handler) => _handler !== handler,
|
|
83
|
+
) as any;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
editorOptions() {
|
|
87
|
+
const opts: WysiwygEditorEventsOptions = {};
|
|
88
|
+
|
|
89
|
+
EDITOR_EVENTS.forEach((event) => {
|
|
90
|
+
const prefixed = prefixedEventName(event);
|
|
91
|
+
opts[prefixed] = (props) => {
|
|
92
|
+
const handlers = this.listeners[event];
|
|
93
|
+
handlers.forEach((handler) => {
|
|
94
|
+
handler(props as any);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return opts;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
4
102
|
|
|
5
103
|
export interface WysiwygEditorContext {
|
|
6
104
|
editor: Editor;
|
|
7
105
|
vui: VuiService;
|
|
8
106
|
}
|
|
9
107
|
|
|
108
|
+
export type WysiwygExtensionFactory<Options = any, Storage = any> = (
|
|
109
|
+
ctx: WysiwygEditorInitializeContext,
|
|
110
|
+
) =>
|
|
111
|
+
| Extension<Options, Storage>
|
|
112
|
+
| Node<Options, Storage>
|
|
113
|
+
| Mark<Options, Storage>;
|
|
114
|
+
|
|
115
|
+
export interface CreatedWysiwygExtension<Options = any, Storage = any> {
|
|
116
|
+
__isCreatedWysiwygExtension: true;
|
|
117
|
+
_configs: Partial<Options>[];
|
|
118
|
+
configure(
|
|
119
|
+
options?: Partial<Options>,
|
|
120
|
+
): CreatedWysiwygExtension<Options, Storage>;
|
|
121
|
+
raw: WysiwygExtensionSource<Options, Storage>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type WysiwygExtensionSource<Options = any, Storage = any> =
|
|
125
|
+
| Extension<Options, Storage>
|
|
126
|
+
| Node<Options, Storage>
|
|
127
|
+
| Mark<Options, Storage>
|
|
128
|
+
| WysiwygExtensionFactory<Options, Storage>;
|
|
129
|
+
|
|
130
|
+
export type RawWysiwygExtension<Options = any, Storage = any> =
|
|
131
|
+
| WysiwygExtensionSource<Options, Storage>
|
|
132
|
+
| CreatedWysiwygExtension<Options, Storage>;
|
|
133
|
+
|
|
134
|
+
// export function createWysiwygExtension<Options = any, Storage = any>(
|
|
135
|
+
// extension: Extension<Options, Storage>,
|
|
136
|
+
// ): Extension<Options, Storage>;
|
|
137
|
+
// export function createWysiwygExtension<Options = any, Storage = any>(
|
|
138
|
+
// node: Node<Options, Storage>,
|
|
139
|
+
// ): Node<Options, Storage>;
|
|
140
|
+
// export function createWysiwygExtension<Options = any, Storage = any>(
|
|
141
|
+
// mark: Mark<Options, Storage>,
|
|
142
|
+
// ): Mark<Options, Storage>;
|
|
143
|
+
// export function createWysiwygExtension<Options = any, Storage = any>(
|
|
144
|
+
// factory: WysiwygExtensionFactory<Options, Storage>,
|
|
145
|
+
// ): WysiwygExtensionFactory<Options, Storage>;
|
|
146
|
+
|
|
147
|
+
function isCreatedWysiwygExtension<Options = any, Storage = any>(
|
|
148
|
+
source: unknown,
|
|
149
|
+
): source is CreatedWysiwygExtension<Options, Storage> {
|
|
150
|
+
return (
|
|
151
|
+
!!source &&
|
|
152
|
+
typeof source === 'object' &&
|
|
153
|
+
(source as CreatedWysiwygExtension).__isCreatedWysiwygExtension === true
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createWysiwygExtension<Options = any, Storage = any>(
|
|
158
|
+
extension: WysiwygExtensionSource<Options, Storage>,
|
|
159
|
+
) {
|
|
160
|
+
const ext: CreatedWysiwygExtension<Options, Storage> = {
|
|
161
|
+
__isCreatedWysiwygExtension: true,
|
|
162
|
+
_configs: [],
|
|
163
|
+
configure: (opts) => {
|
|
164
|
+
opts && ext._configs.push(opts);
|
|
165
|
+
return ext;
|
|
166
|
+
},
|
|
167
|
+
raw: extension,
|
|
168
|
+
};
|
|
169
|
+
return ext;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveRawWysiwygExtension(
|
|
173
|
+
raw: RawWysiwygExtension,
|
|
174
|
+
ctx: WysiwygEditorInitializeContext,
|
|
175
|
+
): AnyExtension {
|
|
176
|
+
if (isCreatedWysiwygExtension(raw)) {
|
|
177
|
+
const { raw: _raw, _configs } = raw;
|
|
178
|
+
let ext = typeof _raw === 'function' ? _raw(ctx) : _raw;
|
|
179
|
+
_configs.forEach((config) => {
|
|
180
|
+
ext = ext.configure(config);
|
|
181
|
+
});
|
|
182
|
+
return ext;
|
|
183
|
+
}
|
|
184
|
+
return typeof raw === 'function' ? raw(ctx) : raw;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function resolveRawWysiwygExtensions(
|
|
188
|
+
raws: RawWysiwygExtension[],
|
|
189
|
+
ctx: WysiwygEditorInitializeContext,
|
|
190
|
+
) {
|
|
191
|
+
return raws.map((raw) => resolveRawWysiwygExtension(raw, ctx));
|
|
192
|
+
}
|
|
193
|
+
|
|
10
194
|
export interface WysiwygEditorTool {
|
|
11
195
|
key: string;
|
|
12
|
-
icon:
|
|
196
|
+
icon:
|
|
197
|
+
| IconName
|
|
198
|
+
| ((ctx: WysiwygEditorContext) => IconName | (() => VNodeChild));
|
|
13
199
|
active?: boolean | ((ctx: WysiwygEditorContext) => boolean);
|
|
14
200
|
disabled?: boolean | ((ctx: WysiwygEditorContext) => boolean);
|
|
15
201
|
onClick: (ctx: WysiwygEditorContext, ev: MouseEvent) => any;
|
|
@@ -39,13 +225,13 @@ export interface ResolvedWysiwygEditorSettings {
|
|
|
39
225
|
}
|
|
40
226
|
|
|
41
227
|
export function resolveRawWysiwygEditorTools(
|
|
42
|
-
|
|
228
|
+
rawTools: RawWysiwygEditorTool[],
|
|
43
229
|
vui: VuiService,
|
|
44
230
|
): ResolvedWysiwygEditorSettings {
|
|
45
231
|
const tools: WysiwygEditorTool[] = [];
|
|
46
232
|
const extensions: Extensions = [];
|
|
47
233
|
|
|
48
|
-
|
|
234
|
+
rawTools.forEach((raw) => {
|
|
49
235
|
let resolved = resolveRawWysiwygEditorTool(raw, vui);
|
|
50
236
|
if (!Array.isArray(resolved)) {
|
|
51
237
|
resolved = [resolved];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--v-wysiwyg-color-tool: 30px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.v-wysiwyg-color-tool {
|
|
6
|
+
&__button {
|
|
7
|
+
position: relative;
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
vertical-align: bottom;
|
|
12
|
+
|
|
13
|
+
&__bar {
|
|
14
|
+
position: absolute;
|
|
15
|
+
right: 0;
|
|
16
|
+
bottom: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
display: block;
|
|
19
|
+
height: 2px;
|
|
20
|
+
background: currentColor;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&__menu {
|
|
25
|
+
.v-dialog__content {
|
|
26
|
+
min-width: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.v-dialog__body {
|
|
30
|
+
padding: 2px;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&__items {
|
|
35
|
+
--item-size: var(--v-wysiwyg-color-tool);
|
|
36
|
+
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
flex-wrap: wrap;
|
|
39
|
+
align-items: center;
|
|
40
|
+
max-width: calc(var(--item-size) * 5);
|
|
41
|
+
vertical-align: bottom;
|
|
42
|
+
|
|
43
|
+
&--with-label {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
// flex-wrap: nowrap;
|
|
47
|
+
align-items: flex-start;
|
|
48
|
+
max-width: none;
|
|
49
|
+
padding: 2px;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&__item {
|
|
54
|
+
--focus-offset: calc(var(--item-size) * 0.05);
|
|
55
|
+
|
|
56
|
+
position: relative;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-basis: var(--item-size);
|
|
59
|
+
align-items: center;
|
|
60
|
+
width: var(--item-size);
|
|
61
|
+
height: var(--item-size);
|
|
62
|
+
padding: 0;
|
|
63
|
+
margin: 0;
|
|
64
|
+
// margin: 2px;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
background: transparent;
|
|
67
|
+
border: 0;
|
|
68
|
+
border-radius: 0;
|
|
69
|
+
outline: 0;
|
|
70
|
+
appearance: none;
|
|
71
|
+
|
|
72
|
+
&__color {
|
|
73
|
+
position: relative;
|
|
74
|
+
flex: 0 0 var(--item-size);
|
|
75
|
+
width: var(--item-size);
|
|
76
|
+
height: var(--item-size);
|
|
77
|
+
|
|
78
|
+
&::before {
|
|
79
|
+
position: absolute;
|
|
80
|
+
top: 0;
|
|
81
|
+
right: 0;
|
|
82
|
+
bottom: 0;
|
|
83
|
+
left: 0;
|
|
84
|
+
display: block;
|
|
85
|
+
content: '';
|
|
86
|
+
background: currentColor;
|
|
87
|
+
border: solid 1px transparent;
|
|
88
|
+
transition: all 0.15s;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&__name {
|
|
93
|
+
position: relative;
|
|
94
|
+
padding: 0 8px;
|
|
95
|
+
font-size: 12px;
|
|
96
|
+
white-space: nowrap;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
&:hover &__color,
|
|
100
|
+
&:focus &__color {
|
|
101
|
+
&::before {
|
|
102
|
+
top: var(--focus-offset);
|
|
103
|
+
right: var(--focus-offset);
|
|
104
|
+
bottom: var(--focus-offset);
|
|
105
|
+
left: var(--focus-offset);
|
|
106
|
+
border-color: rgba(0, 0, 0, 0.1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&__items--with-label &__item {
|
|
112
|
+
flex-basis: auto;
|
|
113
|
+
width: auto;
|
|
114
|
+
width: 100%;
|
|
115
|
+
|
|
116
|
+
&::before {
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: 0;
|
|
119
|
+
right: 0;
|
|
120
|
+
bottom: 0;
|
|
121
|
+
left: 0;
|
|
122
|
+
display: block;
|
|
123
|
+
content: '';
|
|
124
|
+
background: currentColor;
|
|
125
|
+
opacity: 0;
|
|
126
|
+
transition: opacity 0.15s;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&:hover,
|
|
130
|
+
&:focus {
|
|
131
|
+
&::before {
|
|
132
|
+
opacity: 0.1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
& + .v-wysiwyg-color-tool__item {
|
|
137
|
+
margin-top: 2px;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import './color.scss';
|
|
2
|
+
|
|
3
|
+
import { VNodeChild } from 'vue';
|
|
4
|
+
import { WysiwygColorExtension } from '../extensions';
|
|
5
|
+
import { type VuiService } from '../../../service';
|
|
6
|
+
import { WysiwygEditorToolFactory, WysiwygEditorTool } from '../schemes';
|
|
7
|
+
import TextStyle from '@tiptap/extension-text-style';
|
|
8
|
+
import { VIcon } from '../../VIcon';
|
|
9
|
+
|
|
10
|
+
export interface WysiwygColorItem {
|
|
11
|
+
key?: string | number;
|
|
12
|
+
name?: VNodeChild | ((vui: VuiService) => VNodeChild);
|
|
13
|
+
color: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CreateWysiwygColorToolOptions {
|
|
17
|
+
items: WysiwygColorItem[];
|
|
18
|
+
withLabel?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createWysiwygColorTool(opts: CreateWysiwygColorToolOptions) {
|
|
22
|
+
const WysiwygColorTool: WysiwygEditorToolFactory = (vui) => {
|
|
23
|
+
const tool: WysiwygEditorTool = {
|
|
24
|
+
key: 'textColor',
|
|
25
|
+
// active: ({ editor }) => editor.isActive('textStyle'),
|
|
26
|
+
icon:
|
|
27
|
+
({ editor }) =>
|
|
28
|
+
() => {
|
|
29
|
+
const color: string | undefined =
|
|
30
|
+
editor.getAttributes('textStyle').color;
|
|
31
|
+
const style = { color };
|
|
32
|
+
return (
|
|
33
|
+
<span class="v-wysiwyg-color-tool__button">
|
|
34
|
+
<VIcon
|
|
35
|
+
class="v-wysiwyg-color-tool__button__icon"
|
|
36
|
+
name={vui.icon('editorTextColor')}
|
|
37
|
+
/>
|
|
38
|
+
<span class="v-wysiwyg-color-tool__button__bar" style={style} />
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
onClick: (ctx, ev) => {
|
|
43
|
+
ctx.vui.menu({
|
|
44
|
+
class: 'v-wysiwyg-color-tool__menu',
|
|
45
|
+
activator: ev,
|
|
46
|
+
content: (stack) => (
|
|
47
|
+
<div
|
|
48
|
+
class={[
|
|
49
|
+
'v-wysiwyg-color-tool__items',
|
|
50
|
+
{ 'v-wysiwyg-color-tool__items--with-label': opts.withLabel },
|
|
51
|
+
]}>
|
|
52
|
+
{opts.items.map((item, index) => (
|
|
53
|
+
<button
|
|
54
|
+
key={item.key == null ? index : item.key}
|
|
55
|
+
class="v-wysiwyg-color-tool__item"
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={() => {
|
|
58
|
+
const { color } = item;
|
|
59
|
+
let command = ctx.editor.chain().focus();
|
|
60
|
+
if (color) {
|
|
61
|
+
command = command.setColor(color);
|
|
62
|
+
} else {
|
|
63
|
+
command = command.unsetColor();
|
|
64
|
+
}
|
|
65
|
+
command.run();
|
|
66
|
+
stack.close();
|
|
67
|
+
}}>
|
|
68
|
+
<span
|
|
69
|
+
class="v-wysiwyg-color-tool__item__color"
|
|
70
|
+
style={item.color ? { color: item.color } : {}}
|
|
71
|
+
/>
|
|
72
|
+
{opts.withLabel && (
|
|
73
|
+
<span class="v-wysiwyg-color-tool__item__name">
|
|
74
|
+
{item.name}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</button>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
floating: true,
|
|
84
|
+
extensions: [TextStyle, WysiwygColorExtension],
|
|
85
|
+
};
|
|
86
|
+
return tool;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return WysiwygColorTool;
|
|
90
|
+
}
|