@vaadin/crud 22.0.0-rc1 → 23.0.0-alpha2

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.
@@ -8,13 +8,14 @@ import '@vaadin/button/src/vaadin-button.js';
8
8
  import '@vaadin/dialog/src/vaadin-dialog.js';
9
9
  import '@vaadin/confirm-dialog/src/vaadin-confirm-dialog.js';
10
10
  import '@vaadin/vaadin-license-checker/vaadin-license-checker.js';
11
- import './vaadin-dialog-layout.js';
11
+ import './vaadin-crud-dialog.js';
12
12
  import './vaadin-crud-grid.js';
13
13
  import './vaadin-crud-form.js';
14
14
  import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
15
15
  import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
16
16
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
17
17
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
18
+ import { SlotMixin } from '@vaadin/component-base/src/slot-mixin.js';
18
19
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
19
20
 
20
21
  const HOST_PROPS = {
@@ -148,9 +149,10 @@ const HOST_PROPS = {
148
149
  *
149
150
  * @extends HTMLElement
150
151
  * @mixes ElementMixin
152
+ * @mixes SlotMixin
151
153
  * @mixes ThemableMixin
152
154
  */
153
- class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
155
+ class Crud extends SlotMixin(ElementMixin(ThemableMixin(PolymerElement))) {
154
156
  static get template() {
155
157
  return html`
156
158
  <style>
@@ -174,7 +176,8 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
174
176
  height: 100%;
175
177
  }
176
178
 
177
- :host([hidden]) {
179
+ :host([hidden]),
180
+ [hidden] {
178
181
  display: none !important;
179
182
  }
180
183
 
@@ -197,6 +200,39 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
197
200
  :host([editor-position='bottom']) #container {
198
201
  flex-direction: column;
199
202
  }
203
+
204
+ [part='editor'] {
205
+ z-index: 1;
206
+ display: flex;
207
+ flex-direction: column;
208
+ height: 100%;
209
+ }
210
+
211
+ :host(:not([editor-position=''])[editor-opened]:not([fullscreen])) [part='editor'] {
212
+ flex: 1 0 100%;
213
+ }
214
+
215
+ :host([editor-position='bottom'][editor-opened]:not([fullscreen])) [part='editor'] {
216
+ max-height: var(--vaadin-crud-editor-max-height);
217
+ }
218
+
219
+ :host([editor-position='aside'][editor-opened]:not([fullscreen])) [part='editor'] {
220
+ min-width: 300px;
221
+ max-width: var(--vaadin-crud-editor-max-width);
222
+ }
223
+
224
+ [part='scroller'] {
225
+ display: flex;
226
+ flex-direction: column;
227
+ overflow: auto;
228
+ flex: auto;
229
+ }
230
+
231
+ [part='footer'] {
232
+ display: flex;
233
+ flex: none;
234
+ flex-direction: row-reverse;
235
+ }
200
236
  </style>
201
237
 
202
238
  <div id="container">
@@ -220,49 +256,33 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
220
256
  </slot>
221
257
  </div>
222
258
  </div>
223
- <vaadin-dialog-layout
224
- theme$="[[theme]]"
225
- form="[[_form]]"
226
- save-button="[[_saveButton]]"
227
- cancel-button="[[_cancelButton]]"
228
- delete-button="[[_deleteButton]]"
229
- id="dialog"
230
- no-close-on-outside-click="[[__isDirty]]"
231
- no-close-on-esc="[[__isDirty]]"
232
- opened="{{editorOpened}}"
233
- editor-position$="{{editorPosition}}"
234
- mobile="[[__mobile]]"
235
- theme="crud"
236
- >
237
- <h3 slot="header">[[__computeEditorHeader(__isNew, i18n.newItem, i18n.editItem)]]</h3>
238
- <div id="editor">
239
- <slot name="form">
240
- <vaadin-crud-form
241
- theme$="[[theme]]"
242
- id="form"
243
- include="[[include]]"
244
- exclude="[[exclude]]"
245
- ></vaadin-crud-form>
246
- </slot>
259
+
260
+ <div id="editor" part="editor" hidden$="[[__computeEditorHidden(editorOpened, _fullscreen, editorPosition)]]">
261
+ <div part="scroller" id="scroller" role="group" aria-labelledby="header">
262
+ <div part="header" id="header">
263
+ <slot name="header"></slot>
264
+ </div>
265
+ <slot name="form"></slot>
247
266
  </div>
248
267
 
249
- <slot name="save-button">
250
- <vaadin-button id="save" on-click="__save" theme="primary" disabled$="[[__isSaveBtnDisabled(__isDirty)]]">
251
- [[i18n.saveItem]]
252
- </vaadin-button>
253
- </slot>
254
- <slot name="cancel-button">
255
- <vaadin-button id="cancel" on-click="__cancelBound" theme="tertiary">[[i18n.cancel]]</vaadin-button>
256
- </slot>
257
- <div slot="footer" style="flex: auto;"></div>
258
- <slot name="delete-button">
259
- <vaadin-button id="delete" on-click="__delete" theme="tertiary error" hidden$="[[__isNew]]"
260
- >[[i18n.deleteItem]]</vaadin-button
261
- >
262
- </slot>
263
- </vaadin-dialog-layout>
268
+ <div part="footer" role="toolbar">
269
+ <slot name="save-button"></slot>
270
+ <slot name="cancel-button"></slot>
271
+ <slot name="delete-button"></slot>
272
+ </div>
273
+ </div>
264
274
  </div>
265
275
 
276
+ <vaadin-crud-dialog
277
+ id="dialog"
278
+ opened="[[__computeDialogOpened(editorOpened, _fullscreen, editorPosition)]]"
279
+ aria-label="[[__editorAriaLabel]]"
280
+ no-close-on-outside-click="[[__isDirty]]"
281
+ no-close-on-esc="[[__isDirty]]"
282
+ theme$="[[theme]]"
283
+ on-opened-changed="__onDialogOpened"
284
+ ></vaadin-crud-dialog>
285
+
266
286
  <vaadin-confirm-dialog
267
287
  theme$="[[theme]]"
268
288
  id="confirmCancel"
@@ -274,6 +294,7 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
274
294
  message="[[i18n.confirm.cancel.content]]"
275
295
  confirm-theme="primary"
276
296
  ></vaadin-confirm-dialog>
297
+
277
298
  <vaadin-confirm-dialog
278
299
  theme$="[[theme]]"
279
300
  id="confirmDelete"
@@ -286,7 +307,7 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
286
307
  confirm-theme="primary error"
287
308
  ></vaadin-confirm-dialog>
288
309
 
289
- <iron-media-query query="[[__mobileMediaQuery]]" query-matches="{{__mobile}}"></iron-media-query>
310
+ <iron-media-query query="[[_fullscreenMediaQuery]]" query-matches="{{_fullscreen}}"></iron-media-query>
290
311
  `;
291
312
  }
292
313
 
@@ -341,6 +362,14 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
341
362
  observer: '__onCancelButtonChange'
342
363
  },
343
364
 
365
+ /**
366
+ * A reference to the editor header element will be teleported to the dialog.
367
+ * @private
368
+ */
369
+ _headerNode: {
370
+ type: HTMLElement
371
+ },
372
+
344
373
  /**
345
374
  * An array containing the items which will be stamped to the column template instances.
346
375
  * @type {Array<unknown> | undefined}
@@ -450,6 +479,7 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
450
479
  */
451
480
  editorOpened: {
452
481
  type: Boolean,
482
+ reflectToAttribute: true,
453
483
  notify: true,
454
484
  observer: '__onOpenedChanged'
455
485
  },
@@ -465,7 +495,7 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
465
495
  },
466
496
 
467
497
  /**
468
- * Controls visiblity state of toolbar.
498
+ * Controls visibility state of toolbar.
469
499
  * When set to false toolbar is hidden and shown when set to true.
470
500
  * @attr {boolean} no-toolbar
471
501
  */
@@ -546,27 +576,38 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
546
576
  }
547
577
  },
548
578
 
579
+ /** @private */
580
+ __editorAriaLabel: String,
581
+
549
582
  /** @private */
550
583
  __isDirty: Boolean,
551
584
 
552
585
  /** @private */
553
586
  __isNew: Boolean,
554
587
 
555
- /** @private */
556
- __mobileMediaQuery: {
557
- value: '(max-width: 600px), (max-height: 600px)'
588
+ /**
589
+ * @type {boolean}
590
+ * @protected
591
+ */
592
+ _fullscreen: {
593
+ type: Boolean,
594
+ observer: '__fullscreenChanged'
558
595
  },
559
596
 
560
- /** @private */
561
- __mobile: {
562
- type: Boolean,
563
- observer: '__mobileChanged'
597
+ /**
598
+ * @type {string}
599
+ * @protected
600
+ */
601
+ _fullscreenMediaQuery: {
602
+ value: '(max-width: 600px), (max-height: 600px)'
564
603
  }
565
604
  };
566
605
  }
567
606
 
568
607
  static get observers() {
569
608
  return [
609
+ '__headerNodeChanged(_headerNode, __isNew, i18n.newItem, i18n.editItem)',
610
+ '__formChanged(_form, theme, include, exclude)',
570
611
  '__onI18Change(i18n, _grid)',
571
612
  '__onEditOnClickChange(editOnClick, _grid)',
572
613
  '__hostPropsChanged(' +
@@ -590,6 +631,38 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
590
631
  }
591
632
  }
592
633
 
634
+ /** @protected */
635
+ get slots() {
636
+ // NOTE: order in which the toolbar buttons are listed matters.
637
+ return {
638
+ ...super.slots,
639
+ header: () => {
640
+ return document.createElement('h3');
641
+ },
642
+ form: () => {
643
+ return document.createElement('vaadin-crud-form');
644
+ },
645
+ 'save-button': () => {
646
+ const button = document.createElement('vaadin-button');
647
+ button.id = 'save';
648
+ button.setAttribute('theme', 'primary');
649
+ return button;
650
+ },
651
+ 'cancel-button': () => {
652
+ const button = document.createElement('vaadin-button');
653
+ button.id = 'cancel';
654
+ button.setAttribute('theme', 'tertiary');
655
+ return button;
656
+ },
657
+ 'delete-button': () => {
658
+ const button = document.createElement('vaadin-button');
659
+ button.id = 'delete';
660
+ button.setAttribute('theme', 'tertiary error');
661
+ return button;
662
+ }
663
+ };
664
+ }
665
+
593
666
  constructor() {
594
667
  super();
595
668
  this._observer = new FlattenedNodesObserver(this, (info) => {
@@ -608,24 +681,33 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
608
681
  this.__gridSizeListener = this.__onGridSizeChanges.bind(this);
609
682
  this.__boundEditOnClickListener = this.__editOnClickListener.bind(this);
610
683
  this._grid = this.$.grid;
611
- this._form = this.$.form;
612
- this._saveButton = this.$.save;
613
- this._deleteButton = this.$.delete;
614
- this._cancelButton = this.$.cancel;
615
- this.$.dialog.$.dialog.$.overlay.addEventListener('vaadin-overlay-outside-click', this.__cancelBound);
616
- this.$.dialog.$.dialog.$.overlay.addEventListener('vaadin-overlay-escape-press', this.__cancelBound);
684
+ this.$.dialog.$.overlay.addEventListener('vaadin-overlay-outside-click', this.__cancelBound);
685
+ this.$.dialog.$.overlay.addEventListener('vaadin-overlay-escape-press', this.__cancelBound);
686
+ // Initialize the default buttons
687
+ this.__propagateHostAttributes();
617
688
  }
618
689
 
619
690
  /** @private */
620
691
  __isSaveBtnDisabled(isDirty) {
621
- // Used instead of isDirty proprety binding in order to enable overriding of the behaviour
692
+ // Used instead of isDirty property binding in order to enable overriding of the behavior
622
693
  // by overriding the method (i.e. from Flow component)
623
694
  return !isDirty;
624
695
  }
625
696
 
626
697
  /** @private */
627
- __computeEditorHeader(isNew, newItem, editItem) {
628
- return isNew ? newItem : editItem;
698
+ __headerNodeChanged(headerNode, isNew, newItem, editItem) {
699
+ if (headerNode) {
700
+ headerNode.textContent = isNew ? newItem : editItem;
701
+ }
702
+ }
703
+
704
+ /** @private */
705
+ __formChanged(form, theme, include, exclude) {
706
+ if (form) {
707
+ form.include = include;
708
+ form.exclude = exclude;
709
+ form.setAttribute('theme', theme);
710
+ }
629
711
  }
630
712
 
631
713
  /** @private */
@@ -662,26 +744,99 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
662
744
  this.__onFormChange(this._form);
663
745
  }
664
746
 
747
+ if (opened) {
748
+ this.__ensureChildren();
749
+ }
750
+
665
751
  this.__toggleToolbar();
752
+
753
+ // Make sure to reset scroll position
754
+ this.$.scroller.scrollTop = 0;
666
755
  }
667
756
 
668
757
  /** @private */
669
- __mobileChanged() {
670
- this.__toggleToolbar();
758
+ __fullscreenChanged(fullscreen, oldFullscreen) {
759
+ if (fullscreen || oldFullscreen) {
760
+ this.__toggleToolbar();
761
+
762
+ this.__ensureChildren();
763
+
764
+ this.toggleAttribute('fullscreen', fullscreen);
765
+ this.$.dialog.$.overlay.toggleAttribute('fullscreen', fullscreen);
766
+ }
671
767
  }
672
768
 
673
769
  /** @private */
674
770
  __toggleToolbar() {
675
771
  // Hide toolbar to give more room for the editor when it's positioned below the grid
676
- if (this.editorPosition === 'bottom' && !this.__mobile) {
772
+ if (this.editorPosition === 'bottom' && !this._fullscreen) {
677
773
  this.$.toolbar.style.display = this.editorOpened ? 'none' : '';
678
774
  }
679
775
  }
680
776
 
681
777
  /** @private */
682
- __onDomChange(nodes) {
778
+ __moveChildNodes(target) {
779
+ const nodes = [this._headerNode, this._form, this._saveButton, this._cancelButton, this._deleteButton];
780
+ if (!nodes.every((node) => node instanceof HTMLElement)) {
781
+ return;
782
+ }
783
+
784
+ // Teleport header node, form, and the buttons to corresponding slots.
785
+ // NOTE: order in which buttons are moved matches the order of slots.
683
786
  nodes.forEach((node) => {
684
- if (node.getAttribute) {
787
+ target.appendChild(node);
788
+ });
789
+
790
+ // Wait to set label until slotted element has been moved.
791
+ setTimeout(() => {
792
+ this.__dialogAriaLabel = this._headerNode.textContent.trim();
793
+ });
794
+ }
795
+
796
+ /** @private */
797
+ __shouldOpenDialog(fullscreen, editorPosition) {
798
+ return editorPosition === '' || fullscreen;
799
+ }
800
+
801
+ /** @private */
802
+ __ensureChildren() {
803
+ if (this.__shouldOpenDialog(this._fullscreen, this.editorPosition)) {
804
+ // Move form to dialog
805
+ this.__moveChildNodes(this.$.dialog.$.overlay);
806
+ } else {
807
+ // Move form to crud
808
+ this.__moveChildNodes(this);
809
+ }
810
+ }
811
+
812
+ /** @private */
813
+ __computeDialogOpened(opened, fullscreen, editorPosition) {
814
+ // Only open dialog when editorPosition is "" or fullscreen is set
815
+ return this.__shouldOpenDialog(fullscreen, editorPosition) ? opened : false;
816
+ }
817
+
818
+ /** @private */
819
+ __computeEditorHidden(opened, fullscreen, editorPosition) {
820
+ // Only show editor when editorPosition is "bottom" or "aside"
821
+ if (['aside', 'bottom'].includes(editorPosition) && !fullscreen) {
822
+ return !opened;
823
+ }
824
+
825
+ return true;
826
+ }
827
+
828
+ /** @private */
829
+ __onDialogOpened(event) {
830
+ this.editorOpened = event.detail.value;
831
+ }
832
+
833
+ /** @private */
834
+ __onDomChange(addedNodes) {
835
+ // TODO: restore default button when a corresponding slotted button is removed.
836
+ // Consider creating a controller to reuse custom helper logic from FieldMixin.
837
+ addedNodes
838
+ .filter((node) => node.nodeType === Node.ELEMENT_NODE)
839
+ .forEach((node) => {
685
840
  const slotAttributeValue = node.getAttribute('slot');
686
841
  if (!slotAttributeValue) {
687
842
  return;
@@ -697,9 +852,10 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
697
852
  } else if (slotAttributeValue.indexOf('button') >= 0) {
698
853
  const [button] = slotAttributeValue.split('-');
699
854
  this[`_${button}Button`] = node;
855
+ } else if (slotAttributeValue == 'header') {
856
+ this._headerNode = node;
700
857
  }
701
- }
702
- });
858
+ });
703
859
  }
704
860
 
705
861
  /** @private */
@@ -776,9 +932,7 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
776
932
  currentButton.parentElement.removeChild(currentButton);
777
933
  }
778
934
 
779
- if (slottedButton.parentElement === this) {
780
- slottedButton.addEventListener('click', clickListener);
781
- }
935
+ slottedButton.addEventListener('click', clickListener);
782
936
  }
783
937
 
784
938
  /** @private */
@@ -795,6 +949,8 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
795
949
 
796
950
  /** @private */
797
951
  __propagateHostAttributesToButton(button, props) {
952
+ // Ensure the slotted button element is present in the DOM.
953
+ // This is needed because the observer runs before `ready`.
798
954
  if (button) {
799
955
  props.forEach(({ attr, prop, parseProp }) => {
800
956
  if (prop.indexOf('i18n') >= 0) {
@@ -1017,7 +1173,7 @@ class Crud extends ElementMixin(ThemableMixin(PolymerElement)) {
1017
1173
  return;
1018
1174
  }
1019
1175
 
1020
- const item = Object.assign({}, this.editedItem);
1176
+ const item = { ...this.editedItem };
1021
1177
  this._fields.forEach((e) => {
1022
1178
  const path = e.path || e.getAttribute('path');
1023
1179
  path && this.__set(path, e.value, item);