@kubex/zinc 1.0.6 → 1.0.8

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 (40) hide show
  1. package/dist/custom-elements.json +464 -34
  2. package/dist/vscode.html-custom-data.json +38 -6
  3. package/dist/web-types.json +97 -10
  4. package/dist/zn.d.ts +73 -8
  5. package/dist/zn.min.js +393 -349
  6. package/docs/data/products-table.json +203 -0
  7. package/docs/pages/components/button.md +25 -0
  8. package/docs/pages/components/data-table.md +21 -0
  9. package/docs/pages/components/header.md +7 -0
  10. package/docs/pages/components/order-table.md +82 -5
  11. package/package.json +1 -1
  12. package/src/components/button/button.component.ts +168 -10
  13. package/src/components/button/button.scss +115 -0
  14. package/src/components/content-block/content-block.component.ts +186 -107
  15. package/src/components/content-block/content-block.scss +4 -2
  16. package/src/components/data-select/data-select.component.ts +50 -10
  17. package/src/components/data-table/data-table.component.ts +130 -43
  18. package/src/components/data-table/data-table.scss +85 -7
  19. package/src/components/data-table-filter/data-table-filter.component.ts +1 -1
  20. package/src/components/editor/editor.scss +1 -0
  21. package/src/components/editor/modules/attachment/attachment.ts +53 -41
  22. package/src/components/editor/modules/toolbar/toolbar.ts +42 -0
  23. package/src/components/empty-state/empty-state.component.ts +16 -8
  24. package/src/components/expanding-action/expanding-action.component.ts +1 -1
  25. package/src/components/header/header.component.ts +8 -2
  26. package/src/components/header/header.scss +5 -1
  27. package/src/components/icon/icon.component.ts +2 -0
  28. package/src/components/icon/icon.scss +13 -0
  29. package/src/components/item/item.component.ts +11 -3
  30. package/src/components/item/item.scss +7 -4
  31. package/src/components/linked-select/linked-select.component.ts +1 -0
  32. package/src/components/menu/menu.component.ts +0 -1
  33. package/src/components/navbar/navbar.component.ts +1 -1
  34. package/src/components/note/note.component.ts +70 -2
  35. package/src/components/note/note.scss +26 -4
  36. package/src/components/order-table/order-table.component.ts +49 -28
  37. package/src/components/panel/panel.component.ts +5 -1
  38. package/src/components/panel/panel.scss +6 -0
  39. package/src/components/rating/rating.component.ts +10 -9
  40. package/src/components/rating/rating.scss +15 -1
@@ -2,7 +2,7 @@ import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, nothing, type TemplateResult, unsafeCSS} from 'lit';
3
3
  import {HasSlotController} from "../../internal/slot";
4
4
  import {ifDefined} from "lit/directives/if-defined.js";
5
- import {property} from 'lit/decorators.js';
5
+ import {property, query} from 'lit/decorators.js';
6
6
  import {ref} from "lit/directives/ref.js";
7
7
  import {Task} from "@lit/task";
8
8
  import {unsafeHTML} from "lit/directives/unsafe-html.js";
@@ -192,6 +192,12 @@ export default class ZnDataTable extends ZincElement {
192
192
 
193
193
  @property({attribute: "no-initial-load", type: Boolean}) noInitialLoad: boolean = false;
194
194
 
195
+ @property({attribute: 'group-by'}) groupBy = '';
196
+
197
+ @property() groups = '';
198
+
199
+ @query('#select-all-rows') selectAllButton: ZnButton;
200
+
195
201
  // Data Table Properties
196
202
  private _initialLoad = true;
197
203
  private _lastTableContent: TemplateResult = html``;
@@ -226,6 +232,11 @@ export default class ZnDataTable extends ZincElement {
226
232
  return {rows: [], page: 1, perPage: this.itemsPerPage, total: 0};
227
233
  }
228
234
 
235
+ if (this.groupBy) {
236
+ // we want to load all the data possible so we can group and show multiple tables
237
+ this.itemsPerPage = 1000;
238
+ }
239
+
229
240
  const requestData: DataRequest = {
230
241
  page: this.page,
231
242
  perPage: this.itemsPerPage,
@@ -278,8 +289,6 @@ export default class ZnDataTable extends ZincElement {
278
289
 
279
290
  requestParams: Record<string, any> = {};
280
291
 
281
- builtinRequestParams: Record<string, any> = {};
282
-
283
292
  refresh() {
284
293
  // Allow manual refresh to trigger the first data load when no-initial-load is set
285
294
  this._initialLoad = false;
@@ -392,13 +401,58 @@ export default class ZnDataTable extends ZincElement {
392
401
  return this.emptyState();
393
402
  }
394
403
 
404
+ this._rows = this.getRows(data);
405
+
406
+ if (this.groupBy) {
407
+ // we need to group the rows by the group column
408
+ const groupedRows: Record<string, Row[]> = {};
409
+
410
+ // add groups to the current rows
411
+ const toAdd = this.groups.split(',').map(g => g.trim()).filter(g => g.length > 0);
412
+ toAdd.forEach((group) => {
413
+ if (!groupedRows[this.humanize(group)]) {
414
+ groupedRows[this.humanize(group)] = [];
415
+ }
416
+ });
417
+
418
+ this._rows.forEach((row: Row) => {
419
+ const groupCell = row.cells.find((cell: Cell) => cell.column === this.groupBy);
420
+ const groupValue = groupCell ? groupCell.text : 'Ungrouped';
421
+ if (!groupedRows[this.humanize(groupValue)] && (!this.groups || groupValue === 'Ungrouped')) {
422
+ groupedRows[this.humanize(groupValue)] = [];
423
+ }
424
+
425
+ if (groupedRows[this.humanize(groupValue)]) {
426
+ groupedRows[this.humanize(groupValue)].push(row);
427
+ }
428
+ });
429
+
430
+ // render a table for each group
431
+ return html`
432
+ <zn-sp flush>
433
+ ${Object.keys(groupedRows).map((groupKey) => html`
434
+ <div class="table-group">
435
+ <h3 class="table-group__title">${groupKey === "Ungrouped" ? "" : groupKey}</h3>
436
+ ${this.renderTableData(groupedRows[groupKey])}
437
+ </div>`
438
+ )}
439
+ </zn-sp>
440
+ `;
441
+ }
442
+
443
+
444
+ return this.renderTableData(this._rows);
445
+ }
446
+
447
+ public humanize(str: string) {
448
+ return str.charAt(0).toUpperCase() + str.slice(1);
449
+ }
450
+
451
+ public renderTableData(data: any) {
395
452
  const filteredHeaders = Object.values(this.headers).filter((header) => {
396
453
  return !Object.values(this.hiddenColumns).includes(header.key);
397
454
  });
398
455
 
399
- this._rows = this.getRows(data);
400
-
401
- const hasSelectedRows = this.selectedRows.length > 0;
402
456
  this.rowHasActions = this._rows.some((row: Row) => row.actions && row.actions.length > 0);
403
457
 
404
458
  // To make sure the cells are in the same order as the headers, we need to map them cell.col to heading.key
@@ -423,27 +477,18 @@ export default class ZnDataTable extends ZincElement {
423
477
  <table class="${classMap({
424
478
  'table': true,
425
479
  'table--standalone': this.standalone,
426
- 'with-hover': !this.unsortable,
427
- 'table--with-checkboxes': !this.hideCheckboxes,
480
+ 'with-hover': !this.unsortable && !this.hideCheckboxes,
428
481
  })}">
429
482
  <thead>
430
483
  <tr>
431
- ${this.hideCheckboxes || !hasSelectedRows ? html`` : html`
432
- <th>
433
- <div><input type="checkbox" @change="${this.selectAll}"></div>
434
- </th>`}
435
484
  ${filteredHeaders.map((header: HeaderConfig) => this.renderCellHeader(header))}
436
485
  ${this.rowHasActions ? html`
437
486
  <th></th>` : html``}
438
487
  </tr>
439
488
  </thead>
440
489
  <tbody>
441
- ${this._rows.map((row: Row) => html`
442
- <tr>
443
- ${this.hideCheckboxes ? html`` : html`
444
- <td class="${classMap({'hidden': !hasSelectedRows})}">
445
- <div><input type="checkbox" @change="${this.selectRow}"></div>
446
- </td>`}
490
+ ${data.map((row: Row) => html`
491
+ <tr class="${classMap({'table__row--selected': this.isRowSelected(row)})}">
447
492
  ${row.cells.map((value: Cell, index: number) => this.renderCellBody(index, value))}
448
493
  ${this.rowHasActions ? this.renderActions(row) : null}
449
494
  </tr>`)}
@@ -490,7 +535,7 @@ export default class ZnDataTable extends ZincElement {
490
535
  }
491
536
 
492
537
  getRowsSelected() {
493
- if (this.hideCheckboxes || this.selectedRows.length <= 0) return null;
538
+ if (this.selectedRows.length <= 0) return null;
494
539
 
495
540
  return html`
496
541
  <p>${this.numberOfRowsSelected} of ${this._rows.length} rows selected</p>`
@@ -559,10 +604,37 @@ export default class ZnDataTable extends ZincElement {
559
604
  getActions() {
560
605
  const actions = [];
561
606
 
607
+ const hasSlots = this.hasSlotController.test(ActionSlots.delete.valueOf())
608
+ || this.hasSlotController.test(ActionSlots.modify.valueOf())
609
+ || this.hasSlotController.test(ActionSlots.create.valueOf());
610
+
611
+ if(!hasSlots) {
612
+ return [];
613
+ }
614
+
615
+ if (!this.hideCheckboxes && this._rows.length > 0) {
616
+ actions.push(html`
617
+ <zn-button @click="${this.selectAll}"
618
+ id="select-all-rows"
619
+ color="transparent"
620
+ size="x-small"
621
+ icon="indeterminate_check_box"
622
+ icon-size="22"
623
+ icon-color="primary"
624
+ tooltip="Select All"
625
+ slot="trigger">
626
+ </zn-button>`);
627
+ }
628
+
562
629
  if (this.selectedRows.length > 0) {
563
630
  actions.push(html`
564
- <zn-button @click="${this.clearSelectedRows}" size="small" outline>
565
- Clear Selection
631
+ <zn-button @click="${this.clearSelectedRows}"
632
+ color="transparent"
633
+ size="x-small"
634
+ icon="disabled_by_default"
635
+ icon-size="22"
636
+ tooltip="Clear Selection"
637
+ slot="trigger">
566
638
  </zn-button>`);
567
639
 
568
640
  if (this.hasSlotController.test(ActionSlots.delete.valueOf())) {
@@ -620,21 +692,25 @@ export default class ZnDataTable extends ZincElement {
620
692
  }
621
693
 
622
694
  selectAll(event: Event) {
623
- const checkbox = event.target as HTMLInputElement;
624
- const checked = checkbox.checked;
695
+ const button = event.target as ZnButton;
696
+ if (button.disabled) return;
625
697
 
626
- // go through all the checkboxes and check them
627
- for (const row of this.renderRoot.querySelectorAll('tbody input[type="checkbox"]')) {
628
- (row as HTMLInputElement).checked = checked;
698
+ if (this.numberOfRowsSelected === this._rows.length) {
699
+ this.clearSelectedRows(event);
700
+ return;
629
701
  }
630
702
 
631
- this.selectedRows = checked ? this._rows : [];
703
+ this.selectedRows = this._rows;
632
704
  this.numberOfRowsSelected = this.selectedRows.length;
633
705
  this.updateKeys();
634
706
  this.requestUpdate();
635
707
  }
636
708
 
637
709
  selectRow(e: Event) {
710
+ if (this.hideCheckboxes) {
711
+ return;
712
+ }
713
+
638
714
  if (!(e.target && (e.target instanceof Element))) {
639
715
  return;
640
716
  }
@@ -655,17 +731,19 @@ export default class ZnDataTable extends ZincElement {
655
731
  return;
656
732
  }
657
733
 
658
- const checkbox: HTMLInputElement | null = parent.querySelector('input[type="checkbox"]');
659
- if (checkbox) {
660
- const isCheckboxTarget = target instanceof HTMLInputElement && target.type === 'checkbox';
661
- if (!isCheckboxTarget) {
662
- checkbox.checked = !checkbox.checked;
663
- }
664
- }
734
+ const rows = Array.from(this.renderRoot.querySelectorAll('tbody tr'));
735
+ const index = rows.indexOf(parent as HTMLTableRowElement);
736
+ if (index === -1) return;
665
737
 
666
- this.selectedRows = this._rows.filter((_, index) => {
667
- return (this.renderRoot.querySelectorAll('tbody input[type="checkbox"]')[index] as HTMLInputElement)?.checked;
668
- });
738
+ const row = this._rows[index] as Row;
739
+ if (!row) return;
740
+
741
+ const alreadySelected = this.selectedRows.some((r: Row) => r.id === row.id);
742
+ if (alreadySelected) {
743
+ this.selectedRows = this.selectedRows.filter((r: Row) => r.id !== row.id);
744
+ } else {
745
+ this.selectedRows = [...(this.selectedRows as Row[]), row];
746
+ }
669
747
 
670
748
  this.numberOfRowsSelected = this.selectedRows.length;
671
749
  this.updateKeys();
@@ -676,13 +754,9 @@ export default class ZnDataTable extends ZincElement {
676
754
  const button = event.target as ZnButton;
677
755
  if (button.disabled) return;
678
756
 
679
- (this.renderRoot.querySelectorAll('thead input[type="checkbox"]')[0] as HTMLInputElement).checked = false;
680
- for (const row of this.renderRoot.querySelectorAll('tbody input[type="checkbox"]')) {
681
- (row as HTMLInputElement).checked = false;
682
- }
683
-
684
757
  this.selectedRows = [];
685
758
  this.numberOfRowsSelected = 0;
759
+ this.updateKeys();
686
760
  this.requestUpdate();
687
761
  }
688
762
 
@@ -842,7 +916,7 @@ export default class ZnDataTable extends ZincElement {
842
916
 
843
917
  return html`
844
918
  <td
845
- @click="${this.selectRow}"
919
+ @click="${this.hideCheckboxes ? undefined : this.selectRow}"
846
920
  class="${classMap({
847
921
  'table__cell': true,
848
922
  'table__cell--wide': headerKey === this.wideColumn,
@@ -852,6 +926,10 @@ export default class ZnDataTable extends ZincElement {
852
926
  </td>`;
853
927
  }
854
928
 
929
+ private isRowSelected(row: Row): boolean {
930
+ return this.selectedRows.some((r: Row) => r.id === row.id);
931
+ }
932
+
855
933
  private getRows(data: Response): Row[] {
856
934
  const sourceRows = data.rows;
857
935
 
@@ -894,10 +972,19 @@ export default class ZnDataTable extends ZincElement {
894
972
  }
895
973
 
896
974
  private updateKeys() {
975
+ this.updateSelectAll();
897
976
  this.updateModifyKeys();
898
977
  this.updateDeleteKeys();
899
978
  }
900
979
 
980
+ private updateSelectAll() {
981
+ if (this.numberOfRowsSelected === this._rows.length) {
982
+ this.selectAllButton.icon = 'check_box';
983
+ } else {
984
+ this.selectAllButton.icon = 'indeterminate_check_box';
985
+ }
986
+ }
987
+
901
988
  private updateModifyKeys() {
902
989
  this.updateActionKeys('modify-action');
903
990
  }
@@ -21,7 +21,6 @@ table {
21
21
  &.table--standalone {
22
22
  background-color: rgb(var(--zn-panel));
23
23
  border-radius: var(--zn-border-radius-large);
24
- box-shadow: var(--zn-shadow-medium);
25
24
  border: 1px solid rgb(var(--zn-border-color));
26
25
  overflow: hidden;
27
26
  }
@@ -35,7 +34,7 @@ table {
35
34
  border-bottom: 1px solid rgb(var(--zn-border-color)) !important;
36
35
  }
37
36
 
38
- .table--with-checkboxes tbody tr:hover {
37
+ &.with-hover tbody tr:hover {
39
38
  background-color: rgba(var(--zn-primary), 0.05) !important;
40
39
  cursor: pointer;
41
40
  }
@@ -115,10 +114,10 @@ table {
115
114
  }
116
115
  }
117
116
 
118
- &:first-of-type > div:has(input[type="checkbox"]) {
117
+ &:first-of-type > div:has(zn-checkbox) {
119
118
  justify-content: center;
120
119
 
121
- input[type="checkbox"] {
120
+ zn-checkbox {
122
121
  margin: 0;
123
122
  }
124
123
  }
@@ -240,7 +239,7 @@ table {
240
239
  flex-direction: row;
241
240
  justify-content: space-between;
242
241
  align-items: baseline;
243
- padding: var(--zn-spacing-small);
242
+ padding-block: var(--zn-spacing-small);
244
243
  gap: var(--zn-spacing-small);
245
244
 
246
245
  &__actions,
@@ -306,9 +305,7 @@ table {
306
305
  }
307
306
  }
308
307
  }
309
-
310
308
  }
311
-
312
309
  }
313
310
 
314
311
  .reduced-opacity .table tbody {
@@ -333,3 +330,84 @@ table {
333
330
  filter: drop-shadow(0 5px 4px rgba(0, 0, 0, 0.04)) drop-shadow(0 4px 3px rgba(0, 0, 0, 0.1));
334
331
  background-color: rgb(var(--zn-panel, 255, 255, 255));
335
332
  }
333
+
334
+ /* Table Select Styles */
335
+ table tbody tr.table__row--selected {
336
+ background-color: var(--zn-color-purple-100);
337
+
338
+ &:nth-of-type(even) {
339
+ background-color: hsla(from var(--zn-color-purple-200) h s l / 0.8);
340
+ }
341
+
342
+ &:hover {
343
+ background-color: var(--zn-color-purple-200) !important;
344
+ }
345
+
346
+ td {
347
+ border-top: 1px solid var(--zn-color-purple-600) !important;
348
+ border-bottom: 1px solid var(--zn-color-purple-600) !important;
349
+ }
350
+
351
+ td:first-of-type {
352
+ border-left: 1px solid var(--zn-color-purple-600);
353
+ }
354
+
355
+ td:last-of-type {
356
+ border-right: 1px solid var(--zn-color-purple-600);
357
+ }
358
+
359
+ &:last-of-type td:first-of-type {
360
+ border-bottom-left-radius: var(--zn-border-radius-large);
361
+ }
362
+
363
+ &:last-of-type td:last-of-type {
364
+ border-bottom-right-radius: var(--zn-border-radius-large);
365
+ }
366
+
367
+ & + tr.table__row--selected td {
368
+ border-top: 0 !important;
369
+
370
+ &:first-of-type {
371
+ border-top-left-radius: 0;
372
+ }
373
+
374
+ &:last-of-type {
375
+ border-top-right-radius: 0;
376
+ }
377
+ }
378
+
379
+ &:has(+ tr.table__row--selected) td {
380
+ border-bottom: 0 !important;
381
+ }
382
+
383
+ &:first-of-type td {
384
+ border-top: 0 !important;
385
+ }
386
+
387
+ &:has(+ tr.table__row--selected) {
388
+ td:first-of-type {
389
+ border-bottom-left-radius: 0;
390
+ }
391
+
392
+ td:last-of-type {
393
+ border-bottom-right-radius: 0;
394
+ }
395
+ }
396
+ }
397
+
398
+ table:has(tbody tr:first-of-type.table__row--selected) thead tr th {
399
+ border-bottom-color: var(--zn-color-purple-600) !important;
400
+ }
401
+
402
+
403
+ .table-group__title {
404
+ font-weight: var(--zn-font-weight-semibold);
405
+ font-size: var(--zn-font-size-large);
406
+ color: rgba(var(--zn-text-panel-title), 100%);
407
+ line-height: var(--zn-line-height-looser);
408
+ padding-left: var(--zn-spacing-medium);
409
+ margin: 0;
410
+ overflow: hidden;
411
+ white-space: nowrap;
412
+ text-overflow: ellipsis;
413
+ }
@@ -106,7 +106,7 @@ export default class ZnDataTableFilter extends ZincElement implements ZincFormCo
106
106
  return html`
107
107
  <zn-button id="slideout-trigger" color="transparent" size="x-small" icon="filter_alt" icon-size="22"
108
108
  slot="trigger"
109
- tooltip="Open Filter">Filter
109
+ tooltip="Filter">
110
110
  </zn-button>
111
111
  <zn-slideout class="slideout-basic" trigger="slideout-trigger" label="Filters">
112
112
 
@@ -66,6 +66,7 @@
66
66
 
67
67
  img {
68
68
  max-width: 40%;
69
+ height: auto;
69
70
  }
70
71
  }
71
72
 
@@ -2,7 +2,7 @@ import type Quill from 'quill';
2
2
  import type Toolbar from "../toolbar/toolbar";
3
3
 
4
4
  interface AttachmentOptions {
5
- upload: (file: File) => Promise<{ path: any, url: any, filename: any }>;
5
+ upload: (file: File) => Promise<{ path: any; url: any; filename: any }>;
6
6
  onFileUploaded?: (node: HTMLElement, {url}: { url: string }) => void;
7
7
  attachmentInput?: HTMLInputElement;
8
8
  }
@@ -26,6 +26,9 @@ export default class Attachment {
26
26
  if (typeof (this._options.upload) !== "function") {
27
27
  console.warn("[Quill Attachment Module] No upload function provided");
28
28
  }
29
+ if (typeof (this._options.onFileUploaded) !== "function") {
30
+ console.warn("[Quill Attachment Module] No file uploaded function provided");
31
+ }
29
32
 
30
33
  (this._quill
31
34
  .getModule('toolbar') as Toolbar)
@@ -48,24 +51,34 @@ export default class Attachment {
48
51
  }
49
52
 
50
53
  const file = this._fileHolder.files[0];
51
- const attachmentId = generateId();
52
- const fileReader = new FileReader();
54
+ this.addAttachment(file);
55
+ }
53
56
 
54
- fileReader.addEventListener('load', () => {
55
- const base64Content = fileReader.result as string;
56
- this._insertAttachment({dataUrl: base64Content, file, id: attachmentId});
57
- }, false);
57
+ public addAttachment(file: File, dataUrl?: string) {
58
+ const attachmentId = generateId();
59
+ const insertWithDataUrl = (base64: string) => {
60
+ this._insertAttachment({ dataUrl: base64, file, id: attachmentId });
61
+ };
58
62
 
59
- if (file) {
63
+ if (dataUrl) {
64
+ insertWithDataUrl(dataUrl);
65
+ } else {
66
+ const fileReader = new FileReader();
67
+ fileReader.addEventListener('load', () => {
68
+ const base64Content = fileReader.result as string;
69
+ insertWithDataUrl(base64Content);
70
+ }, false);
60
71
  fileReader.readAsDataURL(file);
61
72
  }
62
73
 
63
- this._options.upload(file).then(({path, url}) => {
64
- this._uploadAttachment(file, url);
65
- this._updateAttachment(attachmentId, url, path);
66
- }).catch(err => {
67
- console.warn(err.message);
68
- });
74
+ if (typeof this._options.upload === 'function') {
75
+ this._options.upload(file).then(({ path, url }: { path: string; url: string }) => {
76
+ this._uploadAttachment(file, url);
77
+ this._updateAttachment(attachmentId, url, path);
78
+ }).catch((err: { message: string }) => {
79
+ console.warn(err.message);
80
+ });
81
+ }
69
82
  }
70
83
 
71
84
  private _attachmentContainer = document.createElement('div');
@@ -85,37 +98,36 @@ export default class Attachment {
85
98
  this._quill.container.appendChild(attachmentContainer);
86
99
  }
87
100
 
88
- private _insertAttachment({dataUrl, file, id}: { dataUrl: string, file: File, id: string }) {
101
+ private _insertAttachment({dataUrl, file, id}: { dataUrl: string; file: File; id: string }) {
89
102
  this._attachmentContainer.appendChild(this._createAttachment(dataUrl, file, id));
90
103
  }
91
104
 
92
105
  private _updateAttachment(id: string, url: string, filename: string) {
93
- const element = this._quill.container.querySelector(`#${id}`) as HTMLAnchorElement;
94
- if (element) {
95
- element.setAttribute('href', url);
96
- let attachmentName = element.querySelector('.attachment-name');
97
- if (attachmentName) {
98
- if (filename) {
99
- attachmentName.textContent = filename;
100
- } else {
101
- attachmentName.textContent = 'Error uploading file';
102
- }
103
- }
104
-
105
- if (typeof this._options.onFileUploaded === 'function') {
106
- this._options.onFileUploaded(element, {url});
106
+ const element = this._quill.container.querySelector(`#${id}`);
107
+ if (!element) return;
108
+
109
+ element.setAttribute('href', url);
110
+ const attachmentName = element.querySelector('.attachment-name');
111
+ if (attachmentName) {
112
+ if (filename) {
113
+ attachmentName.textContent = filename;
114
+ } else {
115
+ attachmentName.textContent = 'Error uploading file';
107
116
  }
117
+ }
108
118
 
119
+ if (this._options.onFileUploaded) {
120
+ this._options.onFileUploaded(element as HTMLElement, {url});
121
+ }
109
122
 
110
- // add the url to the hidden input
111
- const attachments = this._options.attachmentInput;
112
- if (attachments && filename) {
113
- // value should be an array of attachment names
114
- const value = attachments.value;
115
- const data = value ? JSON.parse(value) : [];
116
- data.push(filename);
117
- attachments.value = JSON.stringify(data);
118
- }
123
+ // add the url to the hidden input
124
+ const attachments = this._options.attachmentInput;
125
+ if (attachments && filename) {
126
+ // value should be an array of attachment names
127
+ const value = attachments.value;
128
+ const data: string[] = value ? JSON.parse(value) as string[] : [];
129
+ data.push(filename);
130
+ attachments.value = JSON.stringify(data);
119
131
  }
120
132
  }
121
133
 
@@ -162,9 +174,9 @@ export default class Attachment {
162
174
  if (attachments) {
163
175
  // value should be an array of attachment names
164
176
  const value = attachments.value;
165
- let attachmentName = attachment?.querySelector('.attachment-name');
166
- const data = value ? JSON.parse(value) : [];
167
- const index = data.indexOf(attachmentName ? attachmentName.textContent : '');
177
+ const attachmentName = attachment?.querySelector('.attachment-name');
178
+ const data: string[] = value ? JSON.parse(value) as string[] : [];
179
+ const index = data.indexOf(attachmentName?.textContent ? attachmentName.textContent : '');
168
180
  if (index > -1) {
169
181
  data.splice(index, 1);
170
182
  attachments.value = JSON.stringify(data);
@@ -1,6 +1,7 @@
1
1
  import './toolbar.component';
2
2
  import Quill from "quill";
3
3
  import QuillToolbar from "quill/modules/toolbar";
4
+ import type Attachment from "../attachment/attachment";
4
5
  import type DialogComponent from "../dialog/dialog.component";
5
6
  import type Emoji from "../emoji/emoji";
6
7
  import type ToolbarComponent from "./toolbar.component";
@@ -26,6 +27,7 @@ class Toolbar extends QuillToolbar {
26
27
  this.addHandler('redo', () => quill.history.redo());
27
28
  this.addHandler('undo', () => quill.history.undo());
28
29
  this.addHandler('dialog', (value: string) => this._openDialog(value));
30
+ this.addHandler('image', () => this._addImage());
29
31
 
30
32
  this._quill = quill;
31
33
  this._component = options.container;
@@ -323,6 +325,46 @@ class Toolbar extends QuillToolbar {
323
325
  const emoji = this._quill.getModule('emoji') as Emoji;
324
326
  emoji?.initPicker();
325
327
  }
328
+
329
+ private _addImage() {
330
+ const input = document.createElement('input')
331
+ input.setAttribute('type', 'file')
332
+ input.setAttribute('accept', 'image/*')
333
+ input.click()
334
+
335
+ input.onchange = () => {
336
+ const file = input.files?.[0];
337
+ if (!file?.type || !file.type.startsWith('image/')) return;
338
+
339
+ const reader = new window.FileReader();
340
+ reader.onload = () => {
341
+ const dataUrl = reader.result as string;
342
+ const selection = this._quill.getSelection(true);
343
+ const index = selection ? selection.index + selection.length : this._quill.getLength();
344
+
345
+ this._quill.insertEmbed(index, 'image', dataUrl, Quill.sources.USER);
346
+
347
+ const root = this._quill.root as HTMLElement;
348
+ const images = Array.from(root.querySelectorAll('img')) as HTMLImageElement[];
349
+ const inserted = images.reverse().find(img => img.getAttribute('src') === dataUrl) || null;
350
+ if (inserted) {
351
+ inserted.setAttribute('alt', file.name.replace(/\.[^/.]+$/, ''));
352
+ inserted.setAttribute('title', file.name);
353
+ }
354
+
355
+ this._quill.setSelection(index + 1, 0, Quill.sources.USER);
356
+ this._syncToolbarState();
357
+
358
+ try {
359
+ const attachmentModule = this._quill.getModule('attachment') as Attachment | undefined;
360
+ attachmentModule?.addAttachment(file, dataUrl);
361
+ } catch (err) {
362
+ console.warn('[Toolbar] Failed to process image with attachment handler', err);
363
+ }
364
+ };
365
+ reader.readAsDataURL(file);
366
+ }
367
+ }
326
368
  }
327
369
 
328
370
  export default Toolbar;