LCOV - code coverage report
Current view: top level - src/components - SVGSelector.vue.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 14 69 20.3 %
Date: 2025-06-29 02:18:36 Functions: 6 19 31.6 %

          Line data    Source code
       1             : // @ts-check
       2             : 
       3             : import Vue from "vue";
       4             : import { mapGetters, mapState } from "vuex";
       5             : import { find, get, isEmpty } from "lodash";
       6             : import { select } from "d3-selection";
       7             : import { makeDeferred } from "@cern/nodash";
       8             : import { BaseKeyboardEventMixin as KBMixin } from "@cern/base-vue";
       9             : import { getElementPath } from "../utils";
      10             : import d from "debug";
      11             : 
      12           1 : const debug = d("app:svg-selector");
      13             : 
      14             : /**
      15             :  * @typedef {import("d3-selection").Selection<Element|null, any, any, any>} Selection
      16             :  * @typedef {{ deferred: ReturnType<makeDeferred>|null }} Opts
      17             :  */
      18             : 
      19             : /**
      20             :  * @brief find and select element on SVG
      21             :  */
      22             : export default /** @type {V.Constructor<Opts, any>} */ (Vue).extend({
      23             :   name: "SVGSelector",
      24             :   mixins: [ KBMixin({ local: false }) ],
      25             :   computed: {
      26             :     ...mapState("engine", {
      27             :       /** @type {(state: AppStore.SSVGEngine) => Element|null} */
      28          16 :       directSvg: (state) => get(state, [ "directEngine", "svg" ]),
      29             :       /** @type {(state: AppStore.SSVGEngine) => Element|null} */
      30          16 :       svg: (state) => get(state, [ "engine", "svg" ])
      31             :     }),
      32             :     .../**
      33             :         * @type {{ selectorHelper(): string|null,
      34             :         *   path(): AppStore.Selection["path"] }}
      35             :         */(
      36             :       mapState("selection", [ "selectorHelper", "path" ])),
      37             :     .../**  @type {{ svgSelectorsSet(): Set<string> }} */(
      38             :       mapGetters("engine", [ "svgSelectorsSet" ]))
      39             :   },
      40             :   watch: {
      41             :     selectorHelper() {
      42           0 :       if (this.$options.deferred?.isPending && this.selectorHelper) {
      43           0 :         this.$options.deferred.resolve(this.selectorHelper);
      44             :       }
      45             :     },
      46             :     path() {
      47             :       /* on current selection change, abort any ongoing item selection */
      48           9 :       if (this.$options.deferred?.isPending) {
      49           0 :         debug("path changed, aborting selection");
      50           0 :         this.$options.deferred.resolve(null);
      51             :       }
      52             :     }
      53             :   },
      54             :   mounted() {
      55          16 :     this.onKey("esc", this._removeAll);
      56             :   },
      57             :   beforeDestroy() {
      58          16 :     this._removeAll();
      59             :   },
      60             :   methods: {
      61             :     /** @returns {Promise<string>} */
      62             :     async select() {
      63           0 :       const svgSelect = select(this.svg);
      64           0 :       const directSvgSelect = select(this.directSvg);
      65             : 
      66           0 :       svgSelect.call(this._hoverHook);
      67           0 :       directSvgSelect.call(this._hoverHook);
      68           0 :       this.$options.deferred = makeDeferred();
      69             : 
      70             :       /**
      71             :        * @param  {MouseEvent} event
      72             :        */ /* eslint-disable-next-line complexity */
      73           0 :       const selectItem = (/** @type {typeof svgSelect} */ selection, event) => {
      74             : 
      75           0 :         const root = /** @type {Element} */(selection.node());
      76             :         const target = /** @type {Element|null} */(
      77           0 :           this._findTarget(root, /** @type {Element|null} */(event.target)) ??
      78             :           event.target);
      79           0 :         let ret = this._getSelectClass(target);
      80           0 :         if (ret && this.$options.deferred) {
      81           0 :           return this.$options.deferred.resolve("." + ret);
      82             :         }
      83           0 :         ret = target?.getAttribute("id") ?? null;
      84           0 :         if (ret && this.$options.deferred) {
      85           0 :           return this.$options.deferred.resolve("#" + ret);
      86             :         }
      87           0 :         ret = getElementPath(root, target);
      88           0 :         if (ret && this.$options.deferred) {
      89           0 :           return this.$options.deferred.resolve(ret);
      90             :         }
      91             : 
      92           0 :         if (this.$options.deferred) {
      93           0 :           return this.$options.deferred.resolve(null);
      94             :         }
      95             :       };
      96           0 :       svgSelect.on("click.svgselector", (event) => selectItem(svgSelect, event));
      97           0 :       directSvgSelect.on("click.svgselector", (event) => selectItem(directSvgSelect, event));
      98           0 :       return this.$options.deferred.promise
      99             :       .then((res) => {
     100           0 :         this.$store.commit("selection/update", { selectorHelper: res });
     101           0 :         return /** @type {string}*/(res);
     102             :       })
     103             :       .finally(() => {
     104           0 :         this.$options.deferred = null;
     105           0 :         this._removeAll();
     106             :       });
     107             :     },
     108             :     _removeAll() {
     109          16 :       const svgSelect = select(this.svg);
     110          16 :       const directSvgSelect = select(this.directSvg);
     111          16 :       for (const e of [ "click", "mouseover", "mouseout" ]) {
     112          48 :         svgSelect.on(e + ".svgselector", null);
     113          48 :         directSvgSelect.on(e + ".svgselector", null);
     114             :       }
     115          16 :       svgSelect.selectAll(".hovered").classed("hovered", false);
     116          16 :       directSvgSelect.selectAll(".hovered").classed("hovered", false);
     117          16 :       if (this.$options.deferred) {
     118           0 :         const deferred = this.$options.deferred;
     119           0 :         this.$options.deferred = null;
     120           0 :         deferred.resolve(null);
     121             :       }
     122             :     },
     123             :     /**
     124             :      * @param  {Element|null} element
     125             :      */
     126             :     _getSelectClass(element) {
     127           0 :       if (!element || !this.svgSelectorsSet) { return null; }
     128           0 :       return find(element.classList,
     129           0 :         (c) => this.svgSelectorsSet.has("." + c)) || null;
     130             :     },
     131             :     /**
     132             :      * @param  {Element} root    [description]
     133             :      * @param  {Element|null} element [description]
     134             :      * @return {Element|null}         [description]
     135             :      */
     136             :     _findTarget(root, element) {
     137           0 :       if (!element) { return null; }
     138             : 
     139           0 :       const id = element.getAttribute("id");
     140           0 :       if (!isEmpty(id)) {
     141           0 :         return element;
     142             :       }
     143           0 :       const c = this._getSelectClass(element);
     144           0 :       if (!isEmpty(c)) {
     145           0 :         return element;
     146             :       }
     147           0 :       if (element !== root) {
     148           0 :         return this._findTarget(root, element.parentElement);
     149             :       }
     150           0 :       return null;
     151             :     },
     152             :     /**
     153             :      * @param  {Selection} selection [description]
     154             :      */
     155             :     _hoverHook(selection) {
     156           0 :       const self = this; // eslint-disable-line @typescript-eslint/no-this-alias
     157           0 :       const root = /** @type {Element} */(selection.node());
     158           0 :       selection.on("mouseover.svgselector", function(event) {
     159           0 :         const target = self._findTarget(root, event.target) ?? event.target;
     160           0 :         if (target) {
     161           0 :           select(target).classed("hovered", true);
     162             :         }
     163             :       })
     164             :       .on("mouseout.svgselector", function(event) {
     165           0 :         const target = self._findTarget(root, event.target) ?? event.target;
     166           0 :         if (target) {
     167           0 :           select(target).classed("hovered", false);
     168             :         }
     169             :       });
     170             :     }
     171             :   }
     172             : });
     173             : 

Generated by: LCOV version 1.16