@kubex/zinc 1.1.40 → 1.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-elements.json +163 -74
- package/dist/vscode.html-custom-data.json +27 -12
- package/dist/web-types.json +53 -23
- package/dist/zn.d.ts +16 -0
- package/dist/zn.min.js +93 -93
- package/docs/pages/components/editor.md +17 -0
- package/package.json +1 -1
- package/src/components/editor/editor.component.ts +51 -6
- package/src/components/editor/editor.test.ts +62 -1
|
@@ -119,6 +119,23 @@ In ticket mode (default), Enter creates new lines. Forms must be submitted using
|
|
|
119
119
|
</script>
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### Caching Unsent Content
|
|
123
|
+
|
|
124
|
+
Set a `store-key` to cache what the agent has typed, so an unsent reply survives page reloads and navigation. Content is saved as it is typed, restored when an editor with the same key next loads, and cleared when the form is submitted or the editor is emptied.
|
|
125
|
+
|
|
126
|
+
Use a key unique to the conversation — for example the ticket or chat ID. Add the `local-storage` attribute to persist across tabs and browser restarts (otherwise `sessionStorage` is used, scoped to the current tab). Cached content expires after a day by default; tune with `store-ttl` (seconds).
|
|
127
|
+
|
|
128
|
+
```html:preview
|
|
129
|
+
<zn-editor
|
|
130
|
+
name="reply"
|
|
131
|
+
interaction-type="ticket"
|
|
132
|
+
store-key="ticket-12345"
|
|
133
|
+
local-storage
|
|
134
|
+
style="height: 250px">
|
|
135
|
+
</zn-editor>
|
|
136
|
+
<p><small>Type something, then reload the page — your draft is restored.</small></p>
|
|
137
|
+
```
|
|
138
|
+
|
|
122
139
|
### Enabling Features
|
|
123
140
|
|
|
124
141
|
The editor has many optional features that can be enabled with boolean attributes.
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import {deepQuerySelectorAll} from "../../utilities/query";
|
|
|
3
3
|
import {FormControlController} from '../../internal/form';
|
|
4
4
|
import {on} from "../../utilities/on";
|
|
5
5
|
import {property, query} from 'lit/decorators.js';
|
|
6
|
+
import {Store} from "../../internal/storage";
|
|
6
7
|
import Attachment from "./modules/attachment/attachment";
|
|
7
8
|
import ContextMenu from "./modules/context-menu/context-menu";
|
|
8
9
|
import DatePicker from "./modules/date-picker/date-picker";
|
|
@@ -90,6 +91,21 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
|
|
|
90
91
|
@property({attribute: 'ai', type: Boolean}) aiEnabled: boolean = false;
|
|
91
92
|
@property({attribute: 'ai-path'}) aiPath: string = '';
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Caches unsent content while typing and restores it when the editor next loads,
|
|
96
|
+
* so agent replies in tickets/chats survive reloads and navigation. Use a key
|
|
97
|
+
* unique to the conversation (e.g. the ticket ID). The cache is cleared on submit.
|
|
98
|
+
*/
|
|
99
|
+
@property({attribute: 'store-key', reflect: true}) storeKey: string = "";
|
|
100
|
+
|
|
101
|
+
/** Cached-content expiry in seconds. Defaults to 1 day. */
|
|
102
|
+
@property({attribute: 'store-ttl', type: Number, reflect: true}) storeTtl = 86400;
|
|
103
|
+
|
|
104
|
+
/** Cache to localStorage instead of sessionStorage, persisting across tabs and browser restarts. */
|
|
105
|
+
@property({attribute: 'local-storage', type: Boolean, reflect: true}) localStorage: boolean;
|
|
106
|
+
|
|
107
|
+
protected _store: Store;
|
|
108
|
+
|
|
93
109
|
private quillElement: Quill;
|
|
94
110
|
private _content: string = '';
|
|
95
111
|
private _selectionRange: Range = {index: 0, length: 0};
|
|
@@ -119,6 +135,11 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
|
|
|
119
135
|
this.formControlController.updateValidity();
|
|
120
136
|
}
|
|
121
137
|
|
|
138
|
+
connectedCallback() {
|
|
139
|
+
super.connectedCallback();
|
|
140
|
+
this._store = new Store(this.localStorage ? window.localStorage : window.sessionStorage, "zned:", this.storeTtl);
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
protected firstUpdated(_changedProperties: PropertyValues) {
|
|
123
144
|
this.formControlController.updateValidity();
|
|
124
145
|
|
|
@@ -261,6 +282,7 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
|
|
|
261
282
|
this.quillElement = quill;
|
|
262
283
|
|
|
263
284
|
this.getForm()?.addEventListener('submit', () => {
|
|
285
|
+
this._clearCachedContent();
|
|
264
286
|
setTimeout(() => {
|
|
265
287
|
const attachmentModule = this.quillElement.getModule('attachment') as Attachment;
|
|
266
288
|
attachmentModule?.reset();
|
|
@@ -372,6 +394,14 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
|
|
|
372
394
|
this._selectionRange = range ?? oldRange;
|
|
373
395
|
});
|
|
374
396
|
|
|
397
|
+
if (this.storeKey && this._isEmpty(this.value ?? '')) {
|
|
398
|
+
const cached = this._store.get(this.storeKey);
|
|
399
|
+
if (cached) {
|
|
400
|
+
this.value = cached;
|
|
401
|
+
this.editorHtml.value = cached;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
375
405
|
const delta = quill.clipboard.convert({html: this.value});
|
|
376
406
|
quill.setContents(delta, Quill.sources.SILENT);
|
|
377
407
|
|
|
@@ -382,15 +412,30 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
|
|
|
382
412
|
private _handleTextChange() {
|
|
383
413
|
this.value = this.quillElement.getSemanticHTML();
|
|
384
414
|
this.editorHtml.value = this.value;
|
|
415
|
+
this._cacheContent();
|
|
385
416
|
this.emit('zn-change');
|
|
386
417
|
}
|
|
387
418
|
|
|
388
|
-
private
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
419
|
+
private _isEmpty(value: string) {
|
|
420
|
+
return value.match(/[^<pbr\s>/]/) === null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private _cacheContent() {
|
|
424
|
+
if (!this.storeKey || !this._store) return;
|
|
425
|
+
if (this._isEmpty(this.value)) {
|
|
426
|
+
this._store.remove(this.storeKey);
|
|
427
|
+
} else {
|
|
428
|
+
this._store.set(this.storeKey, this.value);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
393
431
|
|
|
432
|
+
private _clearCachedContent() {
|
|
433
|
+
if (this.storeKey && this._store) {
|
|
434
|
+
this._store.remove(this.storeKey);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private _getQuillKeyboardBindings() {
|
|
394
439
|
// Always add an Enter binding to support emoji selection in all interaction types
|
|
395
440
|
return {
|
|
396
441
|
'enter': {
|
|
@@ -406,7 +451,7 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
|
|
|
406
451
|
|
|
407
452
|
if (this.interactionType === 'chat') {
|
|
408
453
|
const form = this.closest('form');
|
|
409
|
-
const hasText = !!this.value && this.value.trim().length > 0 && !
|
|
454
|
+
const hasText = !!this.value && this.value.trim().length > 0 && !this._isEmpty(this.value);
|
|
410
455
|
const attachmentInput = form?.querySelector('input[name="attachments"]') as HTMLInputElement | null;
|
|
411
456
|
const hasAttachments = !!attachmentInput?.value && attachmentInput.value !== '[]';
|
|
412
457
|
if (form && (hasText || hasAttachments)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import {
|
|
2
|
+
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
|
+
import type ZnEditor from './editor.component';
|
|
3
4
|
|
|
4
5
|
describe('<zn-editor>', () => {
|
|
5
6
|
it('should render a component', async () => {
|
|
@@ -7,4 +8,64 @@ describe('<zn-editor>', () => {
|
|
|
7
8
|
|
|
8
9
|
expect(el).to.exist;
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
describe('content caching', () => {
|
|
13
|
+
const KEY = 'zned:test-draft';
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
sessionStorage.removeItem(KEY);
|
|
17
|
+
localStorage.removeItem(KEY);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should restore cached content into an empty editor', async () => {
|
|
21
|
+
sessionStorage.setItem(KEY, '0,<p>Hello draft</p>');
|
|
22
|
+
const el = await fixture<ZnEditor>(html`
|
|
23
|
+
<zn-editor store-key="test-draft"></zn-editor>`);
|
|
24
|
+
|
|
25
|
+
expect(el.value).to.contain('Hello draft');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should not overwrite an initial value with cached content', async () => {
|
|
29
|
+
sessionStorage.setItem(KEY, '0,<p>Hello draft</p>');
|
|
30
|
+
const el = await fixture<ZnEditor>(html`
|
|
31
|
+
<zn-editor store-key="test-draft" .value=${'<p>Existing</p>'}></zn-editor>`);
|
|
32
|
+
|
|
33
|
+
expect(el.value).to.contain('Existing');
|
|
34
|
+
expect(el.value).to.not.contain('Hello draft');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should read from localStorage when local-storage is set', async () => {
|
|
38
|
+
localStorage.setItem(KEY, '0,<p>Local draft</p>');
|
|
39
|
+
const el = await fixture<ZnEditor>(html`
|
|
40
|
+
<zn-editor store-key="test-draft" local-storage></zn-editor>`);
|
|
41
|
+
|
|
42
|
+
expect(el.value).to.contain('Local draft');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should cache content when the editor changes', async () => {
|
|
46
|
+
const el = await fixture<ZnEditor>(html`
|
|
47
|
+
<zn-editor store-key="test-draft" .value=${'<p>Typed reply</p>'}></zn-editor>`);
|
|
48
|
+
|
|
49
|
+
document.dispatchEvent(new Event('zn-editor-update'));
|
|
50
|
+
await el.updateComplete;
|
|
51
|
+
|
|
52
|
+
// Quill's getSemanticHTML encodes spaces as
|
|
53
|
+
expect(sessionStorage.getItem(KEY)).to.contain('Typed reply');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should clear cached content on form submit', async () => {
|
|
57
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
58
|
+
<form @submit=${(e: Event) => e.preventDefault()}>
|
|
59
|
+
<zn-editor store-key="test-draft" .value=${'<p>Typed reply</p>'}></zn-editor>
|
|
60
|
+
</form>`);
|
|
61
|
+
const el = form.querySelector<ZnEditor>('zn-editor')!;
|
|
62
|
+
|
|
63
|
+
document.dispatchEvent(new Event('zn-editor-update'));
|
|
64
|
+
await el.updateComplete;
|
|
65
|
+
expect(sessionStorage.getItem(KEY)).to.contain('Typed reply');
|
|
66
|
+
|
|
67
|
+
form.requestSubmit();
|
|
68
|
+
expect(sessionStorage.getItem(KEY)).to.equal(null);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
10
71
|
});
|