@blinkk/editor
Version:
Structured content editor with live previews.
388 lines (386 loc) • 13.9 kB
JavaScript
"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}"
=${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}"
=${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"
=${() => {
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"
=${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__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 || ''}
=${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}
=${this.handlePreviewVideoLoad.bind(this)}
playsinline
disableremoteplayback
muted
autoplay
loop
>
<source src="${url}" />
</video>`;
}
}
return selective_edit_1.html `<img
data-serving-path=${url}
=${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