UNPKG

@blinkk/selective-edit

Version:
654 lines 24.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ListFieldItem = exports.ListField = void 0; const deepObject_1 = require("../../utility/deepObject"); const field_1 = require("../field"); const sortable_1 = require("../../mixins/sortable"); const lit_html_1 = require("lit-html"); const actions_1 = require("../../utility/actions"); const mixins_1 = require("../../mixins"); const dataType_1 = require("../../utility/dataType"); const events_1 = require("../events"); const uuid_1 = require("../../mixins/uuid"); const class_map_js_1 = require("lit-html/directives/class-map.js"); const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep")); const repeat_js_1 = require("lit-html/directives/repeat.js"); const json_stable_stringify_1 = __importDefault(require("json-stable-stringify")); class ListField extends (0, sortable_1.SortableMixin)(field_1.Field) { constructor(types, config, globalConfig, fieldType = 'list') { super(types, config, globalConfig, fieldType); this.config = config; this.items = null; this.usingAutoFields = false; this.ListItemCls = ListFieldItem; this.sortableUi.listeners.add('sort', this.handleSort.bind(this)); } get allowAdd() { // Check if validation rules allow for adding more items. const value = this.value; for (const rule of this.rules.getRulesForZone()) { if (!rule.allowAdd(value)) { return false; } } return true; } get allowRemove() { // Check if validation rules allow for removing items. const value = this.value; for (const rule of this.rules.getRulesForZone()) { if (!rule.allowRemove(value)) { return false; } } return true; } /** * Does the list allow for showing simple fields? */ get allowSimple() { return !this.config.isComplex; } createFields(fieldConfigs) { return new this.types.globals.FieldsCls(this.types, { // Field configs should not be 'shared'. Duplicate the field configs // before creating the fields. fields: (0, lodash_clonedeep_1.default)(fieldConfigs), isGuessed: this.usingAutoFields, parentKey: this.fullKey, previewField: this.config.previewField, previewFields: this.config.previewFields, previewType: this.config.previewType, }, this.globalConfig); } ensureItems(editor) { // Cannot initialize items without valid data format. if (!this.isDataFormatValid) { return []; } if (this.items === null) { this.items = []; let fieldConfigs = this.config.fields || []; // Add list items for each of the values in the list already. for (const value of this.originalValue || []) { // If no field configs, auto guess based on first row with a value. if (fieldConfigs.length === 0) { this.usingAutoFields = true; // Auto-guess fields based on the first item in the list. const autoFields = new this.types.globals.AutoFieldsCls({}); fieldConfigs = autoFields.guessFields(value); // Store the the auto-guessed configs for new list items. this.config.fields = fieldConfigs; } const fields = this.createFields(fieldConfigs); // When an item is not expanded it does not get the value // updated correctly so we need to manually call the data update. fields.updateOriginal(editor, value); for (const field of fields.fields) { field.updateOriginal(editor, (0, deepObject_1.autoDeepObject)(value || fields.guessDefaultValue())); } this.items.push(new this.ListItemCls(this, fields)); } } return this.items; } handleAddItem(evt, editor, data) { const items = this.ensureItems(editor); const fieldConfigs = this.config.fields || []; const fields = this.createFields(fieldConfigs); // When an item is not expanded it does not get the value // updated correctly so we need to manually call the data update. fields.updateOriginal(editor, data); for (const field of fields.fields) { field.updateOriginal(editor, (0, deepObject_1.autoDeepObject)(fields.guessDefaultValue())); } const newItem = new this.ListItemCls(this, fields); newItem.isExpanded = true; items.push(newItem); this.render(); } handleDeleteItem(evt, index) { const items = this.items || []; // Prevent the delete from bubbling. evt.stopPropagation(); // Remove the value at the index. items.splice(index, 1); // Lock the fields to prevent the values from being updated at the same // time as the original value. const downstreamItems = items.slice(index); for (const item of downstreamItems) { item.fields.lock(); } this.lock(); // Unlock fields after saving is complete to let the values be updated // when clean. // TODO: Automate this unlock without having to be done manually. document.addEventListener(events_1.EVENT_UNLOCK, () => { for (const item of downstreamItems) { item.fields.unlock(); } this.unlock(); this.render(); }, { once: true }); this.render(); } handleSort(startIndex, endIndex) { // Rework the arrays to have the items in the correct position. const newListItems = []; const oldListItems = this.items || []; const maxIndex = Math.max(endIndex, startIndex); const minIndex = Math.min(endIndex, startIndex); // Did not move, don't need to sort. if (startIndex === endIndex) { return; } // Determine which direction to shift misplaced items. let modifier = 1; if (startIndex > endIndex) { modifier = -1; } for (let i = 0; i < oldListItems.length; i++) { if (i < minIndex || i > maxIndex) { // Leave in the same order. newListItems[i] = oldListItems[i]; // Lock the fields to prevent the values from being updated at the same // time as the original value. newListItems[i].fields.lock(); } else if (i === endIndex) { // This element is being moved to, place the moved value here. newListItems[i] = oldListItems[startIndex]; // Lock the fields to prevent the values from being updated at the same // time as the original value. newListItems[i].fields.lock(); } else { // Shift the old index using the modifier to determine direction. newListItems[i] = oldListItems[i + modifier]; // Lock the fields to prevent the values from being updated at the same // time as the original value. newListItems[i].fields.lock(); } } this.items = newListItems; this.lock(); // Unlock fields after saving is complete to let the values be updated when clean. document.addEventListener(events_1.EVENT_UNLOCK, () => { for (const item of newListItems) { item.fields.unlock(); } this.unlock(); this.render(); }, { once: true }); // Check if sorted back to original value. if (this.items.length === this.originalValue.length) { let isSame = true; for (let i = 0; i < this.items.length; i++) { if ((0, json_stable_stringify_1.default)(this.items[i].fields.value) !== (0, json_stable_stringify_1.default)(this.originalValue[i])) { isSame = false; break; } } // If the list items are the same across new values and original then it // has been sorted back to original and needs to be unlocked. if (isSame) { this.unlock(); for (const item of newListItems) { item.fields.unlock(); } } } this.render(); } get isClean() { // If there are no items, nothing has changed. if (this.items === null) { return true; } // When locked, the field is automatically considered dirty. if (this.isLocked) { return false; } // Check for a change in length. if (this.items !== null && this.originalValue && this.originalValue.length !== this.items.length) { return false; } // Check if all of the items are clean. for (const item of this.items || []) { if (!item.fields.isClean) { return false; } } return true; } /** * Check if the data format is invalid for what the field expects to edit. */ get isDataFormatValid() { if (this.originalValue === undefined || this.originalValue === null) { return true; } return dataType_1.DataType.isArray(this.originalValue); } get isValid() { // If there are no items, nothing has changed. if (this.items === null) { return true; } for (const item of this.items) { if (!item.fields.isValid) { return false; } } return true; } /** * Length of the list. */ get length() { return this.items?.length || 0; } templateEmpty(editor, data, index) { return (0, lit_html_1.html) ` <div class="selective__list__item selective__list__item--empty" data-index=${index} > ${this.config.emptyLabel || 'No items in list'} </div>`; } templateFooter(editor, data) { if (!this.allowAdd) { return (0, lit_html_1.html) ``; } return (0, lit_html_1.html) `<div class="selective__field__actions"> <button class="selective__action selective__action--add" @click=${(evt) => { this.handleAddItem(evt, editor, data); }} > <span>${this.config.addLabel || 'Add'}</span> </button> </div>`; } // eslint-disable-next-line @typescript-eslint/no-unused-vars templateHeader(editor, data) { const items = this.ensureItems(editor); if (!items.length) { return (0, lit_html_1.html) ``; } const actions = []; // Determine the ability to show simple fields. let areSimpleFields = this.allowSimple; // Determine the expanded/collapsed state for the item. let areAllExpanded = true; let areAllCollapsed = true; for (const item of items) { if (!item.fields.isSimple || !item.fields.allowSimple) { areSimpleFields = false; } if (!item.isExpanded) { areAllExpanded = false; } if (item.isExpanded) { areAllCollapsed = false; } } // Do not show the expand/collapse for simplet fields. if (areSimpleFields) { return (0, lit_html_1.html) ``; } const handleExpandAll = () => { for (const item of items) { item.isExpanded = true; } this.render(); }; actions.push((0, lit_html_1.html) `<div ?disabled=${areAllExpanded} class="selective__action selective__action--expand selective__tooltip--bottom-left" data-tip="Expand all" @click=${handleExpandAll} > <i class="material-icons">unfold_more</i> </div>`); const handleCollapseAll = () => { for (const item of items) { item.isExpanded = false; } this.render(); }; actions.push((0, lit_html_1.html) `<div ?disabled=${areAllCollapsed} class="selective__action selective__action--collapse selective__tooltip--bottom-left" data-tip="Collapse all" @click=${handleCollapseAll} > <i class="material-icons">unfold_less</i> </div>`); return (0, lit_html_1.html) `<div class="selective__field__actions">${actions}</div>`; } templateInput(editor, data) { return (0, lit_html_1.html) `${this.templateHelp(editor, data)} <div class="selective__list"> ${(0, repeat_js_1.repeat)(this.items || [], item => item.uid, (item, index) => { const itemValue = new deepObject_1.DeepObject(index < this.originalValue?.length || 0 ? this.originalValue[index] : item.fields.guessDefaultValue()); return item.template(editor, itemValue, index); })} ${this.items?.length ? '' : this.templateEmpty(editor, data, 0)} </div> ${this.templateErrors(editor, data)}`; } get value() { // Return the original value if the items have never been initialized. if (this.items === null) { return this.originalValue || []; } const value = []; for (const item of this.items) { value.push(item.fields.value); } return value; } } exports.ListField = ListField; class ListFieldItem extends (0, uuid_1.UuidMixin)(mixins_1.Base) { constructor(listField, fields) { super(); this.listField = listField; this.fields = fields; this.isExpanded = false; } actionsCollapsedPre(editor, data, index) { const canDrag = this.listField.length > 1; const actions = new actions_1.Actions({ modifier: 'pre', }); if (canDrag) { actions.add((0, lit_html_1.html) `<div class="selective__list__item__drag"> <i class="material-icons">drag_indicator</i> </div>`); } if (index !== undefined) { actions.add((0, lit_html_1.html) `<div class="selective__list__item__index"> ${index + 1} </div>`); } return actions; } actionsCollapsedPost(editor, data, index) { const actions = new actions_1.Actions({ modifier: 'post', }); actions.add(this.templateRemove(editor, data, index)); return actions; } actionsExpandedPre(editor, data, index) { const actions = new actions_1.Actions({ modifier: 'pre', }); if (index !== undefined) { actions.add((0, lit_html_1.html) `<div class="selective__list__item__index"> ${index + 1} </div>`); } return actions; } actionsExpandedPost( // eslint-disable-next-line @typescript-eslint/no-unused-vars editor, // eslint-disable-next-line @typescript-eslint/no-unused-vars data, // eslint-disable-next-line @typescript-eslint/no-unused-vars index) { const actions = new actions_1.Actions({ modifier: 'post', }); actions.add((0, lit_html_1.html) `<span class="material-icons">keyboard_arrow_down</span>`); return actions; } actionsSimplePre(editor, data, index) { const canDrag = this.listField.length > 1; const actions = new actions_1.Actions({ modifier: 'pre', }); if (canDrag) { actions.add((0, lit_html_1.html) `<div class="selective__list__item__drag"> <i class="material-icons">drag_indicator</i> </div>`); } if (index !== undefined) { actions.add((0, lit_html_1.html) `<div class="selective__list__item__index"> ${index + 1} </div>`); } return actions; } actionsSimplePost(editor, data, index) { const actions = new actions_1.Actions({ modifier: 'post', }); actions.add(this.templateRemove(editor, data, index)); return actions; } classesCollpased( // eslint-disable-next-line @typescript-eslint/no-unused-vars editor, // eslint-disable-next-line @typescript-eslint/no-unused-vars data, // eslint-disable-next-line @typescript-eslint/no-unused-vars index) { return { selective__list__item: true, 'selective__list__item--collapsed': true, 'selective__list__item--no-drag': this.listField.length <= 1, selective__sortable: true, }; } classesExpanded( // eslint-disable-next-line @typescript-eslint/no-unused-vars editor, // eslint-disable-next-line @typescript-eslint/no-unused-vars data, // eslint-disable-next-line @typescript-eslint/no-unused-vars index) { return { selective__list__item: true, 'selective__list__item--expanded': true, selective__sortable: true, }; } classesSimple( // eslint-disable-next-line @typescript-eslint/no-unused-vars editor, // eslint-disable-next-line @typescript-eslint/no-unused-vars data, // eslint-disable-next-line @typescript-eslint/no-unused-vars index) { return { selective__list__item: true, 'selective__list__item--expanded': true, selective__sortable: true, }; } handleCollapseItem() { this.isExpanded = false; this.listField.render(); } handleExpandItem() { this.isExpanded = true; this.listField.render(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars handleHoverOffItem(evt, index) { // Do nothing. } // eslint-disable-next-line @typescript-eslint/no-unused-vars handleHoverOnItem(evt, index) { // Do nothing. } template(editor, data, index) { if (this.listField.allowSimple && // The list field allows for simple fields. this.fields.allowSimple && // The fields allows for simple fields. this.fields.isSimple // The fields are simple. ) { return this.templateSimple(editor, data, index); } else if (this.isExpanded) { return this.templateExpanded(editor, data, index); } return this.templateCollapsed(editor, data, index); } templateCollapsed(editor, data, index) { // Need to update the original value on the collapsed items. this.fields.updateOriginal(editor, data, true); const canDrag = this.listField.length > 1; const sortable = this.listField.sortableUi; return (0, lit_html_1.html) ` <div class=${(0, class_map_js_1.classMap)(this.classesCollpased(editor, data, index))} draggable=${canDrag && sortable.canDrag ? 'true' : 'false'} data-index=${index} data-item-uid=${this.uid} @dragenter=${sortable.handleDragEnter.bind(sortable)} @dragleave=${sortable.handleDragLeave.bind(sortable)} @dragover=${sortable.handleDragOver.bind(sortable)} @dragstart=${sortable.handleDragStart.bind(sortable)} @drop=${sortable.handleDrop.bind(sortable)} @focusin=${(evt) => { sortable.handleFocusIn(evt); this.listField.render(); }} @focusout=${(evt) => { sortable.handleFocusOut(evt); this.listField.render(); }} @mouseenter=${(evt) => { this.handleHoverOnItem(evt, index); }} @mouseleave=${(evt) => { this.handleHoverOffItem(evt, index); }} > ${this.actionsCollapsedPre(editor, data, index).template()} <div class="selective__list__item__preview" data-item-uid=${this.uid} @click=${this.handleExpandItem.bind(this)} > ${this.templatePreviewValue(editor, data, index)} </div> ${this.actionsCollapsedPost(editor, data, index).template()} </div>`; } templateExpanded(editor, data, index) { const canDrag = this.listField.length > 1; const sortable = this.listField.sortableUi; return (0, lit_html_1.html) ` <div class=${(0, class_map_js_1.classMap)(this.classesExpanded(editor, data, index))} draggable=${canDrag && sortable.canDrag ? 'true' : 'false'} data-index=${index} data-item-uid=${this.uid} @dragenter=${sortable.handleDragEnter.bind(sortable)} @dragleave=${sortable.handleDragLeave.bind(sortable)} @dragover=${sortable.handleDragOver.bind(sortable)} @dragstart=${sortable.handleDragStart.bind(sortable)} @drop=${sortable.handleDrop.bind(sortable)} @focusin=${(evt) => { sortable.handleFocusIn(evt); this.listField.render(); }} @focusout=${(evt) => { sortable.handleFocusOut(evt); this.listField.render(); }} @mouseenter=${(evt) => { this.handleHoverOnItem(evt, index); }} @mouseleave=${(evt) => { this.handleHoverOffItem(evt, index); }} > <div class="selective__list__fields__header" @click=${this.handleCollapseItem.bind(this)} > ${this.actionsExpandedPre(editor, data, index).template()} <div class="selective__list__item__preview"> ${this.templatePreviewValue(editor, data, index)} </div> ${this.actionsExpandedPost(editor, data, index).template()} </div> <div class="selective__list__fields"> ${this.fields.template(editor, data)} </div> </div>`; } /** * Template for how to render a preview. * * @param editor Selective editor used to render the template. * @param data Data provided to render the template. */ templatePreviewValue(editor, data, index) { return this.fields.templatePreviewValue(editor, data, index); } templateRemove(editor, data, index) { if (!this.listField.allowRemove) { return (0, lit_html_1.html) ``; } return (0, lit_html_1.html) `<div class="selective__action selective__action--delete selective__tooltip--left" data-item-uid=${this.uid} @click=${(evt) => { this.listField.handleDeleteItem(evt, index); }} aria-label="Delete item" data-tip="Delete item" > <i class="material-icons icon icon--delete">remove_circle</i> </div>`; } templateSimple(editor, data, index) { const canDrag = this.listField.length > 1; const sortable = this.listField.sortableUi; return (0, lit_html_1.html) ` <div class=${(0, class_map_js_1.classMap)(this.classesSimple(editor, data, index))} draggable=${canDrag && sortable.canDrag ? 'true' : 'false'} data-index=${index} data-item-uid=${this.uid} @dragenter=${sortable.handleDragEnter.bind(sortable)} @dragleave=${sortable.handleDragLeave.bind(sortable)} @dragover=${sortable.handleDragOver.bind(sortable)} @dragstart=${sortable.handleDragStart.bind(sortable)} @drop=${sortable.handleDrop.bind(sortable)} @focusin=${(evt) => { sortable.handleFocusIn(evt); this.listField.render(); }} @focusout=${(evt) => { sortable.handleFocusOut(evt); this.listField.render(); }} @mouseenter=${(evt) => { this.handleHoverOnItem(evt, index); }} @mouseleave=${(evt) => { this.handleHoverOffItem(evt, index); }} > ${this.actionsSimplePre(editor, data, index).template()} ${this.fields.template(editor, data)} ${this.actionsSimplePost(editor, data, index).template()} </div>`; } } exports.ListFieldItem = ListFieldItem; //# sourceMappingURL=list.js.map