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 :
|