UNPKG

@blinkk/editor

Version:

Structured content editor with live previews.

529 lines 22.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SitePart = void 0; const selective_edit_1 = require("@blinkk/selective-edit"); const modal_1 = require("../../ui/modal"); const filter_1 = require("../../../utility/filter"); const index_1 = require("./index"); const events_1 = require("../../events"); const lodash_merge_1 = __importDefault(require("lodash.merge")); const selective_edit_2 = require("@blinkk/selective-edit"); const template_1 = require("../../template"); const DEFAULT_SITE_FILTER = { includes: [/\.(yaml|yml|html|md)$/], excludes: [/\/[_.]/], }; const MODAL_KEY_COPY = 'menu_file_copy'; const MODAL_KEY_DELETE = 'menu_file_delete'; const MODAL_KEY_NEW = 'menu_file_new'; const STORAGE_FILE_EXPANDED = 'live.menu.site.expandedDirs'; class SitePart extends index_1.MenuSectionPart { constructor(config) { super(config); // Recreate the file structure whenever the files are reloaded. this.config.state.addListener('getFiles', () => { this.fileStructure = undefined; this.render(); }); } classesForPart() { const classes = super.classesForPart(); classes.le__part__menu__site = true; return classes; } getOrCreateModalCopy(editor) { if (!editor.parts.modals.modals[MODAL_KEY_COPY]) { const selectiveConfig = lodash_merge_1.default({}, editor.config.selectiveConfig); const modal = new modal_1.FormDialogModal({ title: 'Copy file', selectiveConfig: selectiveConfig, }); modal.templateModal = this.templateFileCopy.bind(this); modal.actions.push({ label: 'Copy file', level: modal_1.DialogActionLevel.Primary, isDisabledFunc: () => { return modal.isProcessing || !modal.selective.isValid; }, isSubmit: true, onClick: () => { const value = modal.selective.value; modal.startProcessing(); this.config.state.copyFile(value.originalPath, value.path, (newFile) => { // Log the success to the notifications. editor.parts.notifications.addInfo({ message: `New '${newFile.path}' file successfully created.`, actions: [ { label: 'Load file', customEvent: events_1.EVENT_FILE_LOAD, details: newFile, }, ], }); // Reset the data for the next time the form is shown. modal.data = new selective_edit_1.DeepObject(); modal.stopProcessing(true); }, (error) => { // Log the error to the notifications. editor.parts.notifications.addError(error, true); modal.error = error; modal.stopProcessing(); }); }, }); modal.addCancelAction(); editor.parts.modals.modals[MODAL_KEY_COPY] = modal; } return editor.parts.modals.modals[MODAL_KEY_COPY]; } getOrCreateModalDelete(editor) { if (!editor.parts.modals.modals[MODAL_KEY_DELETE]) { const selectiveConfig = lodash_merge_1.default({}, editor.config.selectiveConfig); const modal = new modal_1.FormDialogModal({ title: 'Delete file', selectiveConfig: selectiveConfig, }); modal.templateModal = this.templateFileDelete.bind(this); modal.actions.push({ label: 'Delete file', level: modal_1.DialogActionLevel.Extreme, isDisabledFunc: () => false, isSubmit: true, onClick: () => { const path = modal.data.get('path'); modal.startProcessing(); this.config.state.deleteFile({ path: path, }, () => { // Log the success to the notifications. editor.parts.notifications.addInfo({ message: `Deleted '${path}' file successfully.`, }); // Reset the data for the next time the form is shown. modal.data = new selective_edit_1.DeepObject(); modal.stopProcessing(true); }, (error) => { // Log the error to the notifications. editor.parts.notifications.addError(error, true); modal.error = error; modal.stopProcessing(); }); }, }); modal.addCancelAction(); editor.parts.modals.modals[MODAL_KEY_DELETE] = modal; } return editor.parts.modals.modals[MODAL_KEY_DELETE]; } getOrCreateModalNew(editor) { if (!editor.parts.modals.modals[MODAL_KEY_NEW]) { const selectiveConfig = lodash_merge_1.default({}, editor.config.selectiveConfig); const modal = new modal_1.FormDialogModal({ title: 'New file', selectiveConfig: selectiveConfig, }); modal.templateModal = this.templateFileNew.bind(this); modal.actions.push({ label: 'Create file', level: modal_1.DialogActionLevel.Primary, isDisabledFunc: () => { return modal.isProcessing || !modal.selective.isValid; }, isSubmit: true, onClick: () => { const value = modal.selective.value; modal.startProcessing(); this.config.state.createFile(`${value.directory}${value.path}`, (newFile) => { // Log the success to the notifications. editor.parts.notifications.addInfo({ message: `New '${newFile.path}' file successfully created.`, actions: [ { label: 'Load file', customEvent: events_1.EVENT_FILE_LOAD, details: newFile, }, ], }); // Reset the data for the next time the form is shown. modal.data = new selective_edit_1.DeepObject(); modal.stopProcessing(true); }, (error) => { // Log the error to the notifications. editor.parts.notifications.addError(error, true); modal.error = error; modal.stopProcessing(); }); }, }); modal.addCancelAction(); editor.parts.modals.modals[MODAL_KEY_NEW] = modal; } return editor.parts.modals.modals[MODAL_KEY_NEW]; } loadFiles() { this.config.state.getFiles(() => { this.fileStructure = undefined; this.render(); }); } loadProject() { this.config.state.getProject(); } templateContent(editor) { const project = this.config.state.project; const files = this.config.state.files; // Lazy load the project. if (!project) { this.loadProject(); } // Lazy load the files. if (files === undefined) { this.loadFiles(); } if (!project || files === undefined) { return template_1.templateLoading(editor, { pad: true, }); } if (files.length === 0) { return selective_edit_1.html `<div class="le__part__menu__section__content"> <div class="le__list"> <div class="le__list__item"> <div class="le__list__item__label">No files found.</div> </div> </div> </div>`; } if (!this.fileStructure) { const eventHandlers = { fileCopy: (evt, file) => { evt.stopPropagation(); const modal = this.getOrCreateModalCopy(editor); modal.data.set('originalPath', file.path); // TODO: Modify the new path so it is not automatically an error. modal.data.set('path', file.path); // Make the form field custom to the file being copied. modal.selective.resetFields(); modal.selective.fields.addField({ type: 'text', key: 'path', label: 'File path', help: `Copy '${file.path}' file to this new file.`, validation: [ { type: 'require', message: 'File name is required.', }, { type: 'pattern', pattern: '^[a-z0-9-_./]*$', message: 'File name can only contain lowercase alpha-numeric characters, . (period), _ (underscore), / (forward slash), and - (dash).', }, { type: 'pattern', pattern: '/[a-z0-9]+[a-z0-9-_./]*$', message: 'File name in the sub directory needs to start with alpha-numeric characters.', }, { type: 'pattern', pattern: '^/content/[a-z0-9]+/', message: 'File name needs to be in a collection (ex: /content/pages/).', }, // TODO: Extension matching. // { // type: 'pattern', // pattern: `^.*\.(${originalExt})$`, // message: `File name needs to end with ".${originalExt}" to match the original file.`, // }, { type: 'match', excluded: { values: [file.path], message: 'Cannot copy to the same file.', }, }, // TODO: Existing file match checking. // { // type: 'match', // level: 'warning', // excluded: { // values: otherPodPaths, // message: // 'File name already exists. Copying will overwrite the existing file.', // }, // }, ], }); modal.show(); }, fileDelete: (evt, file) => { evt.stopPropagation(); const modal = this.getOrCreateModalDelete(editor); modal.data.set('path', file.path); modal.show(); }, fileLoad: (evt, file) => { evt.stopPropagation(); document.dispatchEvent(new CustomEvent(events_1.EVENT_FILE_LOAD, { detail: file, })); }, fileNew: (evt, directory) => { evt.stopPropagation(); const modal = this.getOrCreateModalNew(editor); modal.data.set('directory', directory); modal.selective.resetFields(); modal.selective.fields.addField({ type: 'text', key: 'path', label: 'File name', help: `Creating new file in the '${directory}' directory. File name may also be used in the url.`, validation: [ { type: 'require', message: 'File name is required.', }, { type: 'pattern', pattern: '^[a-z0-9-_./]*$', message: 'File name can only contain lowercase alpha-numeric characters, . (period), _ (underscore), / (forward slash), and - (dash).', }, { type: 'pattern', pattern: '^[a-z0-9]+', message: 'File name needs to start with an alpha-numeric character.', }, ], }); modal.show(); }, render: this.render.bind(this), }; // Determine what file filtering to use for the file list. let filterConfig = DEFAULT_SITE_FILTER; if (project.site?.files?.filter) { filterConfig = project.site.files.filter; // TODO: Allow for service specific default (ex: grow). } const filesFilter = new filter_1.IncludeExcludeFilter(filterConfig); // Create the directory structure using the filtered files. this.fileStructure = new DirectoryStructure(files.filter(file => filesFilter.matches(file.path)), eventHandlers, this.config.storage); // If there is a file loaded, expand the directory out to show it. if (this.config.state.file) { this.fileStructure.expandToFile(this.config.state.file); } } return selective_edit_1.html `<div class="le__part__menu__section__content"> <div class="le__list le__list--indent"> <div class="le__list__item le__list__item--heading"> <div class="le__list__item__icon"> <span class="material-icons">folder</span> </div> <div class="le__list__item__label"> ${editor.config.labels?.files || 'Files'} </div> </div> ${this.fileStructure.template(editor)} </div> </div>`; } templateFileCopy(editor) { const modal = this.getOrCreateModalCopy(editor); const isValid = modal.selective.isValid; try { return modal.selective.template(modal.selective, modal.data); } finally { if (isValid !== modal.selective.isValid) { this.render(); } } } templateFileDelete(editor) { const modal = this.getOrCreateModalDelete(editor); return selective_edit_1.html `<div class="le__modal__content__template__padded"> Do you want to delete the <code>${modal.data.get('path')}</code> file? </div>`; } templateFileNew(editor) { const modal = this.getOrCreateModalNew(editor); const isValid = modal.selective.isValid; try { return modal.selective.template(modal.selective, modal.data); } finally { if (isValid !== modal.selective.isValid) { this.render(); } } } templateTitle(editor) { return selective_edit_1.html `<div class="le__part__menu__section__title"> ${editor.config.labels?.menuSite || this.title} </div>`; } get title() { return 'Site'; } } exports.SitePart = SitePart; class DirectoryStructure { constructor(rootFiles, eventHandlers, storage, root = '/') { this.rootFiles = rootFiles; this.root = root; this.storage = storage; this.eventHandlers = eventHandlers; this.directories = {}; this.files = []; if (this.root === '/') { this.isExpanded = true; } const currentExpandedPaths = this.storage.getItemArray(STORAGE_FILE_EXPANDED); if (currentExpandedPaths.includes(this.root)) { this.isExpanded = true; } for (const fileData of this.rootFiles) { const relativePath = fileData.path.slice(this.root.length); const pathParts = relativePath.split('/'); // Directories have more segments. // First segment is empty string since it starts with /. if (pathParts.length > 1) { const directoryName = pathParts[0]; if (!this.directories[directoryName]) { const subDirectoryRoot = `${this.root}${directoryName}/`; const subFiles = this.rootFiles.filter(fileData => { return fileData.path.startsWith(subDirectoryRoot); }); this.directories[directoryName] = new DirectoryStructure(subFiles, this.eventHandlers, this.storage, subDirectoryRoot); } } else { this.files.push(fileData); } } } get base() { const trimmedRoot = this.root.replace(/^\/+/, '').replace(/\/+$/, ''); const rootParts = trimmedRoot.split('/'); return rootParts[rootParts.length - 1]; } baseFromFilePath(file) { const pathParts = file.path.split('/'); const fileParts = pathParts[pathParts.length - 1].split('.'); return fileParts.slice(0, -1).join('.'); } expandToFile(file) { // As long as the directory starts with the same path, expand it. if (file.file.path.startsWith(this.root)) { this.isExpanded = true; for (const key of Object.keys(this.directories)) { this.directories[key].expandToFile(file); } } } handleExpandCollapse() { this.isExpanded = !this.isExpanded; const currentExpandedPaths = this.storage.getItemArray(STORAGE_FILE_EXPANDED); if (this.isExpanded) { // Add to the storage. currentExpandedPaths.push(this.root); } else { // Remove from the storage. for (let i = 0; i < currentExpandedPaths.length; i++) { if (currentExpandedPaths[i] === this.root) { currentExpandedPaths.splice(i, 1); break; } } } this.storage.setItemArray(STORAGE_FILE_EXPANDED, currentExpandedPaths); this.eventHandlers.render(); } template(editor) { if (!this.isExpanded) { return selective_edit_1.html ``; } return selective_edit_1.html `${this.templateDirectories(editor)} ${this.templateFiles(editor)}`; } templateDirectories(editor) { if (!this.directories) { return selective_edit_1.html ``; } return selective_edit_1.html `<div class="le__list"> ${selective_edit_2.repeat(Object.keys(this.directories), (key) => key, (key) => selective_edit_1.html `<div class="le__list__item le__list__item--secondary le__clickable" @click=${this.directories[key].handleExpandCollapse.bind(this.directories[key])} > <div class="le__list__item__icon"> <span class="material-icons" >${this.directories[key].isExpanded ? 'expand_more' : 'chevron_right'}</span > </div> <div class="le__list__item__label"> ${this.directories[key].base} </div> </div> ${this.directories[key].template(editor)}`)} </div>`; } // eslint-disable-next-line @typescript-eslint/no-unused-vars templateFiles(editor) { if (!this.files || !this.files.length) { return selective_edit_1.html ``; } return selective_edit_1.html `<div class="le__list"> <div class="le__list__item le__list__item--primary le__clickable" @click=${(evt) => this.eventHandlers.fileNew(evt, this.root)} > <div class="le__list__item__icon"> <span class="material-icons">add_circle</span> </div> <div class="le__list__item__label"> ${editor.config.labels?.fileNew || 'New file'} </div> </div> ${selective_edit_2.repeat(this.files, (file) => file.path, (file) => selective_edit_1.html `<div class=${selective_edit_1.classMap({ le__clickable: true, le__list__item: true, 'le__list__item--selected': editor.state.file?.file.path === file.path, })} @click=${(evt) => this.eventHandlers.fileLoad(evt, file)} > <div class="le__list__item__icon"> <span class="material-icons">notes</span> </div> <div class="le__list__item__label"> ${this.baseFromFilePath(file)} </div> <div class="le__actions le__actions--slim"> <div class="le__actions__action le__clickable le__tooltip--top" @click=${(evt) => this.eventHandlers.fileCopy(evt, file)} data-tip="Duplicate file" > <span class="material-icons">file_copy</span> </div> <div class="le__actions__action le__actions__action--extreme le__clickable le__tooltip--top-left" @click=${(evt) => this.eventHandlers.fileDelete(evt, file)} data-tip="Delete file" > <span class="material-icons">remove_circle</span> </div> </div> </div>`)} </div>`; } } //# sourceMappingURL=site.js.map