@discoveryjs/discovery
Version:
Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards
190 lines (189 loc) • 5.84 kB
JavaScript
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);
}
}
});
}