UNPKG

@blinkk/editor

Version:

Structured content editor with live previews.

381 lines 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteMediaListField = exports.MediaListField = void 0; const selective_edit_1 = require("@blinkk/selective-edit"); const media_1 = require("./media"); const events_1 = require("@blinkk/selective-edit/dist/src/selective/events"); const DEFAULT_FIELD_CONFIG = { type: 'media', key: '', label: 'Media', }; const DEFAULT_REMOTE_FIELD_CONFIG = { type: 'remoteMedia', key: '', label: 'Media', }; class MediaListField extends selective_edit_1.DroppableMixin(selective_edit_1.SortableMixin(selective_edit_1.Field)) { constructor(types, config, globalConfig, fieldType = 'mediaList') { super(types, config, globalConfig, fieldType); this.config = config; this.globalConfig = globalConfig; this.items = null; this.usingAutoFields = false; this.MediaListItemCls = MediaListFieldItem; this.MediaListItemDefaultConfig = DEFAULT_FIELD_CONFIG; this.sortableUi.listeners.add('sort', this.handleSort.bind(this)); this.droppableUi.validTypes = this.config.fieldConfig?.accepted || [ ...media_1.VALID_IMAGE_MIME_TYPES, ...media_1.VALID_VIDEO_MIME_TYPES, ]; this.droppableUi.listeners.add('files', this.handleFiles.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; } ensureItems(editor) { if (this.items === null) { this.items = []; const fieldConfig = this.config.fieldConfig || this.MediaListItemDefaultConfig; // Add list items for each of the values in the list already. for (const value of this.originalValue || []) { const newField = this.types.fields.newFromKey(fieldConfig.type, this.types, fieldConfig, this.globalConfig); newField.updateOriginal(editor, selective_edit_1.autoDeepObject(value)); this.items.push(new this.MediaListItemCls(this, newField)); } } return this.items; } expandItem(item) { for (const otherItem of this.items || []) { otherItem.isExpanded = false; } item.isExpanded = true; this.render(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars handleAddItem(evt, editor, data) { const items = this.ensureItems(editor); const fieldConfig = this.config.fieldConfig || DEFAULT_FIELD_CONFIG; const newField = this.types.fields.newFromKey(fieldConfig.type, this.types, fieldConfig, this.globalConfig); newField.updateOriginal(editor, new selective_edit_1.DeepObject()); const newItem = new this.MediaListItemCls(this, newField); items.push(newItem); this.expandItem(newItem); } 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 array issues when the original value // is updated next render. const downstreamItems = items.slice(index); for (const item of downstreamItems) { item.mediaField.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.mediaField.unlock(); } this.unlock(); this.render(); }, { once: true }); this.render(); } handleFiles(files) { // Create a new item for each file uploaded. const items = this.items || []; for (const file of files) { const fieldConfig = this.config.fieldConfig || DEFAULT_FIELD_CONFIG; const newField = this.types.fields.newFromKey(fieldConfig.type, this.types, fieldConfig, this.globalConfig); const newItem = new this.MediaListItemCls(this, newField); items.push(newItem); newField.handleFiles([file]); } 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); // 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].mediaField.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].mediaField.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].mediaField.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.mediaField.unlock(); } this.unlock(); this.render(); }, { once: true }); 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.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.mediaField.isClean) { return false; } } return true; } get isValid() { // If there are no items, nothing has changed. if (this.items === null) { return true; } for (const item of this.items) { if (!item.mediaField.isValid) { return false; } } return true; } /** * Length of the list. */ get length() { return this.items?.length || 0; } templateAdd(editor, data) { if (!this.allowAdd) { return selective_edit_1.html ``; } return selective_edit_1.html `<div class="selective__media_list__add selective__droppable__target" @click=${(evt) => { this.handleAddItem(evt, editor, data); }} @dragenter=${this.droppableUi.handleDragEnter.bind(this.droppableUi)} @dragleave=${this.droppableUi.handleDragLeave.bind(this.droppableUi)} @dragover=${this.droppableUi.handleDragOver.bind(this.droppableUi)} @drop=${this.droppableUi.handleDrop.bind(this.droppableUi)} > <div class="selective__media_list__add__icon"> <span class="material-icons">image</span> </div> <div class="selective__media_list__add__label"> ${this.config.addLabel || 'Add media'} </div> </div>`; } // eslint-disable-next-line @typescript-eslint/no-unused-vars templateEmpty(editor, data) { if (this.length > 0 || this.allowAdd) { return selective_edit_1.html ``; } return selective_edit_1.html ` <div class="selective__media_list__item selective__media_list__item--empty" data-index="0" > ${this.config.emptyLabel || 'No items in list'} </div>`; } templateInput(editor, data) { const items = this.ensureItems(editor); return selective_edit_1.html `${this.templateHelp(editor, data)} <div class="selective__media_list"> ${selective_edit_1.repeat(items || [], item => item.uid, (item, index) => { const itemValue = new selective_edit_1.DeepObject(index < this.originalValue?.length || 0 ? this.originalValue[index] : 'TODO: Default value?'); return item.template(editor, itemValue, index); })} ${this.templateEmpty(editor, data)}${this.templateAdd(editor, data)} </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.mediaField.value); } return value; } } exports.MediaListField = MediaListField; class MediaListFieldItem extends selective_edit_1.UuidMixin(selective_edit_1.Base) { constructor(listField, mediaField) { super(); this.listField = listField; this.mediaField = mediaField; this.isExpanded = false; } handleCollapseItem() { this.isExpanded = false; this.listField.render(); } handleExpandItem() { this.listField.expandItem(this); } template(editor, data, index) { 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.mediaField.updateOriginal(editor, data); const canDrag = this.listField.length > 1; const sortable = this.listField.sortableUi; const droppable = this.mediaField.droppableUi; const postActions = []; postActions.push(this.templateRemove(editor, data, index)); return selective_edit_1.html ` <div class=${selective_edit_1.classMap({ selective__droppable__target: true, selective__media_list__item: true, 'selective__media_list__item--collapsed': true, 'selective__media_list__item--no-drag': this.listField.length <= 1, selective__sortable: true, })} draggable=${canDrag ? 'true' : 'false'} data-index=${index} @dragenter=${(evt) => { sortable.handleDragEnter(evt); droppable.handleDragEnter(evt); }} @dragleave=${(evt) => { sortable.handleDragLeave(evt); droppable.handleDragLeave(evt); }} @dragover=${(evt) => { sortable.handleDragOver(evt); droppable.handleDragOver(evt); }} @dragstart=${sortable.handleDragStart.bind(sortable)} @drop=${(evt) => { sortable.handleDrop(evt); droppable.handleDrop(evt); }} > <div class="selective__media_list__item__preview" data-item-uid=${this.uid} @click=${this.handleExpandItem.bind(this)} > ${this.mediaField.templatePreviewMedia(editor, data)} </div> <div class="selective__field__actions selective__field__actions--post"> ${postActions} </div> </div>`; } templateExpanded(editor, data, index) { const sortable = this.listField.sortableUi; return selective_edit_1.html ` <div class="selective__media_list__item selective__media_list__item--expanded selective__sortable" data-index=${index} @dragenter=${sortable.handleDragEnter.bind(sortable)} @dragleave=${sortable.handleDragLeave.bind(sortable)} @dragover=${sortable.handleDragOver.bind(sortable)} @drop=${sortable.handleDrop.bind(sortable)} > <div class="selective__media_list__fields__header" @click=${this.handleCollapseItem.bind(this)} > <span class="material-icons">keyboard_arrow_down</span> ${this.mediaField.templatePreviewValue(editor, data, index)} </div> <div class="selective__media_list__fields"> ${this.mediaField.template(editor, data)} </div> </div>`; } templateRemove(editor, data, index) { if (!this.listField.allowRemove) { return selective_edit_1.html ``; } return selective_edit_1.html `<div class="selective__action selective__action--delete selective__tooltip--bottom-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>`; } } class RemoteMediaListField extends MediaListField { constructor(types, config, globalConfig, fieldType = 'remoteMediaList') { super(types, config, globalConfig, fieldType); this.MediaListItemDefaultConfig = DEFAULT_REMOTE_FIELD_CONFIG; } } exports.RemoteMediaListField = RemoteMediaListField; //# sourceMappingURL=mediaList.js.map