LCOV - code coverage report
Current view: top level - src/utils - CMSelectorPlugin.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 31 39 79.5 %
Date: 2025-06-29 02:18:36 Functions: 9 15 60.0 %

          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;

Generated by: LCOV version 1.16