UNPKG

@discoveryjs/discovery

Version:

Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards

190 lines (189 loc) 5.84 kB
import { createElement } from "../../core/utils/dom.js"; import { escapeHtml } from "../../core/utils/html.js"; import { ContentRect } from "../../core/utils/size.js"; import { Emitter } from "../../core/emitter.js"; import CodeMirror from "codemirror"; import modeQuery from "./editor-mode-query.js"; import modeView from "./editor-mode-view.js"; import "codemirror/mode/javascript/javascript.js"; import "./editors-hint.js"; Object.defineProperty(CodeMirror.prototype, "display", { configurable: true, set(value) { value.scroller.addEventListener = function(eventName, cb, options) { EventTarget.prototype.addEventListener.call(this, eventName, cb, typeof options !== "boolean" ? options : { capture: options, passive: ["touchstart", "touchmove", "wheel", "mousewheel"].includes(eventName) }); }; Object.defineProperty(this, "display", { configurable: true, enumerable: true, value }); return value; } }); const renderQueryAutocompleteItem = (el, _, { entry: { type, text, value } }) => { const startChar = text[0]; const lastChar = text[text.length - 1]; const start = startChar === '"' || startChar === "'" ? 1 : 0; const end = lastChar === '"' || lastChar === "'" ? 1 : 0; const pattern = text.toLowerCase().substring(start, text.length - end); const offset = pattern ? value.toLowerCase().indexOf(pattern, value[0] === '"' || value[0] === "'" ? 1 : 0) : -1; if (offset !== -1) { value = escapeHtml(value.substring(0, offset)) + '<span class="match">' + escapeHtml(value.slice(offset, offset + pattern.length)) + "</span>" + escapeHtml(value.slice(offset + pattern.length)); } el.classList.add("type-" + type); el.appendChild(createElement("span", "name", value)); }; export class Editor extends Emitter { static CodeMirror = CodeMirror; el; cm; get container() { return null; } constructor({ hint, mode, placeholder }) { super(); this.el = document.createElement("div"); this.el.className = "discovery-view-editor empty-value"; const self = this; const cm = CodeMirror(this.el, { extraKeys: { "Alt-Space": "autocomplete" }, mode: mode || "javascript", theme: "neo", indentUnit: 0, showHintOptions: { hint, get container() { return self.container; } } }); if (placeholder) { cm.display.lineDiv.parentNode.dataset.placeholder = placeholder; } cm.on("change", () => { const newValue = cm.getValue(); this.el.classList.toggle("empty-value", newValue === ""); this.emit("change", newValue); }); if (typeof hint === "function") { cm.on("cursorActivity", (cm2) => { if (cm2.state.completionEnabled && cm2.state.focused) { cm2.showHint(); } }); cm.on("focus", (cm2) => { if (cm2.getValue() === "") { cm2.state.completionEnabled = true; } if (cm2.state.completionEnabled && !cm2.state.completionActive) { cm2.showHint(); } }); cm.on("change", (_, change) => { if (change.origin !== "complete") { cm.state.completionEnabled = true; } }); } this.cm = cm; const rect = new ContentRect(); rect.subscribe(() => cm.refresh()); rect.observe(cm.display.wrapper); } getValue() { return this.cm.getValue(); } setValue(value) { requestAnimationFrame(() => this.cm.refresh()); if (typeof value === "string" && this.getValue() !== value) { this.cm.setValue(value || ""); return true; } return false; } focus() { this.cm.focus(); } } export class QueryEditor extends Editor { queryData; queryContext; inputPanelEl; outputPanelEl; constructor(getSuggestions) { super({ mode: "discovery-query", placeholder: "Enter a jora query", hint: (cm) => { const cursor = cm.getCursor(); const suggestions = getSuggestions( cm.getValue(), cm.doc.indexFromPos(cursor), this.queryData, this.queryContext ); if (!suggestions) { return null; } return { list: suggestions.slice(0, 50).map((entry) => { return { entry, text: entry.value, render: renderQueryAutocompleteItem, from: cm.posFromIndex(entry.from), to: cm.posFromIndex(entry.to) }; }) }; } }); this.inputPanelEl = createElement("div", "discovery-view-editor__input-panel"); this.outputPanelEl = createElement("div", "discovery-view-editor__output-panel"); this.el.append(this.inputPanelEl, this.outputPanelEl); } setValue(value, data, context) { const dataChanged = this.queryData !== data || this.queryContext !== context; this.queryData = data; this.queryContext = context; if (super.setValue(value)) { return true; } if (dataChanged) { if (this.cm.state.completionEnabled && this.cm.state.focused) { this.cm.showHint(); } } return false; } } export class ViewEditor extends Editor { constructor() { super({ mode: { name: "discovery-view", isDiscoveryViewDefined: (name) => this.isViewDefined(name) } }); } isViewDefined() { return false; } } CodeMirror.defineMode("jora", modeQuery); CodeMirror.defineMode("discovery-query", modeQuery); CodeMirror.defineMode("discovery-view", modeView); export default function(host) { Object.assign(host.view, { QueryEditor: class extends QueryEditor { get container() { return host.dom.container; } }, ViewEditor: class extends ViewEditor { isViewDefined(name) { return host.view.isDefined(name) || host.textView.isDefined(name); } } }); }