@jinntec/jinn-codemirror
Version:
Source code editor component based on codemirror with language support for XML and Leiden+
333 lines (330 loc) • 9.52 kB
JavaScript
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import { XMLConfig } from "./xml";
import { LeidenConfig } from "./leiden+";
import { AncientTextConfig } from "./ancientText";
import { XQueryConfig } from "./xquery";
import { CSSConfig } from "./css";
import { JSONConfig } from "./json";
import { PlainConfig } from "./plain";
import { TeXConfig } from "./tex";
import { SourceType, initCommand } from "./config";
import { HTMLConfig } from "./html";
import { MarkdownConfig } from "./markdown";
class JinnCodemirror extends HTMLElement {
constructor() {
super();
this._mode = SourceType.xml;
this._placeholder = "";
this.attachShadow({ mode: "open" });
this.ignoreBlur = true;
}
static get observedAttributes() {
return ["placeholder", "mode", "code"];
}
connectedCallback() {
const css = document.createElement("style");
css.innerHTML = this.styles();
this.shadowRoot?.appendChild(css);
const headerSlot = document.createElement("slot");
headerSlot.name = "header";
this.shadowRoot?.appendChild(headerSlot);
const toolbarSlot = document.createElement("slot");
toolbarSlot.name = "toolbar";
this.shadowRoot?.appendChild(toolbarSlot);
const wrapper = document.createElement("div");
wrapper.id = "editor";
this.shadowRoot?.appendChild(wrapper);
this.registerToolbar(this.shadowRoot?.querySelector("[name=toolbar]"));
this._placeholder = this.getAttribute("placeholder") || "";
this.namespace = this.getAttribute("namespace");
this.linter = this.getAttribute("linter");
this.mode = this.initModes() || this.getAttribute("mode") || "xml";
if (!this.hasAttribute("mode")) {
this.setAttribute("mode", this._mode);
}
if (this.hasAttribute("code")) {
this.value = this.getAttribute("code");
}
if (this.hasAttribute("theme")) {
this.theme = this.getAttribute("theme");
}
this.ignoreBlur = this.hasAttribute("ignore-blur");
if (!this.ignoreBlur) {
this.addEventListener("blur", (ev) => {
if (!ev.relatedTarget || this.contains(ev.relatedTarget)) {
return;
}
this.dispatchEvent(new CustomEvent("leave", {
composed: true,
bubbles: true
}));
});
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (!oldValue || oldValue === newValue) {
return;
}
switch (name) {
case "placeholder":
this.placeholder = newValue;
break;
case "mode":
this.mode = newValue;
break;
case "code":
this.value = newValue;
}
}
/**
* Move keyboard focus to the editor
*/
focus() {
if (this._editor) {
this._editor.focus();
}
}
get placeholder() {
return this._placeholder;
}
/**
* A placeholder string to be shown if the user has not yet entered anything.
*
* @attr {string} placeholder
*/
set placeholder(label) {
this._placeholder = label;
this.setMode(this.mode);
}
/**
* The mode to use. Currently supported are 'xml', 'xquery', 'css', 'html', 'tex', 'markdown', 'leiden_plus', 'edcs', 'phi' or 'default'.
*
* @attr {string} mode
*/
set mode(mode) {
this.setMode(mode);
}
setMode(mode, update = true) {
const wrapper = this.shadowRoot?.getElementById("editor");
if (!wrapper) {
return;
}
if (this._editor) {
this._editor.destroy();
wrapper.innerHTML = "";
}
this._mode = SourceType[mode];
console.log(`<jinn-codemirror> mode: ${this.mode}`);
this.activateToolbar(this.shadowRoot?.querySelector("[name=toolbar]"));
this.configure();
const select = this.querySelector("[name=modes]");
if (select && select instanceof HTMLSelectElement) {
select.value = this._mode;
}
this._config?.getConfig().then((stateConfig) => {
this._editor = new EditorView({
state: EditorState.create(stateConfig),
parent: wrapper
});
if (!this._config) {
return;
}
if (update) {
this.content = this._config.setFromValue(this._value);
}
});
}
configure() {
const toolbar = this.getToolbarControls(this.shadowRoot?.querySelector("[name=toolbar]"));
switch (this._mode) {
case SourceType.edcs:
case SourceType.phi:
this._config = new AncientTextConfig(this, toolbar, this._mode);
break;
case SourceType.leiden_plus:
this._config = new LeidenConfig(this, toolbar);
break;
case SourceType.xquery:
this._config = new XQueryConfig(this, toolbar, this.linter);
break;
case SourceType.css:
this._config = new CSSConfig(this, toolbar);
break;
case SourceType.json:
this._config = new JSONConfig(this, toolbar);
break;
case SourceType.tex:
this._config = new TeXConfig(this, toolbar);
break;
case SourceType.html:
this._config = new HTMLConfig(this, toolbar);
break;
case SourceType.xml:
this._config = new XMLConfig(this, toolbar, this.namespace);
break;
case SourceType.markdown:
this._config = new MarkdownConfig(this, toolbar);
break;
default:
this._config = new PlainConfig(this, toolbar);
break;
}
}
get mode() {
return this._mode;
}
set valid(value) {
this.setAttribute("valid", value.toString());
}
get valid() {
return Boolean(this.hasAttribute("valid"));
}
/**
* Show a status message below the editor.
*/
set status(msg) {
this._config.status = msg;
}
/**
* The content edited in the editor as a string.
*/
set content(text) {
if (!this._editor) {
console.log("no editor");
return;
}
setTimeout(
() => this._editor.dispatch({
changes: { from: 0, to: this._editor.state.doc.length, insert: text }
})
);
}
get content() {
return this._editor?.state.doc.toString() || "";
}
/**
* The value edited in the editor as either an Element or string - depending on the mode set.
*/
set value(value) {
const updated = this.setValue(value);
if (updated && this._editor && this._config) {
this.content = this._config?.setFromValue(this._value);
}
}
get value() {
return this.getValue();
}
setValue(value) {
if (!this._config) {
return false;
}
const _val = this._config.setFromValue(value);
if (this._value === _val) {
return false;
}
this._value = value;
return true;
}
getValue() {
if (!this._value) {
return null;
}
return this._value;
}
set code(text) {
this.value = text;
}
clear() {
this._value = "";
this._editor.dispatch({
changes: { from: 0, to: this._editor.state.doc.length, insert: "" }
});
}
emitUpdateEvent(content) {
this.dispatchEvent(new CustomEvent("update", {
detail: { content },
composed: true,
bubbles: true
}));
}
initModes() {
const select = this.querySelector("[name=modes]");
if (select && select instanceof HTMLSelectElement) {
select.addEventListener("change", () => {
this.mode = select.value;
});
return select.value;
}
return null;
}
registerToolbar(slot) {
slot?.assignedElements().forEach((elem) => {
elem.querySelectorAll("slot").forEach((sl) => this.registerToolbar(sl));
elem.querySelectorAll("[data-command]").forEach((btn) => {
const cmdName = btn.dataset.command;
if (btn.hasAttribute("data-key")) {
btn.title = `${btn.title} (${btn.getAttribute("data-key")})`;
}
btn.addEventListener("click", () => {
if (!this._config) {
return;
}
const commands = this._config.getCommands();
const command = commands[cmdName];
if (command) {
const func = initCommand(cmdName, command, btn);
if (func) {
func(this._editor);
if (cmdName !== "encloseWithCommand") {
this._editor?.focus();
}
}
}
});
});
});
}
activateToolbar(slot) {
slot?.assignedElements().forEach((elem) => {
elem.querySelectorAll("slot").forEach((sl) => this.activateToolbar(sl));
elem.querySelectorAll("[data-command]").forEach((elem2) => {
const btn = elem2;
if (!btn.dataset.mode || btn.dataset.mode === this._mode) {
btn.style.display = "inline";
} else {
btn.style.display = "none";
}
});
});
}
getToolbarControls(slot, toolbar = []) {
slot?.assignedElements().forEach((elem) => {
elem.querySelectorAll("[data-command]").forEach((btn) => {
toolbar.push(btn);
});
elem.querySelectorAll("slot").forEach((sl) => this.getToolbarControls(sl, toolbar));
});
return toolbar;
}
styles() {
return `
:host > div {
width: 100%;
background-color: var(--jinn-codemirror-background-color, #fff);
}
.cm-cursor {
min-height: 1rem;
}
.status {
padding-left: .5rem;
}
`;
}
}
if (!customElements.get("jinn-codemirror")) {
window.customElements.define("jinn-codemirror", JinnCodemirror);
}
export {
JinnCodemirror
};