UNPKG

@jinntec/jinn-codemirror

Version:

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

173 lines (153 loc) 6.1 kB
import { JinnCodemirror } from "./jinn-codemirror"; import { XMLConfig } from "./xml"; import { AttributeAutocompleteProvider, ZoteroAutocomplete } from "./autocomplete/zotero-autocomplete"; /** * Extends jinn-codemirror for XML editing: adds a boolean property "unwrap" to * indicate if the entire root node passed in as value should be edited or just its * content. Setting the property requires that a DOM element is passed via value. * * @attr {boolean} check-namespace - if enabled, a missing namespace will be reported as error */ export class JinnXMLEditor extends JinnCodemirror { /** * 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. * * @attr {string} unwrap */ public unwrap: boolean | null = false; _wrapper?: Element | null; /** * Schema to load for autocompletion. * * @attr {string} schema */ schema: string | null; /** * Determines the root element to be used for autocomplete. * * @attr {string} schema-root */ schemaRoot: string | null; /** * Base URL for autocomplete providers. * * @attr {string} base-url */ baseUrl : string | null; autocompleteProviders: AttributeAutocompleteProvider[] = []; constructor() { super(); this.schema = null; this.schemaRoot = null; this.baseUrl = null; } connectedCallback() { this.schema = this.getAttribute('schema'); this.schemaRoot = this.getAttribute('schema-root'); this.baseUrl = this.getAttribute('base-url'); this.unwrap = this.hasAttribute('unwrap'); this.autocompleteProviders = this.getAttribute('providers')?.split(',').map((provider: string) => { if (provider === 'zotero') { return new ZoteroAutocomplete(this.baseUrl); } throw new Error(`Unknown autocomplete provider: ${provider}`); }) || []; super.connectedCallback(); const wrapper = this.getAttribute('wrapper'); if (wrapper) { const parser = new DOMParser(); let parsed = parser.parseFromString(wrapper, 'application/xml'); let errors = parsed.getElementsByTagName("parsererror"); let root = null; if (errors.length) { console.error('<jinn-xml-editor> Invalid XML for wrapper attribute: %s', new XMLSerializer().serializeToString(parsed)); } else { root = parsed.firstElementChild; if (root && this.hasAttribute('code')) { const code = this.getAttribute('code') || ''; parsed = parser.parseFromString(code, 'application/xml'); errors = parsed.getElementsByTagName("parsererror"); if (errors.length) { console.error('<jinn-xml-editor> Invalid XML for code attribute: %s', new XMLSerializer().serializeToString(parsed)); } else if (parsed.firstElementChild) { root.appendChild(parsed.firstElementChild); } } this.setValue(root); } } } configure() { const toolbar = this.getToolbarControls(<HTMLSlotElement|null> this.shadowRoot?.querySelector('[name=toolbar]')); const checkNamespace = this.hasAttribute('check-namespace'); this._config = new XMLConfig(this, toolbar, this.namespace, checkNamespace, this.unwrap, this.autocompleteProviders.flatMap((provider: AttributeAutocompleteProvider) => provider.createAutocomplete())); } emitUpdateEvent(content: string) { if (!this.unwrap) { return super.emitUpdateEvent(content); } this.updateValue(); super.emitUpdateEvent(this._wrapper); } protected updateValue() { if (!this._wrapper) { console.log("no wrapper !!!"); return null; } // remove old children this._wrapper.replaceChildren(); if (!this._value) { // empty console.log("xml editor value is empty"); } else if (this._value instanceof NodeList) { for (let i = 0; i < this._value.length; i++) { const child = this._wrapper.ownerDocument.importNode(this._value[i], true); this._wrapper?.appendChild(child); }; } else if (!(this._value instanceof Node)) { console.error("<xml-editor> Value is not a node"); throw new Error('value is not a node'); } else { this._wrapper?.appendChild(this._value); } } protected setValue(value: Element | string | null | undefined): boolean { if (!this.unwrap) { return super.setValue(value); } if (this._config?.setFromValue(this._wrapper) === this._config?.setFromValue(value)) { return false; } if (!value) { this._wrapper = null; } if (typeof value === 'string') { const parser = new DOMParser(); const fragment = parser.parseFromString(value, 'application/xml'); if (!fragment.firstElementChild) { return false; } value = fragment.firstElementChild; } this._wrapper = value; this._value = value?.childNodes; return true; } protected getValue(): Element | NodeListOf<ChildNode> | string | null { if (!this.unwrap) { return super.getValue(); } if (!this._wrapper) { return null; } if (!(this._wrapper instanceof Element)) { throw new Error("Value is not a node"); } this.updateValue(); return this._wrapper; } } if (!customElements.get('jinn-xml-editor')) { window.customElements.define('jinn-xml-editor', JinnXMLEditor); }