@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.
@@ -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
@@ -0,0 +1,5 @@
1
+
2
+ # Changelog
3
+
4
+ The complete changelog is available on the [GitHub Releases page](https://github.com/devforth/adminforth-rich-editor/releases).
5
+
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
@@ -0,0 +1,4 @@
1
+
2
+ > @adminforth/rich-editor@1.0.18 build
3
+ > tsc
4
+
@@ -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
- window.adminforth.alert({
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
- window.adminforth.alert({
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, '&lt;').replace(/>/g, '&gt;')
@@ -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((_d) => __awaiter(this, [_d], void 0, function* ({ record, adminUser }) {
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((_e) => __awaiter(this, [_e], void 0, function* ({ recordId, record, adminUser }) {
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((_f) => __awaiter(this, [_f], void 0, function* ({ record, adminUser }) {
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, AfterSaveFunction } from "adminforth";
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 as Array<AfterSaveFunction>).push(async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {
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 as Array<AfterSaveFunction>).push(
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 as Array<AfterSaveFunction>).push(
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.17",
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
- "rollout": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch && npm publish --access public",
10
- "prepare": "npm link adminforth"
9
+ "prepare": "npm link adminforth",
10
+ "build": "tsc"
11
11
  },
12
- "keywords": [],
13
- "author": "",
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,11 +0,0 @@
1
- ## [1.0.17] - 2024-12-06
2
-
3
- ### Added
4
-
5
- - Reworked completion for adapter
6
-
7
- ## [1.0.12] - 2021-10-07
8
-
9
- ### Added
10
-
11
- - Rate limiting for openai requests support
@@ -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
- }
@@ -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, '&lt;').replace(/>/g, '&gt;')
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>