@adminforth/rich-editor 1.0.17 โ 1.1.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.
- package/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +44 -0
- package/.woodpecker/release.yml +43 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/build.log +4 -0
- package/custom/quillEditor.vue +6 -4
- package/custom/tsconfig.json +19 -0
- package/dist/index.js +7 -3
- package/index.ts +8 -5
- package/package.json +43 -6
- package/ChangeLog.md +0 -11
- package/dist/custom/async-queue.ts +0 -31
- package/dist/custom/package-lock.json +0 -74
- package/dist/custom/package.json +0 -15
- package/dist/custom/quillEditor.vue +0 -560
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
#!/bin/bash
|
|
3
|
+
|
|
4
|
+
# write npm run output both to console and to build.log
|
|
5
|
+
npm run build 2>&1 | tee build.log
|
|
6
|
+
build_status=${PIPESTATUS[0]}
|
|
7
|
+
|
|
8
|
+
# if exist status from the npm run build is not 0
|
|
9
|
+
# then exit with the status code from the npm run build
|
|
10
|
+
if [ $build_status -ne 0 ]; then
|
|
11
|
+
echo "Build failed. Exiting with status code $build_status"
|
|
12
|
+
exit $build_status
|
|
13
|
+
fi
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -x
|
|
4
|
+
|
|
5
|
+
COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if [ "$CI_STEP_STATUS" = "success" ]; then
|
|
9
|
+
MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
|
|
10
|
+
|
|
11
|
+
curl -s -X POST -H "Content-Type: application/json" -d '{
|
|
12
|
+
"username": "'"$CI_COMMIT_AUTHOR"'",
|
|
13
|
+
"icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
|
|
14
|
+
"attachments": [
|
|
15
|
+
{
|
|
16
|
+
"mrkdwn_in": ["text", "pretext"],
|
|
17
|
+
"color": "#36a64f",
|
|
18
|
+
"text": "'"$MESSAGE"'"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}' "$DEVELOPERS_SLACK_WEBHOOK"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
export BUILD_LOG=$(cat ./build.log)
|
|
25
|
+
|
|
26
|
+
BUILD_LOG=$(echo $BUILD_LOG | sed 's/"/\\"/g')
|
|
27
|
+
|
|
28
|
+
MESSAGE="Broke \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\` with commit _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
|
|
29
|
+
CODE_BLOCK="\`\`\`$BUILD_LOG\n\`\`\`"
|
|
30
|
+
|
|
31
|
+
echo "Sending slack message to developers $MESSAGE"
|
|
32
|
+
# Send the message
|
|
33
|
+
curl -sS -X POST -H "Content-Type: application/json" -d '{
|
|
34
|
+
"username": "'"$CI_COMMIT_AUTHOR"'",
|
|
35
|
+
"icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
|
|
36
|
+
"attachments": [
|
|
37
|
+
{
|
|
38
|
+
"mrkdwn_in": ["text", "pretext"],
|
|
39
|
+
"color": "#8A1C12",
|
|
40
|
+
"text": "'"$CODE_BLOCK"'",
|
|
41
|
+
"pretext": "'"$MESSAGE"'"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}' "$DEVELOPERS_SLACK_WEBHOOK" 2>&1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
clone:
|
|
2
|
+
git:
|
|
3
|
+
image: woodpeckerci/plugin-git
|
|
4
|
+
settings:
|
|
5
|
+
partial: false
|
|
6
|
+
depth: 5
|
|
7
|
+
|
|
8
|
+
steps:
|
|
9
|
+
init-secrets:
|
|
10
|
+
when:
|
|
11
|
+
- event: push
|
|
12
|
+
image: infisical/cli
|
|
13
|
+
environment:
|
|
14
|
+
INFISICAL_TOKEN:
|
|
15
|
+
from_secret: VAULT_TOKEN
|
|
16
|
+
commands:
|
|
17
|
+
- infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
|
|
18
|
+
secrets:
|
|
19
|
+
- VAULT_TOKEN
|
|
20
|
+
|
|
21
|
+
release:
|
|
22
|
+
image: node:20
|
|
23
|
+
when:
|
|
24
|
+
- event: push
|
|
25
|
+
volumes:
|
|
26
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
27
|
+
commands:
|
|
28
|
+
- export $(cat /woodpecker/deploy.vault.env | xargs)
|
|
29
|
+
- npm clean-install
|
|
30
|
+
- /bin/bash ./.woodpecker/buildRelease.sh
|
|
31
|
+
- npm audit signatures
|
|
32
|
+
- npx semantic-release
|
|
33
|
+
|
|
34
|
+
slack-on-failure:
|
|
35
|
+
when:
|
|
36
|
+
- event: push
|
|
37
|
+
status: [failure, success]
|
|
38
|
+
- event: push
|
|
39
|
+
image: curlimages/curl
|
|
40
|
+
commands:
|
|
41
|
+
- export $(cat /woodpecker/deploy.vault.env | xargs)
|
|
42
|
+
- /bin/sh ./.woodpecker/buildSlackNotify.sh
|
|
43
|
+
|
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Devforth.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/build.log
ADDED
package/custom/quillEditor.vue
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
2
|
<div
|
|
4
3
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500
|
|
5
4
|
focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400
|
|
@@ -22,6 +21,8 @@
|
|
|
22
21
|
import { onMounted, ref, onUnmounted, watch, type Ref } from "vue";
|
|
23
22
|
import { callAdminForthApi } from '@/utils';
|
|
24
23
|
import { AdminForthColumnCommon } from '@/types/Common';
|
|
24
|
+
import adminforth from '@/adminforth';
|
|
25
|
+
|
|
25
26
|
import AsyncQueue from './async-queue';
|
|
26
27
|
import Quill from "quill";
|
|
27
28
|
import "quill/dist/quill.snow.css";
|
|
@@ -129,7 +130,7 @@ async function saveToServer(file: File) {
|
|
|
129
130
|
});
|
|
130
131
|
|
|
131
132
|
if (error) {
|
|
132
|
-
|
|
133
|
+
adminforth.alert({
|
|
133
134
|
message: `File was not uploaded because of error: ${error}`,
|
|
134
135
|
variant: 'danger'
|
|
135
136
|
});
|
|
@@ -154,7 +155,7 @@ async function saveToServer(file: File) {
|
|
|
154
155
|
xhr.send(file);
|
|
155
156
|
});
|
|
156
157
|
if (!success) {
|
|
157
|
-
|
|
158
|
+
adminforth.alert({
|
|
158
159
|
messageHtml: `<div>Sorry but the file was not uploaded because of S3 Request Error: </div>
|
|
159
160
|
<pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
|
|
160
161
|
xhr.responseText.replace(/</g, '<').replace(/>/g, '>')
|
|
@@ -200,6 +201,7 @@ onMounted(() => {
|
|
|
200
201
|
|
|
201
202
|
quill = new Quill(editor.value as HTMLElement, {
|
|
202
203
|
theme: "snow",
|
|
204
|
+
readOnly:props.column?.editReadonly,
|
|
203
205
|
placeholder: 'Type here...',
|
|
204
206
|
// formats : ['complete'],
|
|
205
207
|
modules: {
|
|
@@ -431,7 +433,7 @@ function approveCompletion(type: 'all' | 'word') {
|
|
|
431
433
|
}
|
|
432
434
|
|
|
433
435
|
async function startCompletion() {
|
|
434
|
-
if (!props.meta.shouldComplete) {
|
|
436
|
+
if (!props.meta.shouldComplete || props.column?.editReadonly ) {
|
|
435
437
|
return;
|
|
436
438
|
}
|
|
437
439
|
completion.value = null;
|
|
@@ -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/dist/index.js
CHANGED
|
@@ -112,7 +112,7 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
112
112
|
});
|
|
113
113
|
})));
|
|
114
114
|
});
|
|
115
|
-
resourceConfig.hooks.create.afterSave.push((
|
|
115
|
+
(resourceConfig.hooks.create.afterSave).push((_a) => __awaiter(this, [_a], void 0, function* ({ record, adminUser }) {
|
|
116
116
|
// find all s3Paths in the html
|
|
117
117
|
const s3Paths = getAttachmentPathes(record[this.options.htmlFieldName]);
|
|
118
118
|
process.env.HEAVY_DEBUG && console.log('๐ธ Found s3Paths', s3Paths);
|
|
@@ -122,7 +122,7 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
122
122
|
}));
|
|
123
123
|
// after edit we need to delete attachments that are not in the html anymore
|
|
124
124
|
// and add new ones
|
|
125
|
-
resourceConfig.hooks.edit.afterSave.push((
|
|
125
|
+
(resourceConfig.hooks.edit.afterSave).push((_a) => __awaiter(this, [_a], void 0, function* ({ recordId, record, adminUser }) {
|
|
126
126
|
process.env.HEAVY_DEBUG && console.log('โ Cought hook', recordId, 'rec', record);
|
|
127
127
|
if (record[this.options.htmlFieldName] === undefined) {
|
|
128
128
|
// field was not changed, do nothing
|
|
@@ -147,7 +147,7 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
147
147
|
return { ok: true };
|
|
148
148
|
}));
|
|
149
149
|
// after delete we need to delete all attachments
|
|
150
|
-
resourceConfig.hooks.delete.afterSave.push((
|
|
150
|
+
(resourceConfig.hooks.delete.afterSave).push((_a) => __awaiter(this, [_a], void 0, function* ({ record, adminUser }) {
|
|
151
151
|
const existingAparts = yield adminforth.resource(this.options.attachments.attachmentResource).list([
|
|
152
152
|
Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, record[editorRecordPkField.name]),
|
|
153
153
|
Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
|
|
@@ -161,7 +161,11 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
164
|
+
var _a, _b;
|
|
164
165
|
this.adminforth = adminforth;
|
|
166
|
+
if ((_a = this.options.completion) === null || _a === void 0 ? void 0 : _a.adapter) {
|
|
167
|
+
(_b = this.options.completion) === null || _b === void 0 ? void 0 : _b.adapter.validate();
|
|
168
|
+
}
|
|
165
169
|
// optional method where you can safely check field types after database discovery was performed
|
|
166
170
|
if (this.options.completion && !this.options.completion.adapter) {
|
|
167
171
|
throw new Error(`Completion adapter is required`);
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import type { IAdminForth, IHttpServer, AdminForthResource, AdminUser
|
|
2
|
+
import type { IAdminForth, IHttpServer, AdminForthResource, AdminUser } from "adminforth";
|
|
3
3
|
import type { PluginOptions } from './types.js';
|
|
4
4
|
import { AdminForthPlugin, Filters, RateLimiter } from "adminforth";
|
|
5
5
|
import * as cheerio from 'cheerio';
|
|
@@ -142,7 +142,7 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
(resourceConfig.hooks.create.afterSave
|
|
145
|
+
(resourceConfig.hooks.create.afterSave).push(async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {
|
|
146
146
|
// find all s3Paths in the html
|
|
147
147
|
const s3Paths = getAttachmentPathes(record[this.options.htmlFieldName])
|
|
148
148
|
|
|
@@ -157,7 +157,7 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
157
157
|
|
|
158
158
|
// after edit we need to delete attachments that are not in the html anymore
|
|
159
159
|
// and add new ones
|
|
160
|
-
(resourceConfig.hooks.edit.afterSave
|
|
160
|
+
(resourceConfig.hooks.edit.afterSave).push(
|
|
161
161
|
async ({ recordId, record, adminUser }: { recordId: any, record: any, adminUser: AdminUser }) => {
|
|
162
162
|
process.env.HEAVY_DEBUG && console.log('โ Cought hook', recordId, 'rec', record);
|
|
163
163
|
if (record[this.options.htmlFieldName] === undefined) {
|
|
@@ -188,7 +188,7 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
188
188
|
);
|
|
189
189
|
|
|
190
190
|
// after delete we need to delete all attachments
|
|
191
|
-
(resourceConfig.hooks.delete.afterSave
|
|
191
|
+
(resourceConfig.hooks.delete.afterSave).push(
|
|
192
192
|
async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {
|
|
193
193
|
const existingAparts = await adminforth.resource(this.options.attachments.attachmentResource).list(
|
|
194
194
|
[
|
|
@@ -202,12 +202,15 @@ export default class RichEditorPlugin extends AdminForthPlugin {
|
|
|
202
202
|
|
|
203
203
|
return { ok: true };
|
|
204
204
|
}
|
|
205
|
-
|
|
205
|
+
);
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
210
210
|
this.adminforth = adminforth;
|
|
211
|
+
if (this.options.completion?.adapter) {
|
|
212
|
+
this.options.completion?.adapter.validate();
|
|
213
|
+
}
|
|
211
214
|
// optional method where you can safely check field types after database discovery was performed
|
|
212
215
|
if (this.options.completion && !this.options.completion.adapter) {
|
|
213
216
|
throw new Error(`Completion adapter is required`);
|
package/package.json
CHANGED
|
@@ -1,18 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/rich-editor",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"prepare": "npm link adminforth",
|
|
10
|
+
"build": "tsc"
|
|
11
11
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/devforth/adminforth-rich-editor.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"adminforth",
|
|
18
|
+
"rich editor"
|
|
19
|
+
],
|
|
20
|
+
"author": "devforth",
|
|
14
21
|
"license": "ISC",
|
|
15
22
|
"dependencies": {
|
|
16
23
|
"cheerio": "^1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.10.7",
|
|
27
|
+
"semantic-release": "^24.2.1",
|
|
28
|
+
"semantic-release-slack-bot": "^4.0.2",
|
|
29
|
+
"typescript": "^5.7.3"
|
|
30
|
+
},
|
|
31
|
+
"release": {
|
|
32
|
+
"plugins": [
|
|
33
|
+
"@semantic-release/commit-analyzer",
|
|
34
|
+
"@semantic-release/release-notes-generator",
|
|
35
|
+
"@semantic-release/npm",
|
|
36
|
+
"@semantic-release/github",
|
|
37
|
+
[
|
|
38
|
+
"semantic-release-slack-bot",
|
|
39
|
+
{
|
|
40
|
+
"notifyOnSuccess": true,
|
|
41
|
+
"notifyOnFail": true,
|
|
42
|
+
"slackIcon": ":package:",
|
|
43
|
+
"markdownReleaseNotes": true
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
],
|
|
47
|
+
"branches": [
|
|
48
|
+
"main",
|
|
49
|
+
{
|
|
50
|
+
"name": "next",
|
|
51
|
+
"prerelease": true
|
|
52
|
+
}
|
|
53
|
+
]
|
|
17
54
|
}
|
|
18
55
|
}
|
package/ChangeLog.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/custom/package.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,560 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
|
|
3
|
-
<div
|
|
4
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500
|
|
5
|
-
focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400
|
|
6
|
-
dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 whitespace-normal af-quill-editor"
|
|
7
|
-
>
|
|
8
|
-
<div
|
|
9
|
-
ref="editor"
|
|
10
|
-
@keydown.tab.prevent.stop="approveCompletion('all')"
|
|
11
|
-
@keydown.ctrl.right.prevent.stop="approveCompletion('word')"
|
|
12
|
-
@keydown.ctrl.down.prevent.stop="startCompletion()"
|
|
13
|
-
>
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</template>
|
|
20
|
-
|
|
21
|
-
<script setup lang="ts">
|
|
22
|
-
import { onMounted, ref, onUnmounted, watch, type Ref } from "vue";
|
|
23
|
-
import { callAdminForthApi } from '@/utils';
|
|
24
|
-
import { AdminForthColumnCommon } from '@/types/Common';
|
|
25
|
-
import AsyncQueue from './async-queue';
|
|
26
|
-
import Quill from "quill";
|
|
27
|
-
import "quill/dist/quill.snow.css";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
function dbg(title: string,...args: any[]) {
|
|
31
|
-
// return; // comment for debug
|
|
32
|
-
console.log(title, ...args.map(a =>JSON.stringify(a, null, 1)));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// blots/embed: Represents inline embed elements, like images or videos that can be inserted into the text flow.
|
|
36
|
-
const Embed = Quill.import('blots/embed');
|
|
37
|
-
const BlockEmbed = Quill.import('blots/block/embed');
|
|
38
|
-
|
|
39
|
-
// @ts-ignore
|
|
40
|
-
class CompleteBlot extends Embed {
|
|
41
|
-
static blotName = 'complete';
|
|
42
|
-
static tagName = 'span';
|
|
43
|
-
// https://stackoverflow.com/a/78434756/27379293
|
|
44
|
-
static className = "complete-blot";
|
|
45
|
-
|
|
46
|
-
static create(value: { text: string }) {
|
|
47
|
-
let node = super.create();
|
|
48
|
-
// we should keep contenteditable=true for case when user clicks on empty area
|
|
49
|
-
// node.setAttribute('contenteditable', 'false');
|
|
50
|
-
node.setAttribute('completer', '');
|
|
51
|
-
node.innerText = value.text;
|
|
52
|
-
return node;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static value(node: HTMLElement) {
|
|
56
|
-
return {
|
|
57
|
-
text: node.innerText,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// @ts-ignore
|
|
63
|
-
class ImageBlot extends BlockEmbed {
|
|
64
|
-
static blotName = 'image';
|
|
65
|
-
static tagName = 'img';
|
|
66
|
-
|
|
67
|
-
static create(value) {
|
|
68
|
-
let node = super.create();
|
|
69
|
-
node.setAttribute('alt', value.alt);
|
|
70
|
-
node.setAttribute('src', value.url);
|
|
71
|
-
node.setAttribute('data-s3path', value['s3Path']);
|
|
72
|
-
return node;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
static value(node) {
|
|
76
|
-
return {
|
|
77
|
-
alt: node.getAttribute('alt'),
|
|
78
|
-
url: node.getAttribute('src'),
|
|
79
|
-
s3Path: node.getAttribute('data-s3path'),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// @ts-ignore
|
|
85
|
-
Quill.register(CompleteBlot);
|
|
86
|
-
// @ts-ignore
|
|
87
|
-
Quill.register(ImageBlot);
|
|
88
|
-
|
|
89
|
-
const updaterQueue = new AsyncQueue();
|
|
90
|
-
|
|
91
|
-
const props = defineProps<{
|
|
92
|
-
column: AdminForthColumn,
|
|
93
|
-
record: any,
|
|
94
|
-
meta: any,
|
|
95
|
-
}>();
|
|
96
|
-
|
|
97
|
-
const emit = defineEmits([
|
|
98
|
-
'update:value',
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
const currentValue: Ref<string> = ref('');
|
|
102
|
-
|
|
103
|
-
const editor = ref<HTMLElement>();
|
|
104
|
-
const completion = ref<string[] | null>(null);
|
|
105
|
-
let quill: any = null;
|
|
106
|
-
const editorFocused = ref(false);
|
|
107
|
-
|
|
108
|
-
let lastText: string | null = null;
|
|
109
|
-
|
|
110
|
-
const imageProgress = ref(0);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
async function saveToServer(file: File) {
|
|
114
|
-
const fd = new FormData();
|
|
115
|
-
fd.append('image', file);
|
|
116
|
-
|
|
117
|
-
const originalFilename = file.name.split('.').slice(0, -1).join('.');
|
|
118
|
-
const originalExtension = file.name.split('.').pop();
|
|
119
|
-
// send fd to s3
|
|
120
|
-
const { uploadUrl, tagline, previewUrl, s3Path, error } = await callAdminForthApi({
|
|
121
|
-
path: `/plugin/${props.meta.uploadPluginInstanceId}/get_s3_upload_url`,
|
|
122
|
-
method: 'POST',
|
|
123
|
-
body: {
|
|
124
|
-
originalFilename,
|
|
125
|
-
contentType: file.type,
|
|
126
|
-
size: file.size,
|
|
127
|
-
originalExtension,
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (error) {
|
|
132
|
-
window.adminforth.alert({
|
|
133
|
-
message: `File was not uploaded because of error: ${error}`,
|
|
134
|
-
variant: 'danger'
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const xhr = new XMLHttpRequest();
|
|
140
|
-
const success = await new Promise((resolve) => {
|
|
141
|
-
xhr.upload.onprogress = (e) => {
|
|
142
|
-
if (e.lengthComputable) {
|
|
143
|
-
imageProgress.value = Math.round((e.loaded / e.total) * 100);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
xhr.addEventListener('loadend', () => {
|
|
147
|
-
const success = xhr.readyState === 4 && xhr.status === 200;
|
|
148
|
-
// try to read response
|
|
149
|
-
resolve(success);
|
|
150
|
-
});
|
|
151
|
-
xhr.open('PUT', uploadUrl, true);
|
|
152
|
-
xhr.setRequestHeader('Content-Type', file.type);
|
|
153
|
-
xhr.setRequestHeader('x-amz-tagging', tagline);
|
|
154
|
-
xhr.send(file);
|
|
155
|
-
});
|
|
156
|
-
if (!success) {
|
|
157
|
-
window.adminforth.alert({
|
|
158
|
-
messageHtml: `<div>Sorry but the file was not uploaded because of S3 Request Error: </div>
|
|
159
|
-
<pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
|
|
160
|
-
xhr.responseText.replace(/</g, '<').replace(/>/g, '>')
|
|
161
|
-
}</pre>`,
|
|
162
|
-
variant: 'danger',
|
|
163
|
-
timeout: 30,
|
|
164
|
-
});
|
|
165
|
-
imageProgress.value = 0;
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// here we have s3Path, call createResource to save the image
|
|
170
|
-
const range = quill.getSelection();
|
|
171
|
-
quill.insertEmbed(range.index, 'image', {
|
|
172
|
-
url: previewUrl,
|
|
173
|
-
s3Path: s3Path,
|
|
174
|
-
alt: file.name
|
|
175
|
-
}, 'user');
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function imageHandler() {
|
|
180
|
-
const input = document.createElement('input');
|
|
181
|
-
input.setAttribute('type', 'file');
|
|
182
|
-
input.click();
|
|
183
|
-
|
|
184
|
-
// Listen upload local image and save to server
|
|
185
|
-
input.onchange = () => {
|
|
186
|
-
const file = input.files[0];
|
|
187
|
-
|
|
188
|
-
// file type is only image.
|
|
189
|
-
if (/^image\//.test(file.type)) {
|
|
190
|
-
saveToServer(file);
|
|
191
|
-
} else {
|
|
192
|
-
console.warn('You could only upload images.');
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
onMounted(() => {
|
|
198
|
-
currentValue.value = props.record[props.column.name] || '';
|
|
199
|
-
editor.value.innerHTML = currentValue.value;
|
|
200
|
-
|
|
201
|
-
quill = new Quill(editor.value as HTMLElement, {
|
|
202
|
-
theme: "snow",
|
|
203
|
-
placeholder: 'Type here...',
|
|
204
|
-
// formats : ['complete'],
|
|
205
|
-
modules: {
|
|
206
|
-
toolbar: {
|
|
207
|
-
container: props.meta.toolbar || [
|
|
208
|
-
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
|
209
|
-
['blockquote', 'code-block', 'link', ...props.meta.uploadPluginInstanceId ? ['image'] : []],
|
|
210
|
-
// [
|
|
211
|
-
// // 'image',
|
|
212
|
-
// // 'video',
|
|
213
|
-
// // 'formula'
|
|
214
|
-
// ],
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
[{ 'header': 2 }, { 'header': 3 }], // custom button values
|
|
218
|
-
[{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
|
|
219
|
-
// [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
|
|
220
|
-
// [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
|
|
221
|
-
// [{ 'direction': 'rtl' }], // text direction
|
|
222
|
-
|
|
223
|
-
// [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
|
|
224
|
-
// [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
|
225
|
-
|
|
226
|
-
// [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
|
|
227
|
-
// [{ 'font': [] }],
|
|
228
|
-
[{ 'align': [] }],
|
|
229
|
-
|
|
230
|
-
['clean'] // remove formatting button
|
|
231
|
-
],
|
|
232
|
-
handlers: {
|
|
233
|
-
image: imageHandler,
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
keyboard: {
|
|
237
|
-
bindings: {
|
|
238
|
-
tab: {
|
|
239
|
-
key: 9,
|
|
240
|
-
handler: function (range: any, context: any) {
|
|
241
|
-
if (completion.value !== null) {
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
lastText = quill.getText();
|
|
252
|
-
|
|
253
|
-
quill.on(Quill.events.TEXT_CHANGE, async (delta: any, oldDelta: any, source: string) => {
|
|
254
|
-
dbg('๐ชฝ TEXT_CHANGE fired ', delta, oldDelta, source);
|
|
255
|
-
updaterQueue.add(emitTextUpdate);
|
|
256
|
-
startCompletion();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
quill.on('selection-change', (range: any, oldRange: any, source: string) => {
|
|
260
|
-
dbg('๐ชฝ selection changed', range, oldRange, source);
|
|
261
|
-
if (range === null) {
|
|
262
|
-
// blur event
|
|
263
|
-
removeCompletionOnBlur();
|
|
264
|
-
editorFocused.value = false;
|
|
265
|
-
return;
|
|
266
|
-
} else {
|
|
267
|
-
editorFocused.value = true;
|
|
268
|
-
startCompletion();
|
|
269
|
-
}
|
|
270
|
-
const text = quill.getText();
|
|
271
|
-
// don't allow to select after completion
|
|
272
|
-
// TODO
|
|
273
|
-
// if (range?.index === text.length) {
|
|
274
|
-
// console.log('RANGE IDX', range.index, text.length, 'text', JSON.stringify(text, null, 1));
|
|
275
|
-
// dbg('โ prevent selection after completion');
|
|
276
|
-
// quill.setSelection(text.length - 1, 0, 'silent');
|
|
277
|
-
// }
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// handle right swipe on mobile uding document/window, and console log if swiped in right direction
|
|
282
|
-
if ('ontouchstart' in window) {
|
|
283
|
-
document.addEventListener('touchstart', handleTouchStart, false);
|
|
284
|
-
document.addEventListener('touchmove', handleTouchMove, false);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
async function emitTextUpdate() {
|
|
291
|
-
const editorHtml = quill.root.innerHTML;
|
|
292
|
-
// remove completion from html
|
|
293
|
-
const html = editorHtml.replace(/<span[^>]*completer[^>]*>.*?<\/span>/g, '');
|
|
294
|
-
|
|
295
|
-
if (lastText === html) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
lastText = html;
|
|
300
|
-
|
|
301
|
-
await (new Promise((resolve) => setTimeout(resolve, 0)));
|
|
302
|
-
|
|
303
|
-
dbg('โฌ๏ธ emit value suggestion-input', html);
|
|
304
|
-
emit('update:value', html);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Auto-Completion functions
|
|
308
|
-
let tmt: null | ReturnType<typeof setTimeout> = null;
|
|
309
|
-
|
|
310
|
-
let xDown: null | number = null;
|
|
311
|
-
let yDown: null | number = null;
|
|
312
|
-
|
|
313
|
-
function handleTouchStart(evt: TouchEvent) {
|
|
314
|
-
xDown = evt.touches[0].clientX;
|
|
315
|
-
yDown = evt.touches[0].clientY;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function handleTouchMove(evt: TouchEvent) {
|
|
319
|
-
if (!xDown || !yDown) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
let xUp = evt.touches[0].clientX;
|
|
324
|
-
let yUp = evt.touches[0].clientY;
|
|
325
|
-
|
|
326
|
-
let xDiff = xDown - xUp;
|
|
327
|
-
let yDiff = yDown - yUp;
|
|
328
|
-
|
|
329
|
-
if (Math.abs(xDiff) > Math.abs(yDiff)) {
|
|
330
|
-
if (xDiff < 0) {
|
|
331
|
-
// complete word if completion and input is focused
|
|
332
|
-
dbg('๐ swipe right', completion.value, editorFocused.value);
|
|
333
|
-
if (completion.value !== null && editorFocused.value) {
|
|
334
|
-
approveCompletion('word');
|
|
335
|
-
// [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/feature/5093566007214080
|
|
336
|
-
// evt.preventDefault();
|
|
337
|
-
evt.stopPropagation();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
xDown = null;
|
|
343
|
-
yDown = null;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
onUnmounted(() => {
|
|
347
|
-
quill.off(Quill.events.TEXT_CHANGE);
|
|
348
|
-
quill.off('selection-change');
|
|
349
|
-
|
|
350
|
-
if ('ontouchstart' in window) {
|
|
351
|
-
document.removeEventListener('touchstart', handleTouchStart);
|
|
352
|
-
document.removeEventListener('touchmove', handleTouchMove);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
async function complete(textBeforeCursor: string) {
|
|
358
|
-
const res = await callAdminForthApi({
|
|
359
|
-
path: `/plugin/${props.meta.pluginInstanceId}/doComplete`,
|
|
360
|
-
method: 'POST',
|
|
361
|
-
body: {
|
|
362
|
-
record: {...props.record, [props.column.name]: textBeforeCursor},
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
return res.completion;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function updateCompleteEmbed(text: string) {
|
|
370
|
-
const curCursorPos = quill.getSelection();
|
|
371
|
-
const d = quill.getContents();
|
|
372
|
-
const c = d.ops.find((op: any) => op.insert.complete);
|
|
373
|
-
if (!c) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
c.insert.complete.text = text;
|
|
377
|
-
quill.setContents(d.ops, 'silent');
|
|
378
|
-
quill.setSelection(curCursorPos.index, curCursorPos.length, 'silent');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function deleteCompleteEmbed() {
|
|
382
|
-
const completeNode = quill.root.querySelector('[completer]');
|
|
383
|
-
const completeBlot = Quill.find(completeNode);
|
|
384
|
-
const blotIdx: number | null = completeBlot ? quill.getIndex(completeBlot) : null;
|
|
385
|
-
|
|
386
|
-
dbg('๐ complete blot idx', blotIdx);
|
|
387
|
-
|
|
388
|
-
if (blotIdx !== null) {
|
|
389
|
-
quill.deleteText(blotIdx, 1, 'silent');
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function approveCompletion(type: 'all' | 'word') {
|
|
394
|
-
if (!props.meta.shouldComplete) {
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
dbg('๐จ approveCompletion')
|
|
399
|
-
|
|
400
|
-
if (completion.value === null) {
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const cursorPosition = quill.getSelection();
|
|
405
|
-
|
|
406
|
-
let shouldComplete = false;
|
|
407
|
-
if (type === 'all') {
|
|
408
|
-
dbg(`๐ insert all at ${cursorPosition.index}, ${completion.value.join('')}`);
|
|
409
|
-
deleteCompleteEmbed();
|
|
410
|
-
quill.insertText(cursorPosition.index, completion.value.join(''), 'silent');
|
|
411
|
-
shouldComplete = true;
|
|
412
|
-
} else {
|
|
413
|
-
const word = completion.value[0];
|
|
414
|
-
quill.insertText(cursorPosition.index, word, 'silent');
|
|
415
|
-
completion.value = completion.value.slice(1);
|
|
416
|
-
if (completion.value.length === 0) {
|
|
417
|
-
shouldComplete = true;
|
|
418
|
-
} else {
|
|
419
|
-
// update completion
|
|
420
|
-
// TODO probably better way to update Embed?
|
|
421
|
-
updateCompleteEmbed(completion.value.join(''));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
updaterQueue.add(emitTextUpdate);
|
|
426
|
-
|
|
427
|
-
if (shouldComplete) {
|
|
428
|
-
startCompletion();
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async function startCompletion() {
|
|
434
|
-
if (!props.meta.shouldComplete) {
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
completion.value = null;
|
|
438
|
-
deleteCompleteEmbed();
|
|
439
|
-
|
|
440
|
-
if (tmt) {
|
|
441
|
-
clearTimeout(tmt);
|
|
442
|
-
}
|
|
443
|
-
tmt = setTimeout(async () => {
|
|
444
|
-
const currentTmt = tmt;
|
|
445
|
-
const cursorPosition = quill.getSelection();
|
|
446
|
-
dbg('๐ get pos', cursorPosition.index, cursorPosition.length)
|
|
447
|
-
if (cursorPosition.length !== 0) {
|
|
448
|
-
// we will not complete if text selected
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const charAfterCursor = quill.getText(cursorPosition.index, 1);
|
|
453
|
-
dbg('๐ charAfterCursor', charAfterCursor);
|
|
454
|
-
if (charAfterCursor !== '\n') {
|
|
455
|
-
// we will not complete if not at the end of the line
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const textBeforeCursor = quill.getText(0, cursorPosition.index);
|
|
460
|
-
|
|
461
|
-
const completionAnswer = await complete(textBeforeCursor);
|
|
462
|
-
if (currentTmt !== tmt) {
|
|
463
|
-
// while we were waiting for completion, new completion was started
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
quill.insertEmbed(cursorPosition.index, 'complete', { text: completionAnswer.join('') }, 'silent');
|
|
468
|
-
|
|
469
|
-
//dbg('๐ set pos', cursorPosition.index, cursorPosition.length)
|
|
470
|
-
//quill.setSelection(cursorPosition.index, cursorPosition.length, 'silent');
|
|
471
|
-
|
|
472
|
-
completion.value = completionAnswer;
|
|
473
|
-
|
|
474
|
-
dbg('๐ completion finished', quill.getContents());
|
|
475
|
-
|
|
476
|
-
}, props.meta.debounceTime || 300);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function removeCompletionOnBlur() {
|
|
480
|
-
if (lastText?.trim().length === 0) {
|
|
481
|
-
completion.value = null;
|
|
482
|
-
const d = quill.getContents();
|
|
483
|
-
const i = d.ops.findIndex((op: any) => op.insert.complete);
|
|
484
|
-
if (i !== -1) {
|
|
485
|
-
d.ops.splice(i, 1);
|
|
486
|
-
quill.setContents(d, 'silent');
|
|
487
|
-
dbg('๐งน Cleaned completion from ops to make ph visible');
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
</script>
|
|
493
|
-
|
|
494
|
-
<style lang="scss">
|
|
495
|
-
|
|
496
|
-
.af-quill-editor {
|
|
497
|
-
|
|
498
|
-
.ql-toolbar.ql-snow[class] {
|
|
499
|
-
border: none;
|
|
500
|
-
padding: 0 0 1rem 0;
|
|
501
|
-
.ql-picker-label{
|
|
502
|
-
padding-left: 0;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
.ql-container {
|
|
507
|
-
border: 0;
|
|
508
|
-
.ql-editor {
|
|
509
|
-
position: relative;
|
|
510
|
-
padding: 0;
|
|
511
|
-
min-height: 100px;
|
|
512
|
-
&.ql-blank::before {
|
|
513
|
-
left: 0px;
|
|
514
|
-
font-style: normal;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// .ql-editor:not(:focus) [completer] {
|
|
520
|
-
// display: none;
|
|
521
|
-
// }
|
|
522
|
-
|
|
523
|
-
.ql-editor [completer] {
|
|
524
|
-
color: gray;
|
|
525
|
-
font-style: italic;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
.ql-editor p {
|
|
529
|
-
margin-bottom: 0.5rem;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
.ql-snow .ql-stroke {
|
|
533
|
-
@apply dark:stroke-darkPrimary;
|
|
534
|
-
@apply stroke-lightPrimary;
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
.ql-snow button:hover .ql-stroke,
|
|
538
|
-
.ql-snow [role="button"]:hover .ql-stroke {
|
|
539
|
-
@apply dark:stroke-darkPrimary;
|
|
540
|
-
@apply stroke-lightPrimary;
|
|
541
|
-
filter: brightness(1.3);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
.ql-snow .ql-fill {
|
|
545
|
-
@apply dark:fill-darkPrimary;
|
|
546
|
-
@apply fill-lightPrimary;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
.ql-snow button:hover .ql-fill {
|
|
550
|
-
@apply dark:fill-darkPrimary;
|
|
551
|
-
@apply fill-lightPrimary;
|
|
552
|
-
filter: brightness(1.3);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
</style>
|