UNPKG

@jinntec/jinn-codemirror

Version:

Source code editor component based on codemirror with language support for XML and Leiden+

291 lines (264 loc) 9.96 kB
import "./xml-editor"; import { JinnXMLEditor } from "./xml-editor"; import { JinnCodemirror } from "./jinn-codemirror"; interface LeidenEditorUpdateEvent extends Event { detail: { content: string } } const style = ` :host { display: block; width: 100%; } jinn-codemirror { font-size: 1rem; display:block; width:100%; } jinn-codemirror[valid="true"] { outline: thin solid green; } jinn-codemirror[valid="false"] { outline: thin solid red; } #leiden-editor { margin-bottom:0.5rem; } [slot=toolbar] { display: flex; } .hidden { display: none; } #close-leiden { margin-left: .75rem; font-weight: bold; }`; const ignoreKeys = ['Shift', 'Alt', 'Meta', 'Control', 'ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End']; const EDITOR_MODES = { leiden_plus: 'Leiden+', edcs: 'EDCS/EDH', default: 'Petrae' }; function createModeSelect(mode: string) { const options:string[] = []; Object.entries(EDITOR_MODES).forEach(([key, value]) => { options.push(`<option value="${key}" ${key === mode ? 'selected': ''}>${value}</option>`); }); return `<select name="modes">${options.join('\n')}</select>`; } /** * Combines an XML editor with an option to import and convert markup following variants * of the Leiden convention. * * @slot leiden-header - optional header to be displayed above the toolbar of the leiden editor * @slot xml-header - optional header to be displayed above the toolbar of the xml editor * @slot leiden-toolbar - toolbar for the leiden editor * @slot xml-toolbar - toolbar for the xml editor * @slot open-leiden - control (button by default) which opens/closes the leiden editor */ export class JinnEpidocEditor extends HTMLElement { xmlEditor: JinnXMLEditor | null | undefined; /** * Syntax mode to use for the leiden editor, one of leiden_plus, edcs or petrae */ public mode: string = 'leiden_plus'; /** * if set, user may choose from the supported syntaxes * * @attr {boolean} mode-select */ public modeSelect: boolean; public valid?: boolean; /** * If set, expects that a value passed in is a DOM element, which will serve as a wrapper for the content. * The wrapper element itself will not be shown in the editor. */ public unwrap?: boolean; /** * an optional schema description (JSON syntax) to load for the XML editor */ public schema: string | null; /** * determines the root element for autocomplete * * @attr {string} schema-root */ public schemaRoot: string | null; public placeholder: string = ''; /** * Should the leiden editor be shown initially? */ public showLeiden: boolean = false; /** * The value edited in the editor as either an Element or string - * depending on the mode set. */ set value(value: Element | string | null | undefined) { this.xmlEditor.value = value; } get value(): Element | null | undefined { return this.xmlEditor.value; } constructor() { super() this.xmlEditor = null; this.valid = true; this.unwrap = false; this.schema = null; this.schemaRoot = null; this.modeSelect = false; this.attachShadow({ mode: 'open' }); } connectedCallback() { this.unwrap = this.hasAttribute('unwrap'); this.schema = this.getAttribute('schema'); this.schemaRoot = this.getAttribute('schema-root'); this.modeSelect = this.hasAttribute('mode-select'); this.mode = this.getAttribute('mode') || 'leiden_plus'; this.placeholder = this.getAttribute('placeholder') || ''; this.showLeiden = this.hasAttribute('show-leiden'); this.shadowRoot.innerHTML = ` <style> ${style} </style> <jinn-codemirror id="leiden-editor" class="${this.showLeiden ? '' : 'hidden'}" mode="${this.mode}" ignore-blur> <div slot="header"><slot name="leiden-header"></slot></div> <div slot="toolbar"> ${ this.modeSelect ? createModeSelect(this.mode) : '' } <slot name="leiden-toolbar"></slot> <button part="button" id="close-leiden">Close</button> </div> </jinn-codemirror> <jinn-xml-editor id="xml-editor" ${this.unwrap ? 'unwrap' : ''} schema="${this.schema}" schema-root="${this.schemaRoot}" placeholder="${this.placeholder}" ignore-blur> <div slot="header"><slot name="xml-header"></slot></div> <div slot="toolbar"> <slot name="open-leiden" id="import" class="${this.showLeiden ? 'hidden' : ''}"> <button part="button" title="Import from Leiden markup">Leiden Editor</button> </slot> <slot name="xml-toolbar"></slot> </div> </jinn-xml-editor> `; this.xmlEditor = this.shadowRoot?.querySelector('#xml-editor'); const leidenEditor:JinnCodemirror | null | undefined = this.shadowRoot?.querySelector('#leiden-editor'); const openLeidenBtn:HTMLSlotElement | null | undefined = this.shadowRoot?.querySelector('#import'); const closeLeidenBtn:HTMLButtonElement | null | undefined = this.shadowRoot?.querySelector('#close-leiden'); if (!(this.xmlEditor && leidenEditor && openLeidenBtn && closeLeidenBtn)) { throw new Error('One or more components were not initialized') } let updateXML = true; let leidenEditorOpened = this.showLeiden; // update XML when Leiden editor changes leidenEditor.addEventListener('update', (ev) => { ev.stopPropagation(); this.showLeiden = false; // avoid XML to be overwritten after conversion to Leiden+ if (updateXML) { this.xmlEditor.content = ev.detail.content; } updateXML = true; }); this.xmlEditor.addEventListener('keyup', (ev) => { if (leidenEditorOpened) { if (ignoreKeys.indexOf(ev.key) > -1) { return; } hideLeiden(); } }); const showLeiden = () => { openLeidenBtn.classList.add('hidden'); leidenEditor.classList.remove('hidden'); leidenEditorOpened = true; leidenEditor.focus(); } const hideLeiden = () => { leidenEditor.classList.add('hidden'); openLeidenBtn.classList.remove('hidden'); leidenEditorOpened = false; this.xmlEditor?.focus(); updateXML = false; leidenEditor?.clear(); } const initLeiden = () => { const hidden = leidenEditor.classList.contains('hidden'); if (hidden || this.showLeiden) { if (this.xmlEditor.content.length > 0) { if (!this.valid) { alert('The XML contains errors. Cannot convert to Leiden+'); return; } const value = this.xmlEditor?.value; updateXML = false; leidenEditor.setMode('leiden_plus', false); try { if (this.unwrap && value instanceof Element) { leidenEditor.value = value.childNodes; } else { leidenEditor.value = value; } this.xmlEditor.status = ''; showLeiden(); } catch (e) { this.xmlEditor.status = e.message; hideLeiden(); } } else { showLeiden(); leidenEditor.value = ''; } } else { hideLeiden(); } } openLeidenBtn.addEventListener('click', () => { initLeiden(); }); closeLeidenBtn.addEventListener('click', () => { hideLeiden(); }); this.xmlEditor.addEventListener('invalid', (ev) => { ev.stopPropagation(); this.valid = false; this.setAttribute('valid', this.valid.toString()); this.dispatchEvent(new CustomEvent('invalid', { detail: ev.detail, composed: true, bubbles: true })); }); this.xmlEditor.addEventListener('valid', (ev) => { ev.stopPropagation() this.valid = true; this.setAttribute('valid', this.valid.toString()); this.dispatchEvent(new CustomEvent('valid', { detail: ev.detail, composed: true, bubbles: true })); }); this.xmlEditor.addEventListener('update', () => { if (this.showLeiden) { initLeiden(); } this.showLeiden = false; }, { once: true }); this.addEventListener('blur', (ev) => { if (!ev.relatedTarget || this.contains(ev.relatedTarget as Node)) { return; } this.dispatchEvent(new CustomEvent('leave', { composed: true, bubbles: true })); }); } } if (!customElements.get('jinn-epidoc-editor')) { window.customElements.define('jinn-epidoc-editor', JinnEpidocEditor); }