Line data Source code
1 : // @ts-check 2 : import { syntaxTree } from "@codemirror/language"; 3 : import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view"; 4 : import { BaseLogger as logger } from "@cern/base-vue"; 5 : 6 : /** 7 : * @typedef {import("@codemirror/state").Range<Decoration>} RangeDecoration 8 : */ 9 : 10 : class SelectorPlugin { 11 : /** @param {EditorView} view */ 12 : constructor(view) { 13 4 : this.state = new WeakMap(); 14 4 : this.decorations = this.makeDecorations(view); 15 : } 16 : 17 : /** 18 : * @param {import("@codemirror/view").ViewUpdate} update 19 : */ 20 : update(update) { 21 14 : if (update.docChanged || update.viewportChanged) { 22 4 : this.decorations = this.makeDecorations(update.view); 23 : } 24 : } 25 : 26 : /** @param {EditorView} view */ 27 : makeDecorations(view) { 28 : /** @type {RangeDecoration[]} */ 29 8 : const widgets = []; 30 8 : for (const range of view.visibleRanges) { 31 4 : syntaxTree(view.state).iterate({ 32 : from: range.from, 33 : to: range.to, 34 : /* @ts-ignore */ 35 : enter: ({ type, from, to }) => { /* jshint ignore:line */ 36 80 : if (type.name !== "Attribute") { return; } 37 10 : const value = view.state.doc.sliceString(from, to); 38 10 : if (!value.startsWith("query-selector=\"")) { return; } 39 : 40 3 : const start = value.indexOf("\"") + 1; 41 3 : const end = value.lastIndexOf("\""); 42 3 : const widget = Decoration.widget({ 43 : widget: new SelectorWidget({ 44 : plugin: this, from: from + start, to: from + end 45 : }) 46 : }); 47 3 : widgets.push(widget.range(from + end)); 48 : } 49 : }); 50 : } 51 8 : return Decoration.set(widgets); 52 : } 53 : } 54 : 55 4 : export const makeSelectorPlugin = (/** @type {() => Promise<string>} */ callback) => ViewPlugin.fromClass(SelectorPlugin, { 56 18 : decorations: (v) => v.decorations, 57 : eventHandlers: { 58 : click(e, view) { 59 0 : const target = /** @type {HTMLElement} */ (e.target); 60 0 : if (target.parentElement?.classList.contains(wrapperClassName)) { 61 : /** @type {SelectorWidget} */ 62 0 : const widget = this.state.get(target.parentElement); 63 : 64 : // click event handler must return a boolean or void (not a promise) 65 0 : (async () => await callback() 66 0 : .then((value) => view.dispatch({ 67 : changes: { from: widget.from, to: widget.to, insert: value } 68 : }), 69 0 : (e) => logger.error(e)))(); 70 : } 71 : } 72 : } 73 : }); 74 : 75 1 : const wrapperClassName = "cm-selector-wrapper"; 76 : class SelectorWidget extends WidgetType { 77 : /** 78 : * @param {{ plugin: SelectorPlugin, from: number, to: number }} kwargs 79 : */ 80 : constructor(kwargs) { 81 3 : super(); 82 3 : this.plugin = kwargs.plugin; 83 3 : this.from = kwargs.from; 84 3 : this.to = kwargs.to; 85 : } 86 : 87 : /** 88 : * @param {SelectorWidget} other 89 : */ 90 : eq(other) { 91 0 : return ( 92 : other.from === this.from && 93 : other.to === this.to); 94 : } 95 : 96 : toDOM() { 97 3 : const elt = document.createElement("i"); 98 3 : elt.classList.add("fa", "fa-crosshairs"); 99 3 : const wrapper = document.createElement("span"); 100 3 : wrapper.appendChild(elt); 101 3 : wrapper.className = wrapperClassName; 102 3 : this.plugin.state.set(wrapper, this); 103 3 : return wrapper; 104 : } 105 : 106 : ignoreEvent() { 107 0 : return false; 108 : } 109 : 110 : /** @param {HTMLElement} dom */ 111 : destroy(dom) { 112 3 : this.plugin.state.delete(dom); 113 : } 114 : } 115 : 116 1 : export const selectorTheme = EditorView.baseTheme({ 117 : [`.${wrapperClassName}`]: { 118 : cursor: "pointer", 119 : // display: "inline-block", 120 : // outline: "1px solid #00000040", 121 : marginLeft: "0.3em", 122 : // height: "1em", 123 : // width: "1em", 124 : // borderRadius: "1px", 125 : verticalAlign: "middle" 126 : // marginTop: "-2px" 127 : } 128 : }); 129 : 130 : export default makeSelectorPlugin;