@blinkk/editor
Version:
Structured content editor with live previews.
381 lines • 14.6 kB
JavaScript
"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"
=${(evt) => {
this.handleAddItem(evt, editor, data);
}}
=${this.droppableUi.handleDragEnter.bind(this.droppableUi)}
=${this.droppableUi.handleDragLeave.bind(this.droppableUi)}
=${this.droppableUi.handleDragOver.bind(this.droppableUi)}
=${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}
=${(evt) => {
sortable.handleDragEnter(evt);
droppable.handleDragEnter(evt);
}}
=${(evt) => {
sortable.handleDragLeave(evt);
droppable.handleDragLeave(evt);
}}
=${(evt) => {
sortable.handleDragOver(evt);
droppable.handleDragOver(evt);
}}
=${sortable.handleDragStart.bind(sortable)}
=${(evt) => {
sortable.handleDrop(evt);
droppable.handleDrop(evt);
}}
>
<div
class="selective__media_list__item__preview"
data-item-uid=${this.uid}
=${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}
=${sortable.handleDragEnter.bind(sortable)}
=${sortable.handleDragLeave.bind(sortable)}
=${sortable.handleDragOver.bind(sortable)}
=${sortable.handleDrop.bind(sortable)}
>
<div
class="selective__media_list__fields__header"
=${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}
=${(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