UNPKG

@blinkk/editor

Version:

Structured content editor with live previews.

388 lines (386 loc) 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteMediaField = exports.MediaField = exports.VALID_VIDEO_MIME_TYPES = exports.VALID_IMAGE_MIME_TYPES = exports.EXT_TO_MIME_TYPE = exports.DEFAULT_EXTRA_KEY = void 0; const selective_edit_1 = require("@blinkk/selective-edit"); const events_1 = require("../events"); const preview_1 = require("@blinkk/selective-edit/dist/src/utility/preview"); const lodash_merge_1 = __importDefault(require("lodash.merge")); const math_1 = require("../../utility/math"); const template_1 = require("../template"); exports.DEFAULT_EXTRA_KEY = 'extra'; exports.EXT_TO_MIME_TYPE = { apng: 'image/apng', avif: 'image/avif', gif: 'image/gif', jpeg: 'image/jpg', jpg: 'image/jpg', mp4: 'image/mp4', mov: 'image/mov', png: 'image/png', svg: 'image/svg+xml', webm: 'image/webm', webp: 'image/webp', }; exports.VALID_IMAGE_MIME_TYPES = [ 'image/apng', 'image/avif', 'image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp', ]; exports.VALID_VIDEO_MIME_TYPES = ['image/mp4', 'image/mov', 'image/webm']; class MediaField extends selective_edit_1.DroppableMixin(selective_edit_1.Field) { constructor(types, config, globalConfig, fieldType = 'media') { super(types, config, globalConfig, fieldType); this.config = config; this.globalConfig = globalConfig; this.droppableUi.validTypes = this.config.accepted || [ ...exports.VALID_IMAGE_MIME_TYPES, ...exports.VALID_VIDEO_MIME_TYPES, ]; this.droppableUi.listeners.add('files', this.handleFiles.bind(this)); this.zoneToKey = { path: 'path', label: 'label', }; } ensureGroup() { if (!this.group && this.config.fields) { this.group = this.types.fields.newFromKey('group', this.types, { type: 'group', key: this.config.extraKey || exports.DEFAULT_EXTRA_KEY, fields: this.config.fields, label: this.config.extraLabel || this.globalConfig.labels.fieldMediaExtra || 'Extra', isExpanded: this.config.isExpanded, previewFields: this.config.previewFields, }, this.globalConfig); } } /** * Handle when the accessibility label changes value. * * @param evt Input event from changing value. */ handleA11yLabel(evt) { const target = evt.target; this.currentValue = lodash_merge_1.default({}, this.currentValue || {}, { _meta: this.meta, label: target.value, }); this.render(); } handleFiles(files) { this.isProcessing = true; this.render(); // Uploads only the first file. this.uploadFile(files[0]).then(file => { this.currentValue = lodash_merge_1.default({}, this.currentValue || {}, { path: file.path, url: file.url, }); this.showFileUpload = false; this.isProcessing = false; // Updating the current value does not change the input value. const inputField = document.querySelector(`#media-${this.uid}`); if (inputField) { inputField.value = file.url; } this.render(); }); } /** * Handle when the input changes value. * * @param evt Input event from changing value. */ handleInput(evt) { const target = evt.target; this.currentValue = lodash_merge_1.default({}, this.currentValue || {}, { _meta: this.meta, path: target.value, }); // Reset the url if it is loaded. this.currentValue.url = undefined; this.render(); } /** * Handle when the input changes value. * * @param evt Input event from changing value. */ handleFileUpload(evt) { const input = evt.target; if (!input.files) { return; } this.handleFiles([input.files[0]]); this.render(); } handlePreviewMediaLoad(evt) { const target = evt.target; this.meta = { height: target.naturalHeight, width: target.naturalWidth, }; this.currentValue = lodash_merge_1.default({}, this.currentValue || {}, { _meta: this.meta, }); this.render(); } handlePreviewVideoLoad(evt) { const target = evt.target; this.meta = { height: target.videoHeight, width: target.videoWidth, }; this.currentValue = lodash_merge_1.default({}, this.currentValue || {}, { _meta: this.meta, }); this.render(); } get isClean() { if (!this.group) { return super.isClean; } return super.isClean && this.group.isClean; } get isSimple() { // Media field has multiple inputs and is considered complex. return false; } get isValid() { if (!this.group) { return super.isValid; } return super.isValid && this.group.isValid; } /** * Retrieve the url for previewing the field. */ get previewUrl() { const value = this.currentValue || {}; if (value && value.url) { return value.url; } if (value && value.path) { if (value.path.startsWith('http:') || value.path.startsWith('https:') || value.path.startsWith('//')) { return value.path; } } // TODO: Use api to get the preview url for the file path. return undefined; } // eslint-disable-next-line @typescript-eslint/no-unused-vars templateAltLabel(editor, data) { return selective_edit_1.html `<div class="selective__media__a11y_label"> <div class="selective__media__section__label"> <label for="media-a11y-label-${this.uid}"> ${this.globalConfig.labels.fieldMediaLabel || 'Media accessibility label'} </label> </div> <div class=${selective_edit_1.classMap(this.classesForInput('label'))}> <input type="text" id="media-a11y-label-${this.uid}" @input=${this.handleA11yLabel.bind(this)} value=${this.currentValue?.label || ''} /> </div> ${this.templateErrors(editor, data, 'label')} </div>`; } templateFileUpload( // eslint-disable-next-line @typescript-eslint/no-unused-vars editor, // eslint-disable-next-line @typescript-eslint/no-unused-vars data) { if (!this.showFileUpload) { return selective_edit_1.html ``; } return selective_edit_1.html `<div class="selective__media__upload"> <input type="file" accept=${[...exports.VALID_IMAGE_MIME_TYPES, ...exports.VALID_VIDEO_MIME_TYPES].join(',')} id="media-file-${this.uid}" @input=${this.handleFileUpload.bind(this)} /> </div>`; } templateInput(editor, data) { const value = this.currentValue || {}; const actions = []; this.ensureGroup(); actions.push(selective_edit_1.html `<div class="selective__action" @click=${() => { this.showFileUpload = true; document.addEventListener(events_1.EVENT_RENDER_COMPLETE, () => { document.querySelector(`#media-file-${this.uid}`).click(); }, { once: true, }); this.render(); }} > <i class="material-icons">upload</i> </div>`); return selective_edit_1.html `${this.templateHelp(editor, data)} <div class="selective__droppable__target" @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__path"> <div class="selective__media__section__label"> <label for="media-${this.uid}" >${this.globalConfig.labels.fieldMediaPath || 'Media path'}</label > </div> <div class="selective__media__path__input"> <div class=${selective_edit_1.classMap(this.classesForInput('path'))}> <input type="text" id="media-${this.uid}" placeholder=${this.config.placeholder || ''} @input=${this.handleInput.bind(this)} value=${value.url || ''} /> </div> ${this.isProcessing ? selective_edit_1.html `${template_1.templateLoading(editor, { padHorizontal: true })}` : ''} <div class="selective__field__actions">${actions}</div> </div> </div> ${this.templateFileUpload(editor, data)} ${this.templatePreview(editor, data)} ${this.templateErrors(editor, data, 'path')} </div> ${this.templateAltLabel(editor, data)} ${this.group?.template(editor, data) || ''}`; } templatePreview(editor, data) { const url = this.previewUrl; if (!url) { return selective_edit_1.html ``; } return selective_edit_1.html `<div class="selective__media__preview"> <div class="selective__media__preview_media"> <div class="selective__media__section__label"> ${this.globalConfig.labels.fieldMediaPreview || 'Media preview'} </div> ${this.templatePreviewMedia(editor, data)} </div> <div class="selective__media__meta"> ${this.templatePreviewMeta(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. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars templatePreviewValue(editor, data, index) { return selective_edit_1.html `${preview_1.findPreviewValue(this.value, [], `{ Media ${index !== undefined ? index + 1 : ''} }`)}`; } templatePreviewMedia( // eslint-disable-next-line @typescript-eslint/no-unused-vars editor, // eslint-disable-next-line @typescript-eslint/no-unused-vars data) { const url = this.previewUrl; if (!url) { return selective_edit_1.html `<span class="material-icons">broken_image</span>`; } for (const fileExt of Object.keys(exports.EXT_TO_MIME_TYPE)) { const extMimeType = exports.EXT_TO_MIME_TYPE[fileExt]; const isVideoFile = exports.VALID_VIDEO_MIME_TYPES.includes(extMimeType); if (isVideoFile && url.endsWith(`.${fileExt}`)) { return selective_edit_1.html `<video data-serving-path=${url} @loadeddata=${this.handlePreviewVideoLoad.bind(this)} playsinline disableremoteplayback muted autoplay loop > <source src="${url}" /> </video>`; } } return selective_edit_1.html `<img data-serving-path=${url} @load=${this.handlePreviewMediaLoad.bind(this)} src="${url}" />`; } templatePreviewMeta( // eslint-disable-next-line @typescript-eslint/no-unused-vars editor, // eslint-disable-next-line @typescript-eslint/no-unused-vars data) { if (!this.meta) { if (!this.currentValue?._meta) { return [selective_edit_1.html ``]; } this.meta = this.currentValue._meta; } const metaInfo = []; metaInfo.push(selective_edit_1.html `<div class="selective__media__meta__item selective__media__meta__size" > <span class="selective__media__meta__label">Size:</span> <span class="selective__media__meta__value" >${this.meta?.width} x ${this.meta?.height}</span > </div>`); const ratio = math_1.reduceFraction(this.meta?.width || 1, this.meta?.height || 1); metaInfo.push(selective_edit_1.html `<div class="selective__media__meta__item selective__media__meta__ratio" > <span class="selective__media__meta__label">Ratio:</span> <span class="selective__media__meta__value">${ratio[0]}:${ratio[1]}</span> </div>`); return metaInfo; } async uploadFile(uploadFile) { return this.globalConfig.api.uploadFile(uploadFile, this.globalConfig.state.project?.media?.options); } /** * Get the value for the field, optionally including the extra values. */ get value() { const extraValue = {}; if (this.group) { extraValue[this.config.extraKey || exports.DEFAULT_EXTRA_KEY] = this.group.value; } return lodash_merge_1.default({}, this.currentValue || {}, extraValue); } } exports.MediaField = MediaField; class RemoteMediaField extends MediaField { constructor(types, config, globalConfig, fieldType = 'remoteMedia') { super(types, config, globalConfig, fieldType); } async uploadFile(uploadFile) { return this.globalConfig.api.uploadFile(uploadFile, this.globalConfig.state.project?.media?.remote); } } exports.RemoteMediaField = RemoteMediaField; //# sourceMappingURL=media.js.map