@vaadin/crud 24.6.0-beta1 → 24.7.0-alpha1

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.
@@ -0,0 +1,1045 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2000 - 2024 Vaadin Ltd.
4
+ *
5
+ * This program is available under Vaadin Commercial License and Service Terms.
6
+ *
7
+ *
8
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
9
+ * license.
10
+ */
11
+ import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
12
+ import { FocusRestorationController } from '@vaadin/a11y-base/src/focus-restoration-controller.js';
13
+ import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
14
+ import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
15
+ import { ButtonSlotController, FormSlotController, GridSlotController } from './vaadin-crud-controllers.js';
16
+ import { getProperty, isValidEditorPosition, setProperty } from './vaadin-crud-helpers.js';
17
+
18
+ /**
19
+ * A mixin providing common crud functionality.
20
+ *
21
+ * @polymerMixin
22
+ */
23
+ export const CrudMixin = (superClass) =>
24
+ class extends superClass {
25
+ static get properties() {
26
+ return {
27
+ /**
28
+ * A reference to the grid used for displaying the item list
29
+ * @private
30
+ */
31
+ _grid: {
32
+ type: Object,
33
+ observer: '__gridChanged',
34
+ },
35
+
36
+ /**
37
+ * A reference to the editor component which will be teleported to the dialog
38
+ * @private
39
+ */
40
+ _form: {
41
+ type: Object,
42
+ observer: '__formChanged',
43
+ },
44
+
45
+ /**
46
+ * A reference to the save button which will be teleported to the dialog
47
+ * @private
48
+ */
49
+ _saveButton: {
50
+ type: Object,
51
+ },
52
+
53
+ /**
54
+ * A reference to the delete button which will be teleported to the dialog
55
+ * @private
56
+ */
57
+ _deleteButton: {
58
+ type: Object,
59
+ },
60
+
61
+ /**
62
+ * A reference to the cancel button which will be teleported to the dialog
63
+ * @private
64
+ */
65
+ _cancelButton: {
66
+ type: Object,
67
+ },
68
+
69
+ /**
70
+ * A reference to the default editor header element created by the CRUD
71
+ * @private
72
+ */
73
+ _defaultHeader: {
74
+ type: Object,
75
+ },
76
+
77
+ /**
78
+ * A reference to the "New item" button
79
+ * @private
80
+ */
81
+ _newButton: {
82
+ type: Object,
83
+ },
84
+
85
+ /**
86
+ * An array containing the items which will be stamped to the column template instances.
87
+ * @type {Array<unknown> | undefined}
88
+ */
89
+ items: {
90
+ type: Array,
91
+ notify: true,
92
+ observer: '__itemsChanged',
93
+ },
94
+
95
+ /**
96
+ * The item being edited in the dialog.
97
+ * @type {unknown}
98
+ */
99
+ editedItem: {
100
+ type: Object,
101
+ observer: '__editedItemChanged',
102
+ notify: true,
103
+ },
104
+
105
+ /**
106
+ * Sets how editor will be presented on desktop screen.
107
+ *
108
+ * Accepted values are:
109
+ * - `` (default) - form will open as overlay
110
+ * - `bottom` - form will open below the grid
111
+ * - `aside` - form will open on the grid side (_right_, if lft and _left_ if rtl)
112
+ * @attr {bottom|aside} editor-position
113
+ * @type {!CrudEditorPosition}
114
+ */
115
+ editorPosition: {
116
+ type: String,
117
+ value: '',
118
+ reflectToAttribute: true,
119
+ observer: '__editorPositionChanged',
120
+ },
121
+
122
+ /**
123
+ * Enables user to click on row to edit it.
124
+ * Note: When enabled, auto-generated grid won't show the edit column.
125
+ * @attr {boolean} edit-on-click
126
+ * @type {boolean}
127
+ */
128
+ editOnClick: {
129
+ type: Boolean,
130
+ value: false,
131
+ },
132
+
133
+ /**
134
+ * Function that provides items lazily. Receives arguments `params`, `callback`
135
+ *
136
+ * `params.page` Requested page index
137
+ * `params.pageSize` Current page size
138
+ * `params.filters` Currently applied filters
139
+ * `params.sortOrders` Currently applied sorting orders
140
+ *
141
+ * `callback(items, size)` Callback function with arguments:
142
+ * - `items` Current page of items
143
+ * - `size` Total number of items
144
+ * @type {CrudDataProvider | undefined}
145
+ */
146
+ dataProvider: {
147
+ type: Function,
148
+ observer: '__dataProviderChanged',
149
+ },
150
+
151
+ /**
152
+ * Disable filtering when grid is autoconfigured.
153
+ * @attr {boolean} no-filter
154
+ */
155
+ noFilter: Boolean,
156
+
157
+ /**
158
+ * Disable sorting when grid is autoconfigured.
159
+ * @attr {boolean} no-sort
160
+ */
161
+ noSort: Boolean,
162
+
163
+ /**
164
+ * Remove grid headers when it is autoconfigured.
165
+ * @attr {boolean} no-head
166
+ */
167
+ noHead: Boolean,
168
+
169
+ /**
170
+ * A comma-separated list of fields to include in the generated grid and the generated editor.
171
+ *
172
+ * It can be used to explicitly define the field order.
173
+ *
174
+ * When it is defined [`exclude`](#/elements/vaadin-crud#property-exclude) is ignored.
175
+ *
176
+ * Default is undefined meaning that all properties in the object should be mapped to fields.
177
+ */
178
+ include: String,
179
+
180
+ /**
181
+ * A comma-separated list of fields to be excluded from the generated grid and the generated editor.
182
+ *
183
+ * When [`include`](#/elements/vaadin-crud#property-include) is defined, this parameter is ignored.
184
+ *
185
+ * Default is to exclude all private fields (those properties starting with underscore)
186
+ */
187
+ exclude: String,
188
+
189
+ /**
190
+ * Reflects the opened status of the editor.
191
+ */
192
+ editorOpened: {
193
+ type: Boolean,
194
+ reflectToAttribute: true,
195
+ notify: true,
196
+ observer: '__editorOpenedChanged',
197
+ },
198
+
199
+ /**
200
+ * Number of items in the data set which is reported by the grid.
201
+ * Typically it reflects the number of filtered items displayed in the grid.
202
+ *
203
+ * Note: As with `<vaadin-grid>`, this property updates automatically only
204
+ * if `items` is used for data.
205
+ */
206
+ size: {
207
+ type: Number,
208
+ readOnly: true,
209
+ notify: true,
210
+ },
211
+
212
+ /**
213
+ * Controls visibility state of toolbar.
214
+ * When set to false toolbar is hidden and shown when set to true.
215
+ * @attr {boolean} no-toolbar
216
+ */
217
+ noToolbar: {
218
+ type: Boolean,
219
+ value: false,
220
+ reflectToAttribute: true,
221
+ },
222
+
223
+ /**
224
+ * The object used to localize this component.
225
+ * For changing the default localization, change the entire
226
+ * _i18n_ object or just the property you want to modify.
227
+ *
228
+ * The object has the following JSON structure and default values:
229
+ *
230
+ * ```
231
+ * {
232
+ * newItem: 'New item',
233
+ * editItem: 'Edit item',
234
+ * saveItem: 'Save',
235
+ * cancel: 'Cancel',
236
+ * deleteItem: 'Delete...',
237
+ * editLabel: 'Edit',
238
+ * confirm: {
239
+ * delete: {
240
+ * title: 'Confirm delete',
241
+ * content: 'Are you sure you want to delete the selected item? This action cannot be undone.',
242
+ * button: {
243
+ * confirm: 'Delete',
244
+ * dismiss: 'Cancel'
245
+ * }
246
+ * },
247
+ * cancel: {
248
+ * title: 'Unsaved changes',
249
+ * content: 'There are unsaved modifications to the item.',
250
+ * button: {
251
+ * confirm: 'Discard',
252
+ * dismiss: 'Continue editing'
253
+ * }
254
+ * }
255
+ * }
256
+ * }
257
+ * ```
258
+ *
259
+ * @type {!CrudI18n}
260
+ * @default {English/US}
261
+ */
262
+ i18n: {
263
+ type: Object,
264
+ value() {
265
+ return {
266
+ newItem: 'New item',
267
+ editItem: 'Edit item',
268
+ saveItem: 'Save',
269
+ cancel: 'Cancel',
270
+ deleteItem: 'Delete...',
271
+ editLabel: 'Edit',
272
+ confirm: {
273
+ delete: {
274
+ title: 'Delete item',
275
+ content: 'Are you sure you want to delete this item? This action cannot be undone.',
276
+ button: {
277
+ confirm: 'Delete',
278
+ dismiss: 'Cancel',
279
+ },
280
+ },
281
+ cancel: {
282
+ title: 'Discard changes',
283
+ content: 'There are unsaved changes to this item.',
284
+ button: {
285
+ confirm: 'Discard',
286
+ dismiss: 'Cancel',
287
+ },
288
+ },
289
+ },
290
+ };
291
+ },
292
+ },
293
+
294
+ /** @private */
295
+ __dialogAriaLabel: String,
296
+
297
+ /** @private */
298
+ __isDirty: Boolean,
299
+
300
+ /** @private */
301
+ __isNew: Boolean,
302
+
303
+ /**
304
+ * @type {boolean}
305
+ * @protected
306
+ */
307
+ _fullscreen: {
308
+ type: Boolean,
309
+ observer: '__fullscreenChanged',
310
+ },
311
+
312
+ /**
313
+ * @type {string}
314
+ * @protected
315
+ */
316
+ _fullscreenMediaQuery: {
317
+ value: '(max-width: 600px), (max-height: 600px)',
318
+ },
319
+ };
320
+ }
321
+
322
+ static get observers() {
323
+ return [
324
+ '__headerPropsChanged(_defaultHeader, __isNew, i18n)',
325
+ '__formPropsChanged(_form, _theme, include, exclude)',
326
+ '__gridPropsChanged(_grid, _theme, include, exclude, noFilter, noHead, noSort, items)',
327
+ '__i18nChanged(i18n, _grid)',
328
+ '__editOnClickChanged(editOnClick, _grid)',
329
+ '__saveButtonPropsChanged(_saveButton, i18n, __isDirty)',
330
+ '__cancelButtonPropsChanged(_cancelButton, i18n)',
331
+ '__deleteButtonPropsChanged(_deleteButton, i18n, __isNew)',
332
+ '__newButtonPropsChanged(_newButton, i18n)',
333
+ ];
334
+ }
335
+
336
+ constructor() {
337
+ super();
338
+
339
+ this.__cancel = this.__cancel.bind(this);
340
+ this.__delete = this.__delete.bind(this);
341
+ this.__save = this.__save.bind(this);
342
+ this.__new = this.__new.bind(this);
343
+ this.__onFormChange = this.__onFormChange.bind(this);
344
+ this.__onGridEdit = this.__onGridEdit.bind(this);
345
+ this.__onGridSizeChanged = this.__onGridSizeChanged.bind(this);
346
+ this.__onGridActiveItemChanged = this.__onGridActiveItemChanged.bind(this);
347
+
348
+ this.__focusRestorationController = new FocusRestorationController();
349
+ }
350
+
351
+ /** @protected */
352
+ get _headerNode() {
353
+ return this._headerController && this._headerController.node;
354
+ }
355
+
356
+ /**
357
+ * A reference to all fields inside the [`_form`](#/elements/vaadin-crud#property-_form) element
358
+ * @return {!Array<!HTMLElement>}
359
+ * @protected
360
+ */
361
+ get _fields() {
362
+ if (!this.__fields || !this.__fields.length) {
363
+ this.__fields = Array.from(this._form.querySelectorAll('*')).filter((e) => e.validate || e.checkValidity);
364
+ }
365
+ return this.__fields;
366
+ }
367
+
368
+ /** @protected */
369
+ ready() {
370
+ super.ready();
371
+
372
+ this.$.dialog.$.overlay.addEventListener('vaadin-overlay-outside-click', this.__cancel);
373
+ this.$.dialog.$.overlay.addEventListener('vaadin-overlay-escape-press', this.__cancel);
374
+
375
+ this._headerController = new SlotController(this, 'header', 'h3', {
376
+ initializer: (node) => {
377
+ this._defaultHeader = node;
378
+ },
379
+ });
380
+ this.addController(this._headerController);
381
+
382
+ this._gridController = new GridSlotController(this);
383
+ this.addController(this._gridController);
384
+
385
+ this.addController(new FormSlotController(this));
386
+
387
+ // Init controllers in `ready()` (not constructor) so that Flow can set `_noDefaultButtons`
388
+ this._newButtonController = new ButtonSlotController(this, 'new', 'primary', this._noDefaultButtons);
389
+ this._saveButtonController = new ButtonSlotController(this, 'save', 'primary', this._noDefaultButtons);
390
+ this._cancelButtonController = new ButtonSlotController(this, 'cancel', 'tertiary', this._noDefaultButtons);
391
+ this._deleteButtonController = new ButtonSlotController(this, 'delete', 'tertiary error', this._noDefaultButtons);
392
+
393
+ this.addController(this._newButtonController);
394
+
395
+ // NOTE: order in which buttons are added should match the order of slots in template
396
+ this.addController(this._saveButtonController);
397
+ this.addController(this._cancelButtonController);
398
+ this.addController(this._deleteButtonController);
399
+
400
+ this.addController(
401
+ new MediaQueryController(this._fullscreenMediaQuery, (matches) => {
402
+ this._fullscreen = matches;
403
+ }),
404
+ );
405
+
406
+ this.addController(this.__focusRestorationController);
407
+ }
408
+
409
+ /**
410
+ * @param {boolean} isDirty
411
+ * @private
412
+ */
413
+ __isSaveBtnDisabled(isDirty) {
414
+ // Used instead of isDirty property binding in order to enable overriding of the behavior
415
+ // by overriding the method (i.e. from Flow component)
416
+ return !isDirty;
417
+ }
418
+
419
+ /**
420
+ * @param {HTMLElement | undefined} headerNode
421
+ * @param {boolean} isNew
422
+ * @param {string} i18nNewItem
423
+ * @param {string} i18nEditItem
424
+ * @private
425
+ */
426
+ __headerPropsChanged(headerNode, isNew, i18n) {
427
+ if (headerNode) {
428
+ headerNode.textContent = isNew ? i18n.newItem : i18n.editItem;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * @param {CrudI18n} i18n
434
+ * @param {CrudGrid | Grid} grid
435
+ * @private
436
+ */
437
+ __i18nChanged(i18n, grid) {
438
+ if (!grid) {
439
+ return;
440
+ }
441
+
442
+ afterNextRender(grid, () => {
443
+ Array.from(grid.querySelectorAll('vaadin-crud-edit-column')).forEach((column) => {
444
+ column.ariaLabel = i18n.editLabel;
445
+ });
446
+ });
447
+ }
448
+
449
+ /** @private */
450
+ __editorPositionChanged(editorPosition) {
451
+ if (isValidEditorPosition(editorPosition)) {
452
+ return;
453
+ }
454
+ this.editorPosition = '';
455
+ }
456
+
457
+ /** @private */
458
+ __editorOpenedChanged(opened, oldOpened) {
459
+ if (!opened && oldOpened) {
460
+ this.__closeEditor();
461
+ } else {
462
+ this.__formChanged(this._form);
463
+ }
464
+
465
+ if (opened) {
466
+ this.__ensureChildren();
467
+
468
+ // When using bottom / aside editor position,
469
+ // auto-focus the editor element on open.
470
+ if (this._form.parentElement === this) {
471
+ this.$.editor.setAttribute('tabindex', '0');
472
+ this.$.editor.focus();
473
+ } else {
474
+ this.$.editor.removeAttribute('tabindex');
475
+ }
476
+ }
477
+
478
+ this.__toggleToolbar();
479
+
480
+ // Make sure to reset scroll position
481
+ this.$.scroller.scrollTop = 0;
482
+ }
483
+
484
+ /** @private */
485
+ __fullscreenChanged(fullscreen, oldFullscreen) {
486
+ if (fullscreen || oldFullscreen) {
487
+ this.__toggleToolbar();
488
+
489
+ this.__ensureChildren();
490
+
491
+ this.toggleAttribute('fullscreen', fullscreen);
492
+ }
493
+ }
494
+
495
+ /** @private */
496
+ __toggleToolbar() {
497
+ // Hide toolbar to give more room for the editor when it's positioned below the grid
498
+ if (this.editorPosition === 'bottom' && !this._fullscreen) {
499
+ this.$.toolbar.style.display = this.editorOpened ? 'none' : '';
500
+ }
501
+ }
502
+
503
+ /** @private */
504
+ __moveChildNodes(target) {
505
+ const nodes = [this._headerNode, this._form, this._saveButton, this._cancelButton, this._deleteButton];
506
+ if (!nodes.every((node) => node instanceof HTMLElement)) {
507
+ return;
508
+ }
509
+
510
+ // Teleport header node, form, and the buttons to corresponding slots.
511
+ // NOTE: order in which buttons are moved matches the order of slots.
512
+ nodes.forEach((node) => {
513
+ target.appendChild(node);
514
+ });
515
+
516
+ // Wait to set label until slotted element has been moved.
517
+ setTimeout(() => {
518
+ this.__dialogAriaLabel = this._headerNode.textContent.trim();
519
+ });
520
+ }
521
+
522
+ /** @private */
523
+ __shouldOpenDialog(fullscreen, editorPosition) {
524
+ return editorPosition === '' || fullscreen;
525
+ }
526
+
527
+ /** @private */
528
+ __ensureChildren() {
529
+ if (this.__shouldOpenDialog(this._fullscreen, this.editorPosition)) {
530
+ // Move form to dialog
531
+ this.__moveChildNodes(this.$.dialog.$.overlay);
532
+ } else {
533
+ // Move form to crud
534
+ this.__moveChildNodes(this);
535
+ }
536
+ }
537
+
538
+ /** @private */
539
+ __computeDialogOpened(opened, fullscreen, editorPosition) {
540
+ // Only open dialog when editorPosition is "" or fullscreen is set
541
+ return this.__shouldOpenDialog(fullscreen, editorPosition) ? opened : false;
542
+ }
543
+
544
+ /** @private */
545
+ __computeEditorHidden(opened, fullscreen, editorPosition) {
546
+ // Only show editor when editorPosition is "bottom" or "aside"
547
+ if (['aside', 'bottom'].includes(editorPosition) && !fullscreen) {
548
+ return !opened;
549
+ }
550
+
551
+ return true;
552
+ }
553
+
554
+ /** @private */
555
+ __onDialogOpened(event) {
556
+ this.editorOpened = event.detail.value;
557
+ }
558
+
559
+ /** @private */
560
+ __onGridEdit(event) {
561
+ event.stopPropagation();
562
+ this.__confirmBeforeChangingEditedItem(event.detail.item);
563
+ }
564
+
565
+ /** @private */
566
+ __onFormChange() {
567
+ this.__isDirty = true;
568
+ }
569
+
570
+ /** @private */
571
+ __onGridSizeChanged() {
572
+ this._setSize(this._grid.size);
573
+ }
574
+
575
+ /**
576
+ * @param {CrudGrid | Grid} grid
577
+ * @param {CrudGrid | Grid | undefined} oldGrid
578
+ * @private
579
+ */
580
+ __gridChanged(grid, oldGrid) {
581
+ if (oldGrid) {
582
+ oldGrid.removeEventListener('edit', this.__onGridEdit);
583
+ oldGrid.removeEventListener('size-changed', this.__onGridSizeChanged);
584
+ }
585
+ if (this.dataProvider) {
586
+ this.__dataProviderChanged(this.dataProvider);
587
+ }
588
+ if (this.editedItem) {
589
+ this.__editedItemChanged(this.editedItem);
590
+ }
591
+ grid.addEventListener('edit', this.__onGridEdit);
592
+ grid.addEventListener('size-changed', this.__onGridSizeChanged);
593
+ this.__onGridSizeChanged();
594
+ }
595
+
596
+ /**
597
+ * @param {HTMLElement | undefined | null} form
598
+ * @param {HTMLElement | undefined | null} oldForm
599
+ * @private
600
+ */
601
+ __formChanged(form, oldForm) {
602
+ if (oldForm && oldForm.parentElement) {
603
+ oldForm.parentElement.removeChild(oldForm);
604
+ oldForm.removeEventListener('change', this.__onFormChange);
605
+ oldForm.removeEventListener('input', this.__onFormChange);
606
+ }
607
+ if (!form) {
608
+ return;
609
+ }
610
+ if (this.items) {
611
+ this.__itemsChanged(this.items);
612
+ }
613
+ if (this.editedItem) {
614
+ this.__editedItemChanged(this.editedItem);
615
+ }
616
+ form.addEventListener('change', this.__onFormChange);
617
+ form.addEventListener('input', this.__onFormChange);
618
+ }
619
+
620
+ /**
621
+ * @param {HTMLElement | undefined} form
622
+ * @param {string} theme
623
+ * @param {string | string[] | undefined} include
624
+ * @param {string | RegExp} exclude
625
+ * @private
626
+ */
627
+ __formPropsChanged(form, theme, include, exclude) {
628
+ if (form) {
629
+ form.include = include;
630
+ form.exclude = exclude;
631
+
632
+ if (theme) {
633
+ form.setAttribute('theme', theme);
634
+ } else {
635
+ form.removeAttribute('theme');
636
+ }
637
+ }
638
+ }
639
+
640
+ /**
641
+ * @param {HTMLElement | undefined} grid
642
+ * @param {string} theme
643
+ * @param {string | string[] | undefined} include
644
+ * @param {string | RegExp} exclude
645
+ * @param {boolean} noFilter
646
+ * @param {boolean} noHead
647
+ * @param {boolean} noSort
648
+ * @param {Array<unknown> | undefined} items
649
+ * @private
650
+ */
651
+ // eslint-disable-next-line @typescript-eslint/max-params
652
+ __gridPropsChanged(grid, theme, include, exclude, noFilter, noHead, noSort, items) {
653
+ if (!grid) {
654
+ return;
655
+ }
656
+
657
+ if (grid === this._gridController.defaultNode) {
658
+ grid.noFilter = noFilter;
659
+ grid.noHead = noHead;
660
+ grid.noSort = noSort;
661
+ grid.include = include;
662
+ grid.exclude = exclude;
663
+
664
+ if (theme) {
665
+ grid.setAttribute('theme', theme);
666
+ } else {
667
+ grid.removeAttribute('theme');
668
+ }
669
+ }
670
+
671
+ grid.items = items;
672
+ }
673
+
674
+ /**
675
+ * @param {HTMLElement | undefined} saveButton
676
+ * @param {string} i18nLabel
677
+ * @param {boolean} isDirty
678
+ * @private
679
+ */
680
+ __saveButtonPropsChanged(saveButton, i18n, isDirty) {
681
+ if (saveButton) {
682
+ saveButton.toggleAttribute('disabled', this.__isSaveBtnDisabled(isDirty));
683
+
684
+ if (saveButton === this._saveButtonController.defaultNode) {
685
+ saveButton.textContent = i18n.saveItem;
686
+ }
687
+ }
688
+ }
689
+
690
+ /**
691
+ * @param {HTMLElement | undefined} deleteButton
692
+ * @param {string} i18nLabel
693
+ * @param {boolean} isNew
694
+ * @private
695
+ */
696
+ __deleteButtonPropsChanged(deleteButton, i18n, isNew) {
697
+ if (deleteButton) {
698
+ deleteButton.toggleAttribute('hidden', isNew);
699
+
700
+ if (deleteButton === this._deleteButtonController.defaultNode) {
701
+ deleteButton.textContent = i18n.deleteItem;
702
+ }
703
+ }
704
+ }
705
+
706
+ /**
707
+ * @param {HTMLElement | undefined} cancelButton
708
+ * @param {string} i18nLabel
709
+ * @private
710
+ */
711
+ __cancelButtonPropsChanged(cancelButton, i18n) {
712
+ if (cancelButton && cancelButton === this._cancelButtonController.defaultNode) {
713
+ cancelButton.textContent = i18n.cancel;
714
+ }
715
+ }
716
+
717
+ /**
718
+ * @param {HTMLElement | undefined} newButton
719
+ * @param {string} i18nNewItem
720
+ * @private
721
+ */
722
+ __newButtonPropsChanged(newButton, i18n) {
723
+ if (newButton && newButton === this._newButtonController.defaultNode) {
724
+ newButton.textContent = i18n.newItem;
725
+ }
726
+ }
727
+
728
+ /** @private */
729
+ __dataProviderChanged(dataProvider) {
730
+ if (this._grid) {
731
+ this._grid.dataProvider = this.__createDataProviderProxy(dataProvider);
732
+ }
733
+ }
734
+
735
+ /** @private */
736
+ __editOnClickChanged(editOnClick, grid) {
737
+ if (!grid) {
738
+ return;
739
+ }
740
+
741
+ grid.hideEditColumn = editOnClick;
742
+
743
+ if (editOnClick) {
744
+ grid.addEventListener('active-item-changed', this.__onGridActiveItemChanged);
745
+ } else {
746
+ grid.removeEventListener('active-item-changed', this.__onGridActiveItemChanged);
747
+ }
748
+ }
749
+
750
+ /** @private */
751
+ __onGridActiveItemChanged(event) {
752
+ const item = event.detail.value;
753
+ if (this.editorOpened && this.__isDirty) {
754
+ this.__confirmBeforeChangingEditedItem(item);
755
+ return;
756
+ }
757
+ if (item) {
758
+ this.__edit(item);
759
+ } else if (!this.__keepOpened) {
760
+ this.__closeEditor();
761
+ }
762
+ }
763
+
764
+ /** @private */
765
+ __confirmBeforeChangingEditedItem(item, keepOpened) {
766
+ if (
767
+ this.editorOpened && // Editor opened
768
+ this.__isDirty && // Form change has been made
769
+ this.editedItem !== item // Item is different
770
+ ) {
771
+ this.$.confirmCancel.opened = true;
772
+ this.addEventListener(
773
+ 'cancel',
774
+ (event) => {
775
+ event.preventDefault(); // Prevent closing the editor
776
+ if (item || keepOpened) {
777
+ this.__edit(item);
778
+ this.__clearItemAndKeepEditorOpened(item, keepOpened);
779
+ } else {
780
+ this.__closeEditor();
781
+ }
782
+ },
783
+ { once: true },
784
+ );
785
+ } else {
786
+ this.__edit(item);
787
+ this.__clearItemAndKeepEditorOpened(item, keepOpened);
788
+ }
789
+ }
790
+
791
+ /** @private */
792
+ __clearItemAndKeepEditorOpened(item, keepOpened) {
793
+ if (!item) {
794
+ setTimeout(() => {
795
+ this.__keepOpened = keepOpened;
796
+ this.editedItem = this._grid.activeItem = undefined;
797
+ });
798
+ }
799
+ }
800
+
801
+ /** @private */
802
+ __createDataProviderProxy(dataProvider) {
803
+ return (params, callback) => {
804
+ const callbackProxy = (chunk, size) => {
805
+ if (chunk && chunk[0]) {
806
+ this.__model = chunk[0];
807
+ }
808
+
809
+ callback(chunk, size);
810
+ };
811
+
812
+ dataProvider(params, callbackProxy);
813
+ };
814
+ }
815
+
816
+ /** @private */
817
+ __itemsChanged(items) {
818
+ if (this.items && this.items[0]) {
819
+ this.__model = items[0];
820
+ }
821
+ }
822
+
823
+ /** @private */
824
+ __editedItemChanged(item) {
825
+ if (!this._form) {
826
+ return;
827
+ }
828
+ if (item) {
829
+ if (!this._fields.length && this._form._configure) {
830
+ if (this.__model) {
831
+ this._form._configure(this.__model);
832
+ } else {
833
+ console.warn(
834
+ '<vaadin-crud> Unable to autoconfigure form because the data structure is unknown. ' +
835
+ 'Either specify `include` or ensure at least one item is available beforehand.',
836
+ );
837
+ }
838
+ }
839
+ this._form.item = item;
840
+ this._fields.forEach((e) => {
841
+ const path = e.path || e.getAttribute('path');
842
+ if (path) {
843
+ e.value = getProperty(path, item);
844
+ }
845
+ });
846
+
847
+ this.__isNew = !!(this.__isNew || (this.items && this.items.indexOf(item) < 0));
848
+ this.editorOpened = true;
849
+ }
850
+ }
851
+
852
+ /** @private */
853
+ __validate() {
854
+ return this._fields.every((e) => (e.validate || e.checkValidity).call(e));
855
+ }
856
+
857
+ /** @private */
858
+ __setHighlightedItem(item) {
859
+ if (this._grid === this._gridController.defaultNode) {
860
+ this._grid.selectedItems = item ? [item] : [];
861
+ }
862
+ }
863
+
864
+ /** @private */
865
+ __closeEditor() {
866
+ this.editorOpened = false;
867
+ this.__isDirty = false;
868
+ this.__setHighlightedItem(null);
869
+
870
+ // Delay changing the item in order not to modify editor while closing
871
+ setTimeout(() => this.__clearItemAndKeepEditorOpened(null, false));
872
+ }
873
+
874
+ /** @private */
875
+ __new() {
876
+ this.__confirmBeforeChangingEditedItem(null, true);
877
+ }
878
+
879
+ /** @private */
880
+ __edit(item) {
881
+ if (this.editedItem === item) {
882
+ return;
883
+ }
884
+ this.__setHighlightedItem(item);
885
+ this.__openEditor(item);
886
+ }
887
+
888
+ /** @private */
889
+ __fireEvent(type, item) {
890
+ const event = new CustomEvent(type, { detail: { item }, cancelable: true });
891
+ this.dispatchEvent(event);
892
+ return event.defaultPrevented === false;
893
+ }
894
+
895
+ /** @private */
896
+ __openEditor(item) {
897
+ this.__focusRestorationController.saveFocus();
898
+
899
+ this.__isDirty = false;
900
+ this.__isNew = !item;
901
+ const result = this.__fireEvent(this.__isNew ? 'new' : 'edit', item);
902
+ if (result) {
903
+ this.editedItem = item || {};
904
+ } else {
905
+ this.editorOpened = true;
906
+ }
907
+ }
908
+
909
+ /** @private */
910
+ __restoreFocusOnDelete() {
911
+ if (this._grid._flatSize === 1) {
912
+ this._newButton.focus();
913
+ } else {
914
+ this._grid._focusFirstVisibleRow();
915
+ }
916
+ }
917
+
918
+ /** @private */
919
+ __restoreFocusOnSaveOrCancel() {
920
+ const focusNode = this.__focusRestorationController.focusNode;
921
+ const row = this._grid._getRowContainingNode(focusNode);
922
+ if (!row) {
923
+ this.__focusRestorationController.restoreFocus();
924
+ return;
925
+ }
926
+
927
+ if (this._grid._isItemAssignedToRow(this.editedItem, row) && this._grid._isInViewport(row)) {
928
+ this.__focusRestorationController.restoreFocus();
929
+ } else {
930
+ this._grid._focusFirstVisibleRow();
931
+ }
932
+ }
933
+
934
+ /** @private */
935
+ __save() {
936
+ if (!this.__validate()) {
937
+ return;
938
+ }
939
+
940
+ const item = { ...this.editedItem };
941
+ this._fields.forEach((e) => {
942
+ const path = e.path || e.getAttribute('path');
943
+ if (path) {
944
+ setProperty(path, e.value, item);
945
+ }
946
+ });
947
+ const result = this.__fireEvent('save', item);
948
+ if (result) {
949
+ if (this.__isNew && !this.dataProvider) {
950
+ if (!this.items) {
951
+ this.items = [item];
952
+ } else {
953
+ this.items.push(item);
954
+ }
955
+ } else {
956
+ if (!this.editedItem) {
957
+ this.editedItem = {};
958
+ }
959
+ Object.assign(this.editedItem, item);
960
+ }
961
+
962
+ this.__restoreFocusOnSaveOrCancel();
963
+ this._grid.clearCache();
964
+ this.__closeEditor();
965
+ }
966
+ }
967
+
968
+ /** @private */
969
+ __cancel() {
970
+ if (this.__isDirty) {
971
+ this.$.confirmCancel.opened = true;
972
+ } else {
973
+ this.__confirmCancel();
974
+ }
975
+ }
976
+
977
+ /** @private */
978
+ __confirmCancel() {
979
+ const result = this.__fireEvent('cancel', this.editedItem);
980
+ if (result) {
981
+ this.__restoreFocusOnSaveOrCancel();
982
+ this.__closeEditor();
983
+ }
984
+ }
985
+
986
+ /** @private */
987
+ __delete() {
988
+ this.$.confirmDelete.opened = true;
989
+ }
990
+
991
+ /** @private */
992
+ __confirmDelete() {
993
+ const result = this.__fireEvent('delete', this.editedItem);
994
+ if (result) {
995
+ if (this.items && this.items.indexOf(this.editedItem) >= 0) {
996
+ this.items.splice(this.items.indexOf(this.editedItem), 1);
997
+ }
998
+
999
+ this.__restoreFocusOnDelete();
1000
+ this._grid.clearCache();
1001
+ this.__closeEditor();
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * Fired when user wants to edit an existing item. If the default is prevented, then
1007
+ * a new item is not assigned to the form, giving that responsibility to the app, though
1008
+ * dialog is always opened.
1009
+ *
1010
+ * @event edit
1011
+ * @param {Object} detail.item the item to edit
1012
+ */
1013
+
1014
+ /**
1015
+ * Fired when user wants to create a new item.
1016
+ *
1017
+ * @event new
1018
+ */
1019
+
1020
+ /**
1021
+ * Fired when user wants to delete item. If the default is prevented, then
1022
+ * no action is performed, items array is not modified nor dialog closed
1023
+ *
1024
+ * @event delete
1025
+ * @param {Object} detail.item the item to delete
1026
+ */
1027
+
1028
+ /**
1029
+ * Fired when user discards edition. If the default is prevented, then
1030
+ * no action is performed, user is responsible to close dialog and reset
1031
+ * item and grid.
1032
+ *
1033
+ * @event cancel
1034
+ * @param {Object} detail.item the item to delete
1035
+ */
1036
+
1037
+ /**
1038
+ * Fired when user wants to save a new or an existing item. If the default is prevented, then
1039
+ * no action is performed, items array is not modified nor dialog closed
1040
+ *
1041
+ * @event save
1042
+ * @param {Object} detail.item the item to save
1043
+ * @param {Object} detail.new whether the item is a new one
1044
+ */
1045
+ };