@adminforth/rich-editor 1.1.0 โ†’ 1.2.0

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.
@@ -22,9 +22,8 @@ steps:
22
22
  image: node:20
23
23
  when:
24
24
  - event: push
25
- volumes:
26
- - /var/run/docker.sock:/var/run/docker.sock
27
25
  commands:
26
+ - apt update && apt install -y rsync
28
27
  - export $(cat /woodpecker/deploy.vault.env | xargs)
29
28
  - npm clean-install
30
29
  - /bin/bash ./.woodpecker/buildRelease.sh
package/build.log CHANGED
@@ -1,4 +1,14 @@
1
1
 
2
- > @adminforth/rich-editor@1.0.18 build
3
- > tsc
2
+ > @adminforth/rich-editor@1.0.0 build
3
+ > tsc && rsync -av --exclude 'node_modules' custom dist/
4
4
 
5
+ sending incremental file list
6
+ custom/
7
+ custom/async-queue.ts
8
+ custom/package-lock.json
9
+ custom/package.json
10
+ custom/quillEditor.vue
11
+ custom/tsconfig.json
12
+
13
+ sent 19,594 bytes received 115 bytes 39,418.00 bytes/sec
14
+ total size is 19,183 speedup is 0.97
@@ -0,0 +1,31 @@
1
+
2
+
3
+ export default class AsyncQueue {
4
+ queue: (() => Promise<any>)[];
5
+ processing: boolean;
6
+
7
+ constructor() {
8
+ this.queue = [];
9
+ this.processing = false;
10
+ }
11
+
12
+ async add(task: () => Promise<any>) {
13
+ this.queue.push(task);
14
+ if (!this.processing) {
15
+ this.process();
16
+ }
17
+ }
18
+
19
+ async process() {
20
+ this.processing = true;
21
+ while (this.queue.length > 0) {
22
+ const task = this.queue.shift()!;
23
+ try {
24
+ await task();
25
+ } catch (error) {
26
+ console.error('Task encountered an error:', error);
27
+ }
28
+ }
29
+ this.processing = false;
30
+ }
31
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "custom",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "custom",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "quill": "^2.0.2"
13
+ }
14
+ },
15
+ "node_modules/eventemitter3": {
16
+ "version": "5.0.1",
17
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
18
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
19
+ },
20
+ "node_modules/fast-diff": {
21
+ "version": "1.3.0",
22
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
23
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
24
+ },
25
+ "node_modules/lodash-es": {
26
+ "version": "4.17.21",
27
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
28
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
29
+ },
30
+ "node_modules/lodash.clonedeep": {
31
+ "version": "4.5.0",
32
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
33
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
34
+ },
35
+ "node_modules/lodash.isequal": {
36
+ "version": "4.5.0",
37
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
38
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
39
+ },
40
+ "node_modules/parchment": {
41
+ "version": "3.0.0",
42
+ "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
43
+ "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A=="
44
+ },
45
+ "node_modules/quill": {
46
+ "version": "2.0.2",
47
+ "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
48
+ "integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
49
+ "license": "BSD-3-Clause",
50
+ "dependencies": {
51
+ "eventemitter3": "^5.0.1",
52
+ "lodash-es": "^4.17.21",
53
+ "parchment": "^3.0.0",
54
+ "quill-delta": "^5.1.0"
55
+ },
56
+ "engines": {
57
+ "npm": ">=8.2.3"
58
+ }
59
+ },
60
+ "node_modules/quill-delta": {
61
+ "version": "5.1.0",
62
+ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
63
+ "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
64
+ "dependencies": {
65
+ "fast-diff": "^1.3.0",
66
+ "lodash.clonedeep": "^4.5.0",
67
+ "lodash.isequal": "^4.5.0"
68
+ },
69
+ "engines": {
70
+ "node": ">= 12.0.0"
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "custom",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "quill": "^2.0.2"
14
+ }
15
+ }
@@ -0,0 +1,562 @@
1
+ <template>
2
+ <div
3
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500
4
+ focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400
5
+ dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 whitespace-normal af-quill-editor"
6
+ >
7
+ <div
8
+ ref="editor"
9
+ @keydown.tab.prevent.stop="approveCompletion('all')"
10
+ @keydown.ctrl.right.prevent.stop="approveCompletion('word')"
11
+ @keydown.ctrl.down.prevent.stop="startCompletion()"
12
+ >
13
+
14
+ </div>
15
+ </div>
16
+
17
+
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { onMounted, ref, onUnmounted, watch, type Ref } from "vue";
22
+ import { callAdminForthApi } from '@/utils';
23
+ import { AdminForthColumnCommon } from '@/types/Common';
24
+ import adminforth from '@/adminforth';
25
+
26
+ import AsyncQueue from './async-queue';
27
+ import Quill from "quill";
28
+ import "quill/dist/quill.snow.css";
29
+
30
+
31
+ function dbg(title: string,...args: any[]) {
32
+ // return; // comment for debug
33
+ console.log(title, ...args.map(a =>JSON.stringify(a, null, 1)));
34
+ }
35
+
36
+ // blots/embed: Represents inline embed elements, like images or videos that can be inserted into the text flow.
37
+ const Embed = Quill.import('blots/embed');
38
+ const BlockEmbed = Quill.import('blots/block/embed');
39
+
40
+ // @ts-ignore
41
+ class CompleteBlot extends Embed {
42
+ static blotName = 'complete';
43
+ static tagName = 'span';
44
+ // https://stackoverflow.com/a/78434756/27379293
45
+ static className = "complete-blot";
46
+
47
+ static create(value: { text: string }) {
48
+ let node = super.create();
49
+ // we should keep contenteditable=true for case when user clicks on empty area
50
+ // node.setAttribute('contenteditable', 'false');
51
+ node.setAttribute('completer', '');
52
+ node.innerText = value.text;
53
+ return node;
54
+ }
55
+
56
+ static value(node: HTMLElement) {
57
+ return {
58
+ text: node.innerText,
59
+ };
60
+ }
61
+ }
62
+
63
+ // @ts-ignore
64
+ class ImageBlot extends BlockEmbed {
65
+ static blotName = 'image';
66
+ static tagName = 'img';
67
+
68
+ static create(value) {
69
+ let node = super.create();
70
+ node.setAttribute('alt', value.alt);
71
+ node.setAttribute('src', value.url);
72
+ node.setAttribute('data-s3path', value['s3Path']);
73
+ return node;
74
+ }
75
+
76
+ static value(node) {
77
+ return {
78
+ alt: node.getAttribute('alt'),
79
+ url: node.getAttribute('src'),
80
+ s3Path: node.getAttribute('data-s3path'),
81
+ };
82
+ }
83
+ }
84
+
85
+ // @ts-ignore
86
+ Quill.register(CompleteBlot);
87
+ // @ts-ignore
88
+ Quill.register(ImageBlot);
89
+
90
+ const updaterQueue = new AsyncQueue();
91
+
92
+ const props = defineProps<{
93
+ column: AdminForthColumn,
94
+ record: any,
95
+ meta: any,
96
+ }>();
97
+
98
+ const emit = defineEmits([
99
+ 'update:value',
100
+ ]);
101
+
102
+ const currentValue: Ref<string> = ref('');
103
+
104
+ const editor = ref<HTMLElement>();
105
+ const completion = ref<string[] | null>(null);
106
+ let quill: any = null;
107
+ const editorFocused = ref(false);
108
+
109
+ let lastText: string | null = null;
110
+
111
+ const imageProgress = ref(0);
112
+
113
+
114
+ async function saveToServer(file: File) {
115
+ const fd = new FormData();
116
+ fd.append('image', file);
117
+
118
+ const originalFilename = file.name.split('.').slice(0, -1).join('.');
119
+ const originalExtension = file.name.split('.').pop();
120
+ // send fd to s3
121
+ const { uploadUrl, tagline, previewUrl, s3Path, error } = await callAdminForthApi({
122
+ path: `/plugin/${props.meta.uploadPluginInstanceId}/get_s3_upload_url`,
123
+ method: 'POST',
124
+ body: {
125
+ originalFilename,
126
+ contentType: file.type,
127
+ size: file.size,
128
+ originalExtension,
129
+ },
130
+ });
131
+
132
+ if (error) {
133
+ adminforth.alert({
134
+ message: `File was not uploaded because of error: ${error}`,
135
+ variant: 'danger'
136
+ });
137
+ return;
138
+ }
139
+
140
+ const xhr = new XMLHttpRequest();
141
+ const success = await new Promise((resolve) => {
142
+ xhr.upload.onprogress = (e) => {
143
+ if (e.lengthComputable) {
144
+ imageProgress.value = Math.round((e.loaded / e.total) * 100);
145
+ }
146
+ };
147
+ xhr.addEventListener('loadend', () => {
148
+ const success = xhr.readyState === 4 && xhr.status === 200;
149
+ // try to read response
150
+ resolve(success);
151
+ });
152
+ xhr.open('PUT', uploadUrl, true);
153
+ xhr.setRequestHeader('Content-Type', file.type);
154
+ xhr.setRequestHeader('x-amz-tagging', tagline);
155
+ xhr.send(file);
156
+ });
157
+ if (!success) {
158
+ adminforth.alert({
159
+ messageHtml: `<div>Sorry but the file was not uploaded because of S3 Request Error: </div>
160
+ <pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
161
+ xhr.responseText.replace(/</g, '&lt;').replace(/>/g, '&gt;')
162
+ }</pre>`,
163
+ variant: 'danger',
164
+ timeout: 30,
165
+ });
166
+ imageProgress.value = 0;
167
+ return;
168
+ }
169
+
170
+ // here we have s3Path, call createResource to save the image
171
+ const range = quill.getSelection();
172
+ quill.insertEmbed(range.index, 'image', {
173
+ url: previewUrl,
174
+ s3Path: s3Path,
175
+ alt: file.name
176
+ }, 'user');
177
+
178
+ }
179
+
180
+ async function imageHandler() {
181
+ const input = document.createElement('input');
182
+ input.setAttribute('type', 'file');
183
+ input.click();
184
+
185
+ // Listen upload local image and save to server
186
+ input.onchange = () => {
187
+ const file = input.files[0];
188
+
189
+ // file type is only image.
190
+ if (/^image\//.test(file.type)) {
191
+ saveToServer(file);
192
+ } else {
193
+ console.warn('You could only upload images.');
194
+ }
195
+ };
196
+ }
197
+
198
+ onMounted(() => {
199
+ currentValue.value = props.record[props.column.name] || '';
200
+ editor.value.innerHTML = currentValue.value;
201
+
202
+ quill = new Quill(editor.value as HTMLElement, {
203
+ theme: "snow",
204
+ readOnly:props.column?.editReadonly,
205
+ placeholder: 'Type here...',
206
+ // formats : ['complete'],
207
+ modules: {
208
+ toolbar: {
209
+ container: props.meta.toolbar || [
210
+ ['bold', 'italic', 'underline', 'strike'], // toggled buttons
211
+ ['blockquote', 'code-block', 'link', ...props.meta.uploadPluginInstanceId ? ['image'] : []],
212
+ // [
213
+ // // 'image',
214
+ // // 'video',
215
+ // // 'formula'
216
+ // ],
217
+
218
+
219
+ [{ 'header': 2 }, { 'header': 3 }], // custom button values
220
+ [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
221
+ // [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
222
+ // [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
223
+ // [{ 'direction': 'rtl' }], // text direction
224
+
225
+ // [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
226
+ // [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
227
+
228
+ // [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
229
+ // [{ 'font': [] }],
230
+ [{ 'align': [] }],
231
+
232
+ ['clean'] // remove formatting button
233
+ ],
234
+ handlers: {
235
+ image: imageHandler,
236
+ },
237
+ },
238
+ keyboard: {
239
+ bindings: {
240
+ tab: {
241
+ key: 9,
242
+ handler: function (range: any, context: any) {
243
+ if (completion.value !== null) {
244
+ return true;
245
+ }
246
+ },
247
+ },
248
+ },
249
+ }
250
+ },
251
+ });
252
+
253
+ lastText = quill.getText();
254
+
255
+ quill.on(Quill.events.TEXT_CHANGE, async (delta: any, oldDelta: any, source: string) => {
256
+ dbg('๐Ÿชฝ TEXT_CHANGE fired ', delta, oldDelta, source);
257
+ updaterQueue.add(emitTextUpdate);
258
+ startCompletion();
259
+ });
260
+
261
+ quill.on('selection-change', (range: any, oldRange: any, source: string) => {
262
+ dbg('๐Ÿชฝ selection changed', range, oldRange, source);
263
+ if (range === null) {
264
+ // blur event
265
+ removeCompletionOnBlur();
266
+ editorFocused.value = false;
267
+ return;
268
+ } else {
269
+ editorFocused.value = true;
270
+ startCompletion();
271
+ }
272
+ const text = quill.getText();
273
+ // don't allow to select after completion
274
+ // TODO
275
+ // if (range?.index === text.length) {
276
+ // console.log('RANGE IDX', range.index, text.length, 'text', JSON.stringify(text, null, 1));
277
+ // dbg('โœ‹ prevent selection after completion');
278
+ // quill.setSelection(text.length - 1, 0, 'silent');
279
+ // }
280
+ });
281
+
282
+
283
+ // handle right swipe on mobile uding document/window, and console log if swiped in right direction
284
+ if ('ontouchstart' in window) {
285
+ document.addEventListener('touchstart', handleTouchStart, false);
286
+ document.addEventListener('touchmove', handleTouchMove, false);
287
+ }
288
+
289
+ });
290
+
291
+
292
+ async function emitTextUpdate() {
293
+ const editorHtml = quill.root.innerHTML;
294
+ // remove completion from html
295
+ const html = editorHtml.replace(/<span[^>]*completer[^>]*>.*?<\/span>/g, '');
296
+
297
+ if (lastText === html) {
298
+ return;
299
+ }
300
+
301
+ lastText = html;
302
+
303
+ await (new Promise((resolve) => setTimeout(resolve, 0)));
304
+
305
+ dbg('โฌ†๏ธ emit value suggestion-input', html);
306
+ emit('update:value', html);
307
+ }
308
+
309
+ // Auto-Completion functions
310
+ let tmt: null | ReturnType<typeof setTimeout> = null;
311
+
312
+ let xDown: null | number = null;
313
+ let yDown: null | number = null;
314
+
315
+ function handleTouchStart(evt: TouchEvent) {
316
+ xDown = evt.touches[0].clientX;
317
+ yDown = evt.touches[0].clientY;
318
+ }
319
+
320
+ function handleTouchMove(evt: TouchEvent) {
321
+ if (!xDown || !yDown) {
322
+ return;
323
+ }
324
+
325
+ let xUp = evt.touches[0].clientX;
326
+ let yUp = evt.touches[0].clientY;
327
+
328
+ let xDiff = xDown - xUp;
329
+ let yDiff = yDown - yUp;
330
+
331
+ if (Math.abs(xDiff) > Math.abs(yDiff)) {
332
+ if (xDiff < 0) {
333
+ // complete word if completion and input is focused
334
+ dbg('๐Ÿ‘‡ swipe right', completion.value, editorFocused.value);
335
+ if (completion.value !== null && editorFocused.value) {
336
+ approveCompletion('word');
337
+ // [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/feature/5093566007214080
338
+ // evt.preventDefault();
339
+ evt.stopPropagation();
340
+ }
341
+ }
342
+ }
343
+
344
+ xDown = null;
345
+ yDown = null;
346
+ }
347
+
348
+ onUnmounted(() => {
349
+ quill.off(Quill.events.TEXT_CHANGE);
350
+ quill.off('selection-change');
351
+
352
+ if ('ontouchstart' in window) {
353
+ document.removeEventListener('touchstart', handleTouchStart);
354
+ document.removeEventListener('touchmove', handleTouchMove);
355
+ }
356
+ });
357
+
358
+
359
+ async function complete(textBeforeCursor: string) {
360
+ const res = await callAdminForthApi({
361
+ path: `/plugin/${props.meta.pluginInstanceId}/doComplete`,
362
+ method: 'POST',
363
+ body: {
364
+ record: {...props.record, [props.column.name]: textBeforeCursor},
365
+ },
366
+ });
367
+
368
+ return res.completion;
369
+ }
370
+
371
+ function updateCompleteEmbed(text: string) {
372
+ const curCursorPos = quill.getSelection();
373
+ const d = quill.getContents();
374
+ const c = d.ops.find((op: any) => op.insert.complete);
375
+ if (!c) {
376
+ return;
377
+ }
378
+ c.insert.complete.text = text;
379
+ quill.setContents(d.ops, 'silent');
380
+ quill.setSelection(curCursorPos.index, curCursorPos.length, 'silent');
381
+ }
382
+
383
+ function deleteCompleteEmbed() {
384
+ const completeNode = quill.root.querySelector('[completer]');
385
+ const completeBlot = Quill.find(completeNode);
386
+ const blotIdx: number | null = completeBlot ? quill.getIndex(completeBlot) : null;
387
+
388
+ dbg('๐Ÿ‘‡ complete blot idx', blotIdx);
389
+
390
+ if (blotIdx !== null) {
391
+ quill.deleteText(blotIdx, 1, 'silent');
392
+ }
393
+ }
394
+
395
+ function approveCompletion(type: 'all' | 'word') {
396
+ if (!props.meta.shouldComplete) {
397
+ return;
398
+ }
399
+
400
+ dbg('๐Ÿ’จ approveCompletion')
401
+
402
+ if (completion.value === null) {
403
+ return;
404
+ }
405
+
406
+ const cursorPosition = quill.getSelection();
407
+
408
+ let shouldComplete = false;
409
+ if (type === 'all') {
410
+ dbg(`๐Ÿ‘‡ insert all at ${cursorPosition.index}, ${completion.value.join('')}`);
411
+ deleteCompleteEmbed();
412
+ quill.insertText(cursorPosition.index, completion.value.join(''), 'silent');
413
+ shouldComplete = true;
414
+ } else {
415
+ const word = completion.value[0];
416
+ quill.insertText(cursorPosition.index, word, 'silent');
417
+ completion.value = completion.value.slice(1);
418
+ if (completion.value.length === 0) {
419
+ shouldComplete = true;
420
+ } else {
421
+ // update completion
422
+ // TODO probably better way to update Embed?
423
+ updateCompleteEmbed(completion.value.join(''));
424
+ }
425
+ }
426
+
427
+ updaterQueue.add(emitTextUpdate);
428
+
429
+ if (shouldComplete) {
430
+ startCompletion();
431
+ }
432
+
433
+ }
434
+
435
+ async function startCompletion() {
436
+ if (!props.meta.shouldComplete || props.column?.editReadonly ) {
437
+ return;
438
+ }
439
+ completion.value = null;
440
+ deleteCompleteEmbed();
441
+
442
+ if (tmt) {
443
+ clearTimeout(tmt);
444
+ }
445
+ tmt = setTimeout(async () => {
446
+ const currentTmt = tmt;
447
+ const cursorPosition = quill.getSelection();
448
+ dbg('๐Ÿ‘‡ get pos', cursorPosition.index, cursorPosition.length)
449
+ if (cursorPosition.length !== 0) {
450
+ // we will not complete if text selected
451
+ return;
452
+ }
453
+
454
+ const charAfterCursor = quill.getText(cursorPosition.index, 1);
455
+ dbg('๐Ÿ‘‡ charAfterCursor', charAfterCursor);
456
+ if (charAfterCursor !== '\n') {
457
+ // we will not complete if not at the end of the line
458
+ return;
459
+ }
460
+
461
+ const textBeforeCursor = quill.getText(0, cursorPosition.index);
462
+
463
+ const completionAnswer = await complete(textBeforeCursor);
464
+ if (currentTmt !== tmt) {
465
+ // while we were waiting for completion, new completion was started
466
+ return;
467
+ }
468
+
469
+ quill.insertEmbed(cursorPosition.index, 'complete', { text: completionAnswer.join('') }, 'silent');
470
+
471
+ //dbg('๐Ÿ‘‡ set pos', cursorPosition.index, cursorPosition.length)
472
+ //quill.setSelection(cursorPosition.index, cursorPosition.length, 'silent');
473
+
474
+ completion.value = completionAnswer;
475
+
476
+ dbg('๐Ÿ‘‡ completion finished', quill.getContents());
477
+
478
+ }, props.meta.debounceTime || 300);
479
+ }
480
+
481
+ function removeCompletionOnBlur() {
482
+ if (lastText?.trim().length === 0) {
483
+ completion.value = null;
484
+ const d = quill.getContents();
485
+ const i = d.ops.findIndex((op: any) => op.insert.complete);
486
+ if (i !== -1) {
487
+ d.ops.splice(i, 1);
488
+ quill.setContents(d, 'silent');
489
+ dbg('๐Ÿงน Cleaned completion from ops to make ph visible');
490
+ }
491
+ }
492
+ }
493
+
494
+ </script>
495
+
496
+ <style lang="scss">
497
+
498
+ .af-quill-editor {
499
+
500
+ .ql-toolbar.ql-snow[class] {
501
+ border: none;
502
+ padding: 0 0 1rem 0;
503
+ .ql-picker-label{
504
+ padding-left: 0;
505
+ }
506
+ }
507
+
508
+ .ql-container {
509
+ border: 0;
510
+ .ql-editor {
511
+ position: relative;
512
+ padding: 0;
513
+ min-height: 100px;
514
+ &.ql-blank::before {
515
+ left: 0px;
516
+ font-style: normal;
517
+ }
518
+ }
519
+ }
520
+
521
+ // .ql-editor:not(:focus) [completer] {
522
+ // display: none;
523
+ // }
524
+
525
+ .ql-editor [completer] {
526
+ color: gray;
527
+ font-style: italic;
528
+ }
529
+
530
+ .ql-editor p {
531
+ margin-bottom: 0.5rem;
532
+ }
533
+
534
+ .ql-snow .ql-stroke {
535
+ @apply dark:stroke-darkPrimary;
536
+ @apply stroke-lightPrimary;
537
+
538
+ }
539
+ .ql-snow button:hover .ql-stroke,
540
+ .ql-snow [role="button"]:hover .ql-stroke {
541
+ @apply dark:stroke-darkPrimary;
542
+ @apply stroke-lightPrimary;
543
+ filter: brightness(1.3);
544
+ }
545
+
546
+ .ql-snow .ql-fill {
547
+ @apply dark:fill-darkPrimary;
548
+ @apply fill-lightPrimary;
549
+ }
550
+
551
+ .ql-snow button:hover .ql-fill {
552
+ @apply dark:fill-darkPrimary;
553
+ @apply fill-lightPrimary;
554
+ filter: brightness(1.3);
555
+ }
556
+
557
+ }
558
+
559
+
560
+
561
+
562
+ </style>
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".", // This should point to your project root
4
+ "paths": {
5
+ "@/*": [
6
+ // "node_modules/adminforth/dist/spa/src/*"
7
+ "../../../spa/src/*"
8
+ ],
9
+ "*": [
10
+ // "node_modules/adminforth/dist/spa/node_modules/*"
11
+ "../../../spa/node_modules/*"
12
+ ],
13
+ "@@/*": [
14
+ // "node_modules/adminforth/dist/spa/src/*"
15
+ "."
16
+ ]
17
+ }
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@adminforth/rich-editor",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Rich editor plugin for adminforth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
9
  "prepare": "npm link adminforth",
10
- "build": "tsc"
10
+ "build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",