@adminforth/rich-editor 1.0.5 → 1.0.7

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.
@@ -32,10 +32,11 @@ function dbg(title: string,...args: any[]) {
32
32
  console.log(title, ...args.map(a =>JSON.stringify(a, null, 1)));
33
33
  }
34
34
 
35
- const BlockEmbed = Quill.import('blots/block/embed');
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');
36
37
 
37
38
  // @ts-ignore
38
- class CompleteBlot extends BlockEmbed {
39
+ class CompleteBlot extends Embed {
39
40
  static blotName = 'complete';
40
41
  static tagName = 'span';
41
42
 
@@ -88,12 +89,13 @@ onMounted(() => {
88
89
  modules: {
89
90
  toolbar: props.meta.toolbar || [
90
91
  ['bold', 'italic', 'underline', 'strike'], // toggled buttons
91
- ['blockquote', 'code-block', 'link'],
92
+ ['blockquote', 'code-block', 'link', ...props.meta.uploadPluginInstanceId ? ['image'] : []],
92
93
  // [
93
94
  // // 'image',
94
95
  // // 'video',
95
96
  // // 'formula'
96
97
  // ],
98
+
97
99
 
98
100
  [{ 'header': 2 }, { 'header': 3 }], // custom button values
99
101
  [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
@@ -278,6 +280,8 @@ function approveCompletion(type: 'all' | 'word') {
278
280
 
279
281
  let shouldComplete = false;
280
282
  if (type === 'all') {
283
+ dbg(`👇 insert all at ${cursorPosition.index}, ${completion.value.join('')}`);
284
+ deleteCompleteEmbed();
281
285
  quill.insertText(cursorPosition.index, completion.value.join(''), 'silent');
282
286
  shouldComplete = true;
283
287
  } else {
@@ -306,7 +310,6 @@ async function startCompletion() {
306
310
  return;
307
311
  }
308
312
  completion.value = null;
309
- // return;
310
313
  deleteCompleteEmbed();
311
314
 
312
315
  if (tmt) {
@@ -336,12 +339,10 @@ async function startCompletion() {
336
339
  return;
337
340
  }
338
341
 
339
- // deleteCompleteEmbed();
340
- // insert on +1 to insert after \n
341
- quill.insertEmbed(cursorPosition.index + 1, 'complete', { text: completionAnswer.join('') }, 'silent');
342
+ quill.insertEmbed(cursorPosition.index, 'complete', { text: completionAnswer.join('') }, 'silent');
342
343
 
343
- dbg('👇 set pos', cursorPosition.index, cursorPosition.length)
344
- quill.setSelection(cursorPosition.index, cursorPosition.length, 'silent');
344
+ // dbg('👇 set pos', cursorPosition.index, cursorPosition.length)
345
+ // quill.setSelection(cursorPosition.index, cursorPosition.length, 'silent');
345
346
 
346
347
  completion.value = completionAnswer;
347
348
 
@@ -372,7 +373,6 @@ function removeCompletionOnBlur() {
372
373
  .ql-toolbar.ql-snow[class] {
373
374
  border: none;
374
375
  padding: 0 0 1rem 0;
375
-
376
376
  .ql-picker-label{
377
377
  padding-left: 0;
378
378
  }
@@ -380,12 +380,10 @@ function removeCompletionOnBlur() {
380
380
 
381
381
  .ql-container {
382
382
  border: 0;
383
-
384
383
  .ql-editor {
385
384
  position: relative;
386
385
  padding: 0;
387
386
  min-height: 100px;
388
-
389
387
  &.ql-blank::before {
390
388
  left: 0px;
391
389
  font-style: normal;
@@ -393,29 +391,36 @@ function removeCompletionOnBlur() {
393
391
  }
394
392
  }
395
393
 
396
- p:has(+ [completer]) br {
394
+ .ql-editor:not(:focus) [completer] {
397
395
  display: none;
398
396
  }
399
- p:has(+ [completer]) {
400
- // background: rgb(255 227 227); // debug
401
- display: inline;
402
- }
403
-
404
- .ql-editor:not(:focus) [completer] {
405
- display: none;
406
- }
407
397
 
408
398
  .ql-editor [completer] {
409
- // important to keep pointer-events non none for cursor position on completer click
410
-
411
- // text is not selectable
412
- user-select: none;
413
399
  color: gray;
400
+ font-style: italic;
401
+ }
414
402
 
415
- // if inline or inline used then user-select: none brakes triple click
416
- display: contents;
403
+ .ql-snow .ql-stroke {
404
+ @apply dark:stroke-darkPrimary;
405
+ @apply stroke-lightPrimary;
417
406
 
418
- font-style: italic;
407
+ }
408
+ .ql-snow button:hover .ql-stroke,
409
+ .ql-snow [role="button"]:hover .ql-stroke {
410
+ @apply dark:stroke-darkPrimary;
411
+ @apply stroke-lightPrimary;
412
+ filter: brightness(1.3);
413
+ }
414
+
415
+ .ql-snow .ql-fill {
416
+ @apply dark:fill-darkPrimary;
417
+ @apply fill-lightPrimary;
418
+ }
419
+
420
+ .ql-snow button:hover .ql-fill {
421
+ @apply dark:fill-darkPrimary;
422
+ @apply fill-lightPrimary;
423
+ filter: brightness(1.3);
419
424
  }
420
425
 
421
426
  }
@@ -32,10 +32,11 @@ function dbg(title: string,...args: any[]) {
32
32
  console.log(title, ...args.map(a =>JSON.stringify(a, null, 1)));
33
33
  }
34
34
 
35
- const BlockEmbed = Quill.import('blots/block/embed');
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');
36
37
 
37
38
  // @ts-ignore
38
- class CompleteBlot extends BlockEmbed {
39
+ class CompleteBlot extends Embed {
39
40
  static blotName = 'complete';
40
41
  static tagName = 'span';
41
42
 
@@ -88,12 +89,13 @@ onMounted(() => {
88
89
  modules: {
89
90
  toolbar: props.meta.toolbar || [
90
91
  ['bold', 'italic', 'underline', 'strike'], // toggled buttons
91
- ['blockquote', 'code-block', 'link'],
92
+ ['blockquote', 'code-block', 'link', ...props.meta.uploadPluginInstanceId ? ['image'] : []],
92
93
  // [
93
94
  // // 'image',
94
95
  // // 'video',
95
96
  // // 'formula'
96
97
  // ],
98
+
97
99
 
98
100
  [{ 'header': 2 }, { 'header': 3 }], // custom button values
99
101
  [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
@@ -278,6 +280,8 @@ function approveCompletion(type: 'all' | 'word') {
278
280
 
279
281
  let shouldComplete = false;
280
282
  if (type === 'all') {
283
+ dbg(`👇 insert all at ${cursorPosition.index}, ${completion.value.join('')}`);
284
+ deleteCompleteEmbed();
281
285
  quill.insertText(cursorPosition.index, completion.value.join(''), 'silent');
282
286
  shouldComplete = true;
283
287
  } else {
@@ -306,7 +310,6 @@ async function startCompletion() {
306
310
  return;
307
311
  }
308
312
  completion.value = null;
309
- // return;
310
313
  deleteCompleteEmbed();
311
314
 
312
315
  if (tmt) {
@@ -336,12 +339,10 @@ async function startCompletion() {
336
339
  return;
337
340
  }
338
341
 
339
- // deleteCompleteEmbed();
340
- // insert on +1 to insert after \n
341
- quill.insertEmbed(cursorPosition.index + 1, 'complete', { text: completionAnswer.join('') }, 'silent');
342
+ quill.insertEmbed(cursorPosition.index, 'complete', { text: completionAnswer.join('') }, 'silent');
342
343
 
343
- dbg('👇 set pos', cursorPosition.index, cursorPosition.length)
344
- quill.setSelection(cursorPosition.index, cursorPosition.length, 'silent');
344
+ // dbg('👇 set pos', cursorPosition.index, cursorPosition.length)
345
+ // quill.setSelection(cursorPosition.index, cursorPosition.length, 'silent');
345
346
 
346
347
  completion.value = completionAnswer;
347
348
 
@@ -372,7 +373,6 @@ function removeCompletionOnBlur() {
372
373
  .ql-toolbar.ql-snow[class] {
373
374
  border: none;
374
375
  padding: 0 0 1rem 0;
375
-
376
376
  .ql-picker-label{
377
377
  padding-left: 0;
378
378
  }
@@ -380,12 +380,10 @@ function removeCompletionOnBlur() {
380
380
 
381
381
  .ql-container {
382
382
  border: 0;
383
-
384
383
  .ql-editor {
385
384
  position: relative;
386
385
  padding: 0;
387
386
  min-height: 100px;
388
-
389
387
  &.ql-blank::before {
390
388
  left: 0px;
391
389
  font-style: normal;
@@ -393,29 +391,36 @@ function removeCompletionOnBlur() {
393
391
  }
394
392
  }
395
393
 
396
- p:has(+ [completer]) br {
394
+ .ql-editor:not(:focus) [completer] {
397
395
  display: none;
398
396
  }
399
- p:has(+ [completer]) {
400
- // background: rgb(255 227 227); // debug
401
- display: inline;
402
- }
403
-
404
- .ql-editor:not(:focus) [completer] {
405
- display: none;
406
- }
407
397
 
408
398
  .ql-editor [completer] {
409
- // important to keep pointer-events non none for cursor position on completer click
410
-
411
- // text is not selectable
412
- user-select: none;
413
399
  color: gray;
400
+ font-style: italic;
401
+ }
414
402
 
415
- // if inline or inline used then user-select: none brakes triple click
416
- display: contents;
403
+ .ql-snow .ql-stroke {
404
+ @apply dark:stroke-darkPrimary;
405
+ @apply stroke-lightPrimary;
417
406
 
418
- font-style: italic;
407
+ }
408
+ .ql-snow button:hover .ql-stroke,
409
+ .ql-snow [role="button"]:hover .ql-stroke {
410
+ @apply dark:stroke-darkPrimary;
411
+ @apply stroke-lightPrimary;
412
+ filter: brightness(1.3);
413
+ }
414
+
415
+ .ql-snow .ql-fill {
416
+ @apply dark:fill-darkPrimary;
417
+ @apply fill-lightPrimary;
418
+ }
419
+
420
+ .ql-snow button:hover .ql-fill {
421
+ @apply dark:fill-darkPrimary;
422
+ @apply fill-lightPrimary;
423
+ filter: brightness(1.3);
419
424
  }
420
425
 
421
426
  }
package/dist/index.js CHANGED
@@ -8,10 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { AdminForthPlugin } from "adminforth";
11
+ // options:
12
+ // attachments: {
13
+ // attachmentResource: 'description_images',
14
+ // attachmentFieldName: 'image_path',
15
+ // attachmentRecordIdFieldName: 'record_id',
16
+ // attachmentResourceIdFieldName: 'resource_id',
17
+ // },
11
18
  export default class RichEditorPlugin extends AdminForthPlugin {
12
19
  constructor(options) {
13
20
  super(options, import.meta.url);
14
- this.resourceConfig = undefined;
21
+ this.activationOrder = 100000;
15
22
  this.options = options;
16
23
  }
17
24
  modifyResourceConfig(adminforth, resourceConfig) {
@@ -19,19 +26,35 @@ export default class RichEditorPlugin extends AdminForthPlugin {
19
26
  modifyResourceConfig: { get: () => super.modifyResourceConfig }
20
27
  });
21
28
  return __awaiter(this, void 0, void 0, function* () {
22
- var _a, _b;
29
+ var _a, _b, _c;
23
30
  _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
24
- this.resourceConfig = resourceConfig;
25
31
  const c = resourceConfig.columns.find(c => c.name === this.options.htmlFieldName);
26
32
  if (!c) {
27
33
  throw new Error(`Column ${this.options.htmlFieldName} not found in resource ${resourceConfig.label}`);
28
34
  }
35
+ if (this.options.attachments) {
36
+ const resource = adminforth.config.resources.find(r => r.resourceId === this.options.attachments.attachmentResource);
37
+ if (!resource) {
38
+ throw new Error(`Resource '${this.options.attachments.attachmentResource}' not found`);
39
+ }
40
+ const field = resource.columns.find(c => c.name === this.options.attachments.attachmentFieldName);
41
+ if (!field) {
42
+ throw new Error(`Field '${this.options.attachments.attachmentFieldName}' not found in resource '${this.options.attachments.attachmentResource}'`);
43
+ }
44
+ const plugin = adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.options.attachments.attachmentResource &&
45
+ p.pluginOptions.pathColumnName === this.options.attachments.attachmentFieldName);
46
+ if (!plugin) {
47
+ throw new Error(`Plugin for attachment field '${this.options.attachments.attachmentFieldName}' not found in resource '${this.options.attachments.attachmentResource}', please check if Upload Plugin is installed on the field ${this.options.attachments.attachmentFieldName}`);
48
+ }
49
+ this.uploadPlugin = plugin;
50
+ }
29
51
  const filed = {
30
52
  file: this.componentPath('quillEditor.vue'),
31
53
  meta: {
32
54
  pluginInstanceId: this.pluginInstanceId,
33
55
  debounceTime: ((_b = (_a = this.options.completion) === null || _a === void 0 ? void 0 : _a.expert) === null || _b === void 0 ? void 0 : _b.debounceTime) || 300,
34
56
  shouldComplete: !!this.options.completion,
57
+ uploadPluginInstanceId: (_c = this.uploadPlugin) === null || _c === void 0 ? void 0 : _c.pluginInstanceId,
35
58
  }
36
59
  };
37
60
  if (!c.components) {
package/index.ts CHANGED
@@ -2,10 +2,19 @@
2
2
  import { IAdminForth, IHttpServer, AdminForthPlugin, AdminForthResource } from "adminforth";
3
3
  import { PluginOptions } from './types.js';
4
4
 
5
+ // options:
6
+ // attachments: {
7
+ // attachmentResource: 'description_images',
8
+ // attachmentFieldName: 'image_path',
9
+ // attachmentRecordIdFieldName: 'record_id',
10
+ // attachmentResourceIdFieldName: 'resource_id',
11
+ // },
5
12
 
6
13
  export default class RichEditorPlugin extends AdminForthPlugin {
7
14
  options: PluginOptions;
8
- resourceConfig?: AdminForthResource = undefined;
15
+ uploadPlugin: AdminForthPlugin;
16
+
17
+ activationOrder: number = 100000;
9
18
 
10
19
  constructor(options: PluginOptions) {
11
20
  super(options, import.meta.url);
@@ -14,27 +23,46 @@ export default class RichEditorPlugin extends AdminForthPlugin {
14
23
 
15
24
  async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
16
25
  super.modifyResourceConfig(adminforth, resourceConfig);
17
- this.resourceConfig = resourceConfig;
18
-
19
26
 
20
27
  const c = resourceConfig.columns.find(c => c.name === this.options.htmlFieldName);
21
28
  if (!c) {
22
29
  throw new Error(`Column ${this.options.htmlFieldName} not found in resource ${resourceConfig.label}`);
23
30
  }
31
+
32
+ if (this.options.attachments) {
33
+ const resource = adminforth.config.resources.find(r => r.resourceId === this.options.attachments!.attachmentResource);
34
+ if (!resource) {
35
+ throw new Error(`Resource '${this.options.attachments!.attachmentResource}' not found`);
36
+ }
37
+ const field = resource.columns.find(c => c.name === this.options.attachments!.attachmentFieldName);
38
+ if (!field) {
39
+ throw new Error(`Field '${this.options.attachments!.attachmentFieldName}' not found in resource '${this.options.attachments!.attachmentResource}'`);
40
+ }
41
+ const plugin = adminforth.activatedPlugins.find(p =>
42
+ p.resourceConfig!.resourceId === this.options.attachments!.attachmentResource &&
43
+ p.pluginOptions.pathColumnName === this.options.attachments!.attachmentFieldName);
44
+ if (!plugin) {
45
+ throw new Error(`Plugin for attachment field '${this.options.attachments!.attachmentFieldName}' not found in resource '${this.options.attachments!.attachmentResource}', please check if Upload Plugin is installed on the field ${this.options.attachments!.attachmentFieldName}`);
46
+ }
47
+ this.uploadPlugin = plugin;
48
+ }
49
+
24
50
  const filed = {
25
51
  file: this.componentPath('quillEditor.vue'),
26
52
  meta: {
27
53
  pluginInstanceId: this.pluginInstanceId,
28
54
  debounceTime: this.options.completion?.expert?.debounceTime || 300,
29
55
  shouldComplete: !!this.options.completion,
56
+ uploadPluginInstanceId: this.uploadPlugin?.pluginInstanceId,
30
57
  }
31
58
  }
59
+
32
60
  if (!c.components) {
33
61
  c.components = {};
34
62
  }
35
63
  c.components.create = filed;
36
64
  c.components.edit = filed;
37
-
65
+
38
66
  }
39
67
 
40
68
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
@@ -42,6 +70,8 @@ export default class RichEditorPlugin extends AdminForthPlugin {
42
70
  if (this.options.completion && this.options.completion.provider !== 'openai-chat-gpt') {
43
71
  throw new Error(`Invalid provider ${this.options.completion.provider}`);
44
72
  }
73
+
74
+
45
75
  }
46
76
 
47
77
  instanceUniqueRepresentation(pluginOptions: any) : string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/rich-editor",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/types.ts CHANGED
@@ -134,6 +134,35 @@ export interface PluginOptions {
134
134
 
135
135
  }
136
136
 
137
-
137
+ /**
138
+ * Allows to attach images to the HTML text
139
+ * Requires to have a separate resource with Upload Plugin installed on attachment field.
140
+ * Each attachment used in HTML will create one record in the attachment resource.
141
+ */
142
+ attachments?: {
143
+ /**
144
+ * Resource name where images are stored. Should point to the existing resource.
145
+ */
146
+ attachmentResource: string;
147
+
148
+ /**
149
+ * Field name in the attachment resource where image is stored. Should point to the existing field in the attachment resource.
150
+ * Also there should be upload plugin installed on this field.
151
+ */
152
+ attachmentFieldName: 'image_path',
153
+
154
+ /**
155
+ * When attachment is created, it will be linked to the record with this field name.
156
+ * For example when RichEditor installed on description field of appartment resource,
157
+ * field in attachment resource describet hear will store id of appartment record.
158
+ */
159
+ attachmentRecordIdFieldName: 'record_id',
160
+
161
+ /**
162
+ * When attachment is created, it will be linked to the resource with this field name.
163
+ * For example when RichEditor installed on description field of appartment resource, it will store id of appartment resource.
164
+ */
165
+ attachmentResourceIdFieldName: 'resource_id',
166
+ },
138
167
  }
139
168