@kubex/zinc 1.1.40 → 1.1.42

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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.40",
3
+ "version": "1.1.42",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -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 _getQuillKeyboardBindings() {
389
- const empty = (value: string) => {
390
- const match = value.match(/[^<pbr\s>/]/);
391
- return match === null;
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 && !empty(this.value);
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 { expect, fixture, html } from '@open-wc/testing';
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 &nbsp;
53
+ expect(sessionStorage.getItem(KEY)).to.contain('Typed&nbsp;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&nbsp;reply');
66
+
67
+ form.requestSubmit();
68
+ expect(sessionStorage.getItem(KEY)).to.equal(null);
69
+ });
70
+ });
10
71
  });
@@ -96,8 +96,9 @@ export default class DialogComponent extends ZincElement {
96
96
  context-data=${JSON.stringify({'editor-id': this._editorId})}>
97
97
  <zn-button
98
98
  class="editor-dialog__close"
99
- icon="close"
100
- icon-size="24"
99
+ icon="x@lu"
100
+ icon-button="small"
101
+ icon-size="20"
101
102
  type="button"
102
103
  color="transparent"
103
104
  @click="${() => this.dialogEl.close()}"
@@ -32,7 +32,7 @@
32
32
  position: absolute;
33
33
  top: 0;
34
34
  right: 0;
35
- margin: var(--zn-spacing-small);
35
+ margin: var(--zn-spacing-x-small);
36
36
  z-index: 10001;
37
37
  }
38
38