@kubex/zinc 1.0.3 → 1.0.5

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.
Files changed (154) hide show
  1. package/.github/workflows/js_build_and_deploy.yaml +1 -1
  2. package/dist/custom-elements.json +25150 -0
  3. package/dist/vscode.html-custom-data.json +2874 -0
  4. package/dist/web-types.json +6611 -0
  5. package/dist/zn.d.ts +6664 -0
  6. package/dist/zn.min.css +1 -0
  7. package/dist/zn.min.js +4740 -0
  8. package/docs/_includes/default.njk +1 -0
  9. package/docs/assets/scripts/docs.js +4 -5
  10. package/docs/assets/scripts/search.js +0 -9
  11. package/docs/assets/styles/docs.css +3 -3
  12. package/docs/data/data-table.json +79 -0
  13. package/docs/data/products-table.json +36 -0
  14. package/docs/eleventy.config.cjs +3 -0
  15. package/docs/pages/components/data-select.md +10 -0
  16. package/docs/pages/components/data-table.md +84 -45
  17. package/docs/pages/components/dialog.md +1 -1
  18. package/docs/pages/components/editor.md +1 -3
  19. package/docs/pages/components/filter-wrapper.md +20 -0
  20. package/docs/pages/components/icon.md +2 -0
  21. package/docs/pages/components/item.md +2 -2
  22. package/docs/pages/components/menu-item.md +12 -3
  23. package/docs/pages/components/panel.md +17 -0
  24. package/docs/pages/components/progress-tile.md +17 -1
  25. package/docs/pages/components/settings-container.md +137 -0
  26. package/docs/pages/components/textarea.md +1 -1
  27. package/package.json +3 -1
  28. package/scss/_root.scss +26 -0
  29. package/scss/boot.scss +27 -1
  30. package/scss/mixins/index.scss +30 -1
  31. package/scss/shared/_base.scss +1 -1
  32. package/scss/shared/_button.scss +1 -1
  33. package/scss/shared/layout.scss +7 -0
  34. package/scss/themes/_dark.scss +5 -5
  35. package/scss/themes/_light.scss +6 -5
  36. package/scss/variables/index.scss +9 -0
  37. package/src/components/alert/alert.component.ts +1 -1
  38. package/src/components/alert/alert.scss +18 -4
  39. package/src/components/button/button.component.ts +6 -2
  40. package/src/components/button/button.scss +16 -1
  41. package/src/components/button-menu/button-menu.component.ts +23 -15
  42. package/src/components/button-menu/button-menu.scss +4 -4
  43. package/src/components/checkbox/checkbox.component.ts +5 -1
  44. package/src/components/checkbox/checkbox.scss +1 -1
  45. package/src/components/chip/chip.component.ts +7 -3
  46. package/src/components/chip/chip.scss +9 -1
  47. package/src/components/collapsible/collapsible.component.ts +48 -2
  48. package/src/components/confirm/confirm.component.ts +1 -2
  49. package/src/components/content-block/content-block.component.ts +25 -1
  50. package/src/components/content-block/content-block.scss +8 -0
  51. package/src/components/data-select/data-select.component.ts +1 -1
  52. package/src/components/data-table/data-table.component.ts +361 -422
  53. package/src/components/data-table/data-table.scss +6 -1
  54. package/src/components/datepicker/datepicker.scss +1 -1
  55. package/src/components/dialog/dialog.component.ts +52 -33
  56. package/src/components/dialog/dialog.scss +15 -1
  57. package/src/components/dropdown/dropdown.component.ts +1 -1
  58. package/src/components/editor/editor.component.ts +196 -158
  59. package/src/components/editor/editor.scss +25 -5
  60. package/src/components/editor/modules/ai/index.ts +281 -0
  61. package/src/components/editor/modules/ai/panel/ai-panel.component.ts +102 -0
  62. package/src/components/editor/modules/ai/panel/ai-panel.scss +83 -0
  63. package/src/components/editor/modules/ai/tooltip/ai-tooltip.component.ts +30 -0
  64. package/src/components/editor/modules/ai/tooltip/ai-tooltip.scss +28 -0
  65. package/src/components/editor/modules/{attachment-module.ts → attachment/attachment.ts} +7 -7
  66. package/src/components/editor/modules/context-menu/context-menu-component.ts +115 -0
  67. package/src/components/editor/modules/context-menu/context-menu.scss +49 -0
  68. package/src/components/editor/modules/context-menu/context-menu.ts +296 -0
  69. package/src/components/editor/modules/context-menu/quick-action/index.ts +12 -0
  70. package/src/components/editor/modules/context-menu/quick-action/quick-action.component.ts +18 -0
  71. package/src/components/editor/modules/date-picker/date-picker.ts +77 -0
  72. package/src/components/editor/modules/dialog/dialog.component.ts +115 -0
  73. package/src/components/editor/modules/dialog/dialog.scss +43 -0
  74. package/src/components/editor/modules/dialog/dialog.ts +46 -0
  75. package/src/components/editor/modules/{drag-drop-module.ts → drag-drop/drag-drop.ts} +6 -6
  76. package/src/components/editor/modules/emoji/emoji.ts +106 -0
  77. package/src/components/editor/modules/emoji/headless/headless-emoji.component.ts +122 -0
  78. package/src/components/editor/modules/emoji/headless/headless-emoji.scss +52 -0
  79. package/src/components/editor/modules/emoji/headless/headless-emoji.ts +223 -0
  80. package/src/components/editor/modules/{image-resize-module/image-resize-module.ts → image-resize/image-resize.ts} +5 -5
  81. package/src/components/editor/modules/{time-tracking-module.ts → time-tracking/time-tracking.ts} +5 -5
  82. package/src/components/editor/modules/toolbar/tool/index.ts +12 -0
  83. package/src/components/editor/modules/toolbar/tool/tool.component.ts +30 -0
  84. package/src/components/editor/modules/toolbar/tool/tool.scss +5 -0
  85. package/src/components/editor/modules/toolbar/toolbar.component.ts +738 -0
  86. package/src/components/editor/modules/toolbar/toolbar.scss +98 -0
  87. package/src/components/editor/modules/toolbar/toolbar.ts +328 -0
  88. package/src/components/expanding-action/expanding-action.component.ts +2 -2
  89. package/src/components/expanding-action/expanding-action.scss +2 -2
  90. package/src/components/filter-wrapper/filter-wrapper.component.ts +91 -0
  91. package/src/components/filter-wrapper/filter-wrapper.scss +11 -0
  92. package/src/components/filter-wrapper/filter-wrapper.test.ts +10 -0
  93. package/src/components/filter-wrapper/index.ts +12 -0
  94. package/src/components/header/header.component.ts +1 -1
  95. package/src/components/header/header.scss +8 -1
  96. package/src/components/hover-container/hover-container.scss +39 -36
  97. package/src/components/icon/icon.component.ts +11 -5
  98. package/src/components/icon/icon.scss +15 -2
  99. package/src/components/inline-edit/inline-edit.component.ts +28 -3
  100. package/src/components/inline-edit/inline-edit.scss +4 -1
  101. package/src/components/input/input.scss +1 -1
  102. package/src/components/item/item.component.ts +30 -19
  103. package/src/components/item/item.scss +56 -14
  104. package/src/components/linked-select/linked-select.component.ts +5 -1
  105. package/src/components/menu/menu.component.ts +1 -0
  106. package/src/components/menu/menu.scss +1 -1
  107. package/src/components/menu-item/menu-item.component.ts +22 -4
  108. package/src/components/menu-item/menu-item.scss +54 -7
  109. package/src/components/navbar/navbar.component.ts +7 -1
  110. package/src/components/navbar/navbar.scss +26 -16
  111. package/src/components/option/option.component.ts +4 -1
  112. package/src/components/option/option.scss +9 -0
  113. package/src/components/page-nav/page-nav.component.ts +2 -2
  114. package/src/components/page-nav/page-nav.scss +4 -4
  115. package/src/components/panel/panel.component.ts +20 -9
  116. package/src/components/panel/panel.scss +5 -1
  117. package/src/components/popup/popup.component.ts +16 -2
  118. package/src/components/progress-tile/progress-tile.component.ts +10 -4
  119. package/src/components/progress-tile/progress-tile.scss +7 -4
  120. package/src/components/query-builder/query-builder.component.ts +1 -1
  121. package/src/components/radio/radio.scss +1 -1
  122. package/src/components/scroll-container/scroll-container.component.ts +56 -9
  123. package/src/components/scroll-container/scroll-container.scss +37 -2
  124. package/src/components/select/select.component.ts +3 -0
  125. package/src/components/select/select.scss +1 -1
  126. package/src/components/settings-container/index.ts +12 -0
  127. package/src/components/settings-container/settings-container.component.ts +226 -0
  128. package/src/components/settings-container/settings-container.scss +92 -0
  129. package/src/components/settings-container/settings-container.test.ts +10 -0
  130. package/src/components/skeleton/skeleton.scss +5 -4
  131. package/src/components/sp/sp.component.ts +2 -0
  132. package/src/components/sp/sp.scss +2 -0
  133. package/src/components/split-button/split-button.component.ts +24 -18
  134. package/src/components/split-pane/split-pane.component.ts +5 -5
  135. package/src/components/stat/stat.component.ts +5 -1
  136. package/src/components/stat/stat.scss +24 -1
  137. package/src/components/style/style.component.ts +18 -0
  138. package/src/components/tabs/tabs.component.ts +12 -1
  139. package/src/components/textarea/textarea.component.ts +3 -1
  140. package/src/components/textarea/textarea.scss +21 -2
  141. package/src/components/vertical-stepper/vertical-stepper.scss +1 -1
  142. package/src/internal/form.ts +3 -1
  143. package/src/internal/slot.ts +29 -3
  144. package/src/utilities/on.ts +7 -8
  145. package/src/utilities/query.ts +5 -1
  146. package/src/wc.scss +7 -0
  147. package/src/zinc.ts +5 -0
  148. package/src/components/editor/modules/dialog-module/dialog-module.component.ts +0 -289
  149. package/src/components/editor/modules/dialog-module/dialog-module.scss +0 -115
  150. package/src/components/editor/modules/dialog-module/dialog-module.ts +0 -99
  151. package/src/components/editor/modules/events/zn-show-canned-response-dialog.ts +0 -15
  152. package/src/components/editor/modules/menu-module/menu-module.component.ts +0 -149
  153. package/src/components/editor/modules/menu-module/menu-module.scss +0 -17
  154. package/src/components/editor/modules/menu-module/menu-module.ts +0 -203
@@ -0,0 +1,106 @@
1
+ import {init, Picker} from 'emoji-mart';
2
+ import data from '@emoji-mart/data';
3
+ import Quill from 'quill';
4
+ import type ToolbarComponent from "../toolbar/toolbar.component";
5
+
6
+ export interface EmojiResult {
7
+ native?: string;
8
+ skins?: { native?: string }[];
9
+ id?: string;
10
+ shortcodes?: string;
11
+ }
12
+
13
+ class Emoji {
14
+ private readonly _quill: Quill;
15
+ private _mo: MutationObserver | null = null;
16
+
17
+ constructor(quill: Quill) {
18
+ this._quill = quill;
19
+ try {
20
+ void init({data});
21
+ } catch {
22
+ // no-op if already initialised
23
+ }
24
+ this.initPicker();
25
+
26
+ const host = this.getHostEditor();
27
+ if (host) {
28
+ this._mo = new MutationObserver(() => this.initPicker());
29
+ this._mo.observe(host, {attributes: true, attributeFilter: ['t']});
30
+ }
31
+ }
32
+
33
+ private getHostEditor(): HTMLElement | null {
34
+ const root = this._quill.container?.getRootNode?.() as ShadowRoot | null;
35
+ return (root && (root as ShadowRoot).host) as HTMLElement | null;
36
+ }
37
+
38
+ private async getToolbarEmojiContainer(): Promise<HTMLElement | null> {
39
+ try {
40
+ const root = this._quill.container?.getRootNode?.() as ShadowRoot | null;
41
+ if (!root) return null;
42
+
43
+ const toolbar = root.getElementById?.('toolbar') as ToolbarComponent | null;
44
+ await toolbar?.updateComplete;
45
+
46
+ const shadow = toolbar?.shadowRoot as ShadowRoot | undefined;
47
+ const container = shadow?.querySelector?.('.emoji-picker') as HTMLElement | null;
48
+ return container ?? null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ private getTheme(): 'light' | 'dark' {
55
+ const host: any = this.getHostEditor();
56
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
57
+ const t = host?.t ?? host?.getAttribute?.('t');
58
+ return t === 'dark' ? 'dark' : 'light';
59
+ }
60
+
61
+ public initPicker() {
62
+ this.getToolbarEmojiContainer().then((container) => {
63
+ if (!container) return;
64
+
65
+ container.innerHTML = '';
66
+
67
+ // eslint-disable-next-line no-new
68
+ new Picker({
69
+ parent: container,
70
+ data: data as Record<string, unknown>,
71
+ previewPosition: 'none',
72
+ skinTonePosition: 'none',
73
+ theme: this.getTheme(),
74
+ set: 'native',
75
+ icons: 'solid',
76
+ onEmojiSelect: (emoji: EmojiResult) => this.onEmojiSelect(emoji)
77
+ });
78
+ });
79
+ }
80
+
81
+ private onEmojiSelect(emoji: EmojiResult | null) {
82
+ const text: string = (emoji?.native) ?? (emoji?.skins?.[0]?.native) ?? '';
83
+ if (!text || !this._quill) return;
84
+
85
+ const range = this._quill.getSelection(true);
86
+ if (range) {
87
+ this._quill.insertText(range.index, text, Quill.sources.USER);
88
+ this._quill.setSelection(range.index + text.length, 0, Quill.sources.USER);
89
+ } else {
90
+ const index = Math.max(0, this._quill.getLength() - 1);
91
+ this._quill.insertText(index, text, Quill.sources.USER);
92
+ this._quill.setSelection(index + text.length, 0, Quill.sources.USER);
93
+ }
94
+
95
+ this.getToolbarEmojiContainer().then((container) => {
96
+ if (!container) return;
97
+
98
+ const root = container.getRootNode() as ShadowRoot;
99
+ const host = root.host as HTMLElement | undefined;
100
+ const dialog = host?.closest?.('zn-dialog') as { hide?: () => void } | null;
101
+ dialog?.hide?.();
102
+ });
103
+ }
104
+ }
105
+
106
+ export default Emoji;
@@ -0,0 +1,122 @@
1
+ import {html, unsafeCSS} from 'lit';
2
+ import {property, state} from 'lit/decorators.js';
3
+ import ZincElement from '../../../../../internal/zinc-element';
4
+ import type {CSSResultGroup, PropertyValues} from 'lit';
5
+
6
+ import styles from './headless-emoji.scss';
7
+
8
+ export interface ResultItem {
9
+ emojiChar: string;
10
+ label: string;
11
+ }
12
+
13
+ export default class HeadlessEmojiComponent extends ZincElement {
14
+ static styles: CSSResultGroup = unsafeCSS(styles);
15
+
16
+ @property({type: Boolean, reflect: true}) open = false;
17
+ @property({type: String}) query = '';
18
+ @property({type: Array}) results: ResultItem[] = [];
19
+
20
+ @state() private _activeIndex = -1;
21
+
22
+ show() {
23
+ this.open = true;
24
+ }
25
+
26
+ hide() {
27
+ this.open = false;
28
+ this._activeIndex = -1;
29
+ }
30
+
31
+ setPosition(left: number, top: number) {
32
+ this.style.left = `${Math.max(0, left)}px`;
33
+ this.style.top = `${top}px`;
34
+ }
35
+
36
+ setActiveIndex(index: number) {
37
+ const len = this.results?.length ?? 0;
38
+ if (!len) {
39
+ this._activeIndex = -1;
40
+ return;
41
+ }
42
+
43
+ if (index < 0) {
44
+ index = len - 1;
45
+ }
46
+ if (index >= len) {
47
+ index = 0;
48
+ }
49
+ this._activeIndex = index;
50
+ this.requestUpdate();
51
+
52
+ requestAnimationFrame(() => {
53
+ const items = Array.from(this.renderRoot.querySelectorAll<HTMLButtonElement>('[data-emoji-item]'));
54
+ const active = items[this._activeIndex];
55
+ active?.scrollIntoView?.({block: 'nearest'});
56
+ });
57
+ }
58
+
59
+ getActiveIndex() {
60
+ return this._activeIndex;
61
+ }
62
+
63
+ private onMouseEnterItem = (e: MouseEvent) => {
64
+ const target = e.currentTarget as HTMLElement | null;
65
+ if (!target) return;
66
+
67
+ const idx = parseInt(target.dataset.index || '-1', 10);
68
+ if (!Number.isNaN(idx)) {
69
+ this.setActiveIndex(idx);
70
+ }
71
+ }
72
+
73
+ private onClickItem = (e: MouseEvent) => {
74
+ const target = e.currentTarget as HTMLElement | null;
75
+ if (!target) return;
76
+
77
+ const idx = parseInt(target.dataset.index || '-1', 10);
78
+ if (Number.isNaN(idx)) return;
79
+
80
+ const item = this.results?.[idx];
81
+ if (!item) return;
82
+
83
+ this.dispatchEvent(new CustomEvent('zn-emoji-select', {
84
+ bubbles: true,
85
+ composed: true,
86
+ detail: {emojiChar: item.emojiChar, label: item.label}
87
+ }));
88
+ }
89
+
90
+ protected willUpdate(changed: PropertyValues) {
91
+ if (changed.has('results')) {
92
+ // Reset active index when results change
93
+ this._activeIndex = this.results?.length ? 0 : -1;
94
+ }
95
+ }
96
+
97
+ render() {
98
+ return html`
99
+ <div class="header">${this.query ? `Emoji: ${this.query}` : 'Emoji'}</div>
100
+ ${Array.isArray(this.results) && this.results.length > 0 ? (
101
+ this.results.slice(0, 20).map((res, i) => html`
102
+ <button
103
+ type="button"
104
+ class="item"
105
+ role="option"
106
+ aria-selected="${String(i === this._activeIndex)}"
107
+ data-emoji-item
108
+ data-index="${i}"
109
+ @mouseenter="${this.onMouseEnterItem}"
110
+ @click="${this.onClickItem}"
111
+ >
112
+ <span class="emoji">${res.emojiChar}</span>
113
+ <span class="label">${res.label}</span>
114
+ </button>
115
+ `)
116
+ ) : html`
117
+ <div class="empty">${this.query ? 'No results' : 'Type something'}</div>`}
118
+ `;
119
+ }
120
+ }
121
+
122
+ HeadlessEmojiComponent.define('zn-headless-emoji');
@@ -0,0 +1,52 @@
1
+ :host {
2
+ position: absolute;
3
+ z-index: var(--zn-z-index-editor-action);
4
+ display: none;
5
+ background: var(--zn-panel-background-color);
6
+ border: 1px solid var(--zn-border-color);
7
+ border-radius: 8px;
8
+ box-shadow: var(--zn-shadow-medium);
9
+ padding: 6px;
10
+ max-height: 220px;
11
+ overflow-y: auto;
12
+ min-width: 180px;
13
+ }
14
+
15
+ :host([open]) {
16
+ display: block;
17
+ }
18
+
19
+ .header {
20
+ padding: 4px 6px;
21
+ font-size: var(--zn-font-size-x-small);
22
+ opacity: 0.6;
23
+ }
24
+
25
+ .item {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 8px;
29
+ padding: 6px 8px;
30
+ width: 100%;
31
+ border: none;
32
+ background: transparent;
33
+ cursor: pointer;
34
+ font: inherit;
35
+ }
36
+
37
+ .item[aria-selected="true"] {
38
+ background: var(--zn-input-background-color-hover);
39
+ }
40
+
41
+ .emoji {
42
+ font-size: var(--zn-font-size-medium);
43
+ }
44
+
45
+ .label {
46
+ opacity: 0.7;
47
+ }
48
+
49
+ .empty {
50
+ padding: 8px;
51
+ opacity: 0.7;
52
+ }
@@ -0,0 +1,223 @@
1
+ import './headless-emoji.component';
2
+ import {html} from 'lit';
3
+ import {init, SearchIndex} from 'emoji-mart';
4
+ import {litToHTML} from '../../../../../utilities/lit-to-html';
5
+ import data from '@emoji-mart/data';
6
+ import Quill from 'quill';
7
+ import type {EmojiResult} from "../emoji";
8
+ import type {ResultItem} from './headless-emoji.component';
9
+ import type HeadlessEmojiComponent from './headless-emoji.component';
10
+
11
+ class HeadlessEmoji {
12
+ private _quill: Quill;
13
+ private _component!: HeadlessEmojiComponent;
14
+ private _startIndex = -1;
15
+ private _keydownHandler = (e: KeyboardEvent) => this.onKeydown(e);
16
+ private _docClickHandler = (e: MouseEvent) => this.onDocumentClick(e);
17
+
18
+ constructor(quill: Quill) {
19
+ this._quill = quill;
20
+ // Initialize emoji-mart index once
21
+ try {
22
+ void init({data});
23
+ } catch {
24
+ // no-op
25
+ }
26
+
27
+ this.initComponent();
28
+ this.attachEvents();
29
+ }
30
+
31
+ private initComponent() {
32
+ this._component = this.createComponent()!;
33
+ this._quill.container.ownerDocument.body.appendChild(this._component);
34
+ }
35
+
36
+ private attachEvents() {
37
+ this._quill.on(Quill.events.TEXT_CHANGE, () => this.updateFromEditor());
38
+ this._quill.on(Quill.events.EDITOR_CHANGE, () => this.updateFromEditor());
39
+ this._quill.root.addEventListener('keydown', this._keydownHandler);
40
+ this._component.addEventListener('zn-emoji-select', (e: Event) => this.onEmojiSelect(e as CustomEvent));
41
+ this._quill.on('editor-change', () => this.positionComponent());
42
+ this._quill.focus();
43
+ }
44
+
45
+ private createComponent() {
46
+ const tpl = html`
47
+ <zn-headless-emoji></zn-headless-emoji>`;
48
+ return litToHTML<HeadlessEmojiComponent>(tpl);
49
+ }
50
+
51
+ private onDocumentClick(e: MouseEvent) {
52
+ const target = e.composedPath ? e.composedPath()[0] as Node : (e.target as Node);
53
+ if (!target) return;
54
+
55
+ if (!this._component.contains(target) && !this._quill.root.contains(target)) {
56
+ this.hide();
57
+ }
58
+ }
59
+
60
+ private async updateFromEditor() {
61
+ const info = this.getEmojiQuery();
62
+ if (!info) {
63
+ this.hide();
64
+ return;
65
+ }
66
+
67
+ const {start, emojiQuery} = info;
68
+ this._startIndex = start;
69
+
70
+ try {
71
+ const results = await SearchIndex.search(emojiQuery) as EmojiResult[];
72
+ const mapped: ResultItem[] = (Array.isArray(results) ? results : []).slice(0, 20).map((e) => ({
73
+ emojiChar: (e?.skins?.[0]?.native) || e?.native || '',
74
+ label: (e?.id || e?.shortcodes || '') || ''
75
+ })).filter(it => !!it.emojiChar);
76
+
77
+ this._component.results = mapped;
78
+ this._component.query = emojiQuery;
79
+ if (mapped.length > 0) {
80
+ this.show();
81
+ this.positionComponent();
82
+ } else {
83
+ this.show();
84
+ this.positionComponent();
85
+ }
86
+ } catch {
87
+ this._component.results = [];
88
+ this._component.query = emojiQuery;
89
+ this.show();
90
+ this.positionComponent();
91
+ }
92
+ }
93
+
94
+ private positionComponent() {
95
+ if (!this._component || !this._component.open) return;
96
+
97
+ const range = this._quill.getSelection();
98
+ if (!range) return;
99
+
100
+ const bounds = this._quill.getBounds(range.index);
101
+ if (!bounds) return;
102
+
103
+ const editorBounds = this._quill.container.getBoundingClientRect();
104
+ const left = editorBounds.left + Math.max(0, bounds.left);
105
+ const top = editorBounds.top + bounds.bottom + 4;
106
+ this._component.setPosition(left, top);
107
+ }
108
+
109
+ private getEmojiQuery(): { start: number; emojiQuery: string } | null {
110
+ try {
111
+ const sel = this._quill.getSelection();
112
+ if (!sel) return null;
113
+
114
+ const cursor = sel.index;
115
+ const characterLimit = 50;
116
+ const textBefore = this._quill.getText(Math.max(0, cursor - characterLimit), Math.min(characterLimit, cursor));
117
+ const offset = cursor - Math.max(0, cursor - characterLimit);
118
+ const uptoCursor = textBefore.slice(0, offset);
119
+ const cIndex = uptoCursor.lastIndexOf(':');
120
+ if (cIndex === -1) return null;
121
+
122
+ const prev = cIndex > 0 ? uptoCursor[cIndex - 1] : ' ';
123
+ if (prev && /[^\s\n]/.test(prev)) return null; // must start at word boundary
124
+
125
+ const emojiQuery = uptoCursor.substring(cIndex + 1);
126
+ if (/[\s\n]/.test(emojiQuery)) return null; // stop at whitespace
127
+
128
+ // Do not trigger for URLs like http://
129
+ if (/^\/\//.test(uptoCursor.substring(Math.max(0, cIndex - 5), cIndex + 1))) return null;
130
+ return {start: cursor - emojiQuery.length - 1, emojiQuery};
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ private onKeydown(e: KeyboardEvent) {
137
+ if (!this._component?.open) return;
138
+
139
+ if (e.key === 'Escape') {
140
+ e.preventDefault();
141
+ e.stopPropagation();
142
+ this.hide();
143
+ return;
144
+ }
145
+
146
+ if (e.key === 'ArrowDown') {
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+ const next = (this._component.getActiveIndex?.() ?? -1) + 1;
150
+ this._component.setActiveIndex?.(next);
151
+ return;
152
+ }
153
+
154
+ if (e.key === 'ArrowUp') {
155
+ e.preventDefault();
156
+ e.stopPropagation();
157
+ const prev = (this._component.getActiveIndex?.() ?? 0) - 1;
158
+ this._component.setActiveIndex?.(prev);
159
+ return;
160
+ }
161
+
162
+ if (e.key === 'Enter') {
163
+ const idx = this._component.getActiveIndex?.();
164
+ const results = this._component.results || [];
165
+ const item = (typeof idx === 'number' && idx >= 0 && idx < results.length) ? results[idx] : undefined;
166
+ if (item) {
167
+ e.preventDefault();
168
+ e.stopPropagation();
169
+ this.replaceAtQuery(item.emojiChar);
170
+ }
171
+ }
172
+ }
173
+
174
+ private onEmojiSelect(e: CustomEvent) {
175
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
176
+ const emojiChar: string = e.detail?.emojiChar || '';
177
+ if (!emojiChar) return;
178
+ this.replaceAtQuery(emojiChar);
179
+ }
180
+
181
+ private replaceAtQuery(emojiChar: string) {
182
+ try {
183
+ const sel = this._quill.getSelection();
184
+ if (!sel || this._startIndex < 0) return;
185
+
186
+ const insertIndex = this._startIndex;
187
+ const length = sel.index - insertIndex;
188
+ if (length < 0) return;
189
+
190
+ // Remove ':' and the query
191
+ this._quill.deleteText(insertIndex, length, Quill.sources.USER);
192
+
193
+ const insertText = `${emojiChar} `;
194
+ this._quill.insertText(insertIndex, insertText, Quill.sources.USER);
195
+
196
+ const cursorPos = insertIndex + insertText.length;
197
+ setTimeout(() => this._quill.setSelection(cursorPos, 0, Quill.sources.SILENT), 0);
198
+ this._quill.focus();
199
+ this.hide();
200
+ } catch {
201
+ // no-op
202
+ }
203
+ }
204
+
205
+ private show() {
206
+ if (!this._component.open) {
207
+ this._component.show();
208
+ this._quill.container.ownerDocument.addEventListener('click', this._docClickHandler);
209
+ }
210
+ }
211
+
212
+ private hide() {
213
+ this._component.hide();
214
+ this._startIndex = -1;
215
+ this._quill.container.ownerDocument.removeEventListener('click', this._docClickHandler);
216
+ }
217
+
218
+ public isOpen() {
219
+ return this._component?.open;
220
+ }
221
+ }
222
+
223
+ export default HeadlessEmoji;
@@ -1,18 +1,18 @@
1
1
  import Quill, {Module} from "quill";
2
2
 
3
- type ImageResizeModuleOptions = {
3
+ interface ImageResizeOptions {
4
4
  overlayStyles?: Partial<CSSStyleDeclaration>;
5
5
  }
6
6
 
7
- class ImageResizeModule extends Module<ImageResizeModuleOptions>
7
+ class ImageResize extends Module<ImageResizeOptions>
8
8
  {
9
9
 
10
- static DEFAULTS: ImageResizeModuleOptions = {};
10
+ static DEFAULTS: ImageResizeOptions = {};
11
11
 
12
12
  private _focusedImage: HTMLImageElement | null = null;
13
13
  private _overlay: HTMLDivElement | null = null;
14
14
 
15
- constructor(quill: Quill, options: ImageResizeModuleOptions)
15
+ constructor(quill: Quill, options: ImageResizeOptions)
16
16
  {
17
17
  super(quill, options);
18
18
 
@@ -154,4 +154,4 @@ class ImageResizeModule extends Module<ImageResizeModuleOptions>
154
154
  };
155
155
  }
156
156
 
157
- export default ImageResizeModule;
157
+ export default ImageResize;
@@ -1,17 +1,17 @@
1
- import Quill from 'quill';
1
+ import type Quill from 'quill';
2
2
 
3
- type TimeTrackingModuleOptions = {
3
+ interface TimeTrackingOptions {
4
4
  startTimeInput?: HTMLInputElement;
5
5
  openTimeInput?: HTMLInputElement;
6
6
  }
7
7
 
8
- export default class TimeTrackingModule {
8
+ export default class TimeTracking {
9
9
  private _quill: Quill;
10
- private _options: TimeTrackingModuleOptions;
10
+ private _options: TimeTrackingOptions;
11
11
  private _startTime: number;
12
12
  private _openTime: number;
13
13
 
14
- constructor(quill: Quill, options: TimeTrackingModuleOptions) {
14
+ constructor(quill: Quill, options: TimeTrackingOptions) {
15
15
  this._quill = quill;
16
16
  this._options = options;
17
17
 
@@ -0,0 +1,12 @@
1
+ import ZnEditorTool from './tool.component';
2
+
3
+ export * from './tool.component';
4
+ export default ZnEditorTool;
5
+
6
+ ZnEditorTool.define('zn-editor-tool');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-editor-tool': ZnEditorTool;
11
+ }
12
+ }
@@ -0,0 +1,30 @@
1
+ import {type CSSResultGroup, html, unsafeCSS} from "lit";
2
+ import {property} from "lit/decorators.js";
3
+ import ZincElement from "../../../../../internal/zinc-element";
4
+
5
+ import styles from './tool.scss';
6
+
7
+ export default class ZnEditorTool extends ZincElement {
8
+ static styles: CSSResultGroup = unsafeCSS(styles);
9
+
10
+ @property() uri: string;
11
+ @property() label: string;
12
+ @property() key: string;
13
+ @property() icon: string;
14
+ @property() handler: string = 'dialog';
15
+
16
+ render() {
17
+ return html`
18
+ <zn-button class="tool-action"
19
+ color="transparent"
20
+ icon="${this.icon}"
21
+ icon-size="18"
22
+ data-format="${this.handler}"
23
+ data-format-type="${this.uri}"
24
+ data-toolbar-key="${this.key}"
25
+ aria-label="${this.label}"></zn-button>
26
+ <div class="${this.handler}"></div>`
27
+ }
28
+ }
29
+
30
+ ZnEditorTool.define('zn-editor-tool');
@@ -0,0 +1,5 @@
1
+ .tool-action {
2
+ width: 28px;
3
+ height: 28px;
4
+ margin: 3px 2px;
5
+ }