LCOV - code coverage report
Current view: top level - src/components/SSVGPropList - CreateRelationDialog.vue.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 9 74 12.2 %
Date: 2025-06-29 02:18:36 Functions: 4 13 30.8 %

          Line data    Source code
       1             : // @ts-check
       2             : 
       3             : import Vue from "vue";
       4             : import { mapState } from "vuex";
       5             : import { BaseDialog } from "@cern/base-vue";
       6             : 
       7             : import { genId } from "../../utils";
       8             : import { select } from "d3-selection";
       9             : import { color } from "d3-color";
      10             : import Input from "../Input.vue";
      11             : import { get } from "lodash";
      12             : import d from "debug";
      13             : 
      14           1 : const debug = d("app:edit");
      15             : 
      16             : /**
      17             :  * @typedef {{ name: string, isAvailable?: (selection: ReturnType<select<Element>>|null) => boolean }} RelationType
      18             :  * @typedef {{
      19             :  *  "dialog": V.Instance<BaseDialog>,
      20             :  *  "from": V.Instance<Input>,
      21             :  *  "to": V.Instance<Input>,
      22             :  *  "type": V.Instance<Input>,
      23             :  *  "attribute-name": V.Instance<Input>
      24             :  * }} Refs
      25             :  */
      26             : 
      27           1 : const hasWidthHeight = new Set([ "filter", "foreignObject", "image", "pattern",
      28             :                                  "rect", "svg", "use", "mask" ]);
      29             : 
      30             : /** @type {{ [name: string]: RelationType }} */
      31           1 : const types = {
      32             :   custom: { name: "custom" },
      33             :   color: { name: "color" },
      34             :   opacity: { name: "opacity" },
      35             :   width: { name: "width", isAvailable(selection) {
      36           0 :     return hasWidthHeight.has(/** @type {Element}*/(selection?.node())?.tagName);
      37             :   } },
      38             :   height: { name: "height", isAvailable(selection) {
      39           0 :     return hasWidthHeight.has(/** @type {Element}*/(selection?.node())?.tagName);
      40             :   } }
      41             : };
      42             : 
      43           1 : const options = {
      44             :   types
      45             : };
      46             : 
      47           1 : const component = /** @type {V.Constructor<typeof options, Refs>} */ (Vue).extend({
      48             :   name: "CreateRelationDialog",
      49             :   components: { BaseDialog, Input }, /* eslint-disable-line vue/no-reserved-component-names -- Input */
      50             :   props: { query: { type: String, default: null } },
      51             :   ...options,
      52             :   /**
      53             :    * @return {{
      54             :    * type: keyof types, svgAttributesId: string, isValid: boolean,
      55             :    * selection: ReturnType<select<Element>>|null }}
      56             :    */
      57          12 :   data() { return { type: "color", svgAttributesId: genId(), isValid: true, selection: null }; },
      58             :   computed: {
      59             :     .../** @return {{ svg(): Element|null }} */mapState("engine", {
      60          17 :       svg(/** @type {AppStore.SSVGEngine} */state) { return get(state, "directEngine.svg"); } })
      61             :   },
      62             :   watch: {
      63           5 :     svg() { this.$nextTick(this.onUpdate); },
      64           0 :     type() { this.$nextTick(this.onUpdate); },
      65           0 :     query() { this.$nextTick(this.onUpdate); }
      66             :   },
      67             :   methods: {
      68             :     async request() {
      69           0 :       if (!(await this.$refs.dialog.request())) { return null; }
      70           0 :       return this.createRelation();
      71             :     },
      72             :     async checkValidity() { /* eslint-disable-line complexity */
      73           0 :       if (!this.query) { return; }
      74           0 :       let valid = true;
      75             : 
      76           0 :       for (const ref of [ "attribute-name", "from", "to" ]) {
      77           0 :         valid = await (/** @type {V.Instance<Input>} */(
      78             :           this.$refs?.[ref])?.checkValidity() ?? true) && valid;
      79             :       }
      80             : 
      81           0 :       if (this.type === "color") {
      82           0 :         if ((!this.selection?.node() || !this.selection?.style("fill")) && !this.$refs["from"]?.editValue) {
      83           0 :           this.$refs["from"]?.addError("x-error", "Failed to retrieve \"from\" value");
      84           0 :           valid = false;
      85             :         }
      86             :         else {
      87           0 :           this.$refs["from"]?.removeError("x-error");
      88             :         }
      89             :       }
      90           0 :       this.isValid = valid;
      91             :     },
      92             :     onUpdate() { /* eslint-disable-line complexity */
      93           5 :       if (!this.svg || !this.query) { return; }
      94           0 :       this.selection = select(this.svg).selectAll(this.query);
      95           0 :       if (!this.selection.node()) {
      96           0 :         this.$refs["type"]?.addWarning("x-warn", "failed to select nodes");
      97             :       }
      98           0 :       else if (this.type === "custom") {
      99           0 :         this.$refs["type"]?.addWarning("x-warn", '"custom" is an expert type, please select another type for guided creation');
     100             :       }
     101             :       else {
     102           0 :         this.$refs["type"]?.removeWarning("x-warn");
     103             :       }
     104             : 
     105           0 :       if (this.type === "color") {
     106           0 :         this.$refs["from"].editValue =
     107             :           color(this.getCurrentValue("fill", "#000000"))?.formatHex() ?? null;
     108             :       }
     109           0 :       else if (this.type === "opacity") {
     110           0 :         const from = this.getCurrentValue("opacity", "1");
     111           0 :         this.$refs["from"].editValue = from;
     112           0 :         this.$refs["to"].editValue = ((Number(from) >= 0.5) ? "0" : "1");
     113             :       }
     114           0 :       else if (this.type === "width" || this.type === "height") {
     115             : 
     116             :         const from =
     117           0 :           /** @type {SVGGraphicsElement} */(this.selection?.node())?.getBBox()?.[this.type] ?? 100;
     118           0 :         this.$refs["from"].editValue = from.toString();
     119           0 :         this.$refs["to"].editValue = `${(from * 2)}`;
     120             :       }
     121           0 :       this.checkValidity();
     122             :     },
     123             :     /**
     124             :      * @param {Element} element
     125             :      * @param {string} name
     126             :      * @param {boolean} optional
     127             :      */
     128             :     addAttribute(element, name, optional = false) {
     129           0 :       const value = /** @type {V.Instance<Input>} */(this.$refs[name])?.editValue;
     130           0 :       if (!optional || value) {
     131           0 :         element.setAttribute(name, value || "");
     132             :       }
     133           0 :       return element;
     134             :     },
     135             :     createRelation() { /* eslint-disable-line complexity */
     136           0 :       const element = document.createElementNS("http://www.w3.org/2000/svg", "relation");
     137           0 :       element.setAttribute("query-selector", this.query);
     138           0 :       if (this.type === "custom") {
     139           0 :         this.addAttribute(element, "attribute-name", true);
     140           0 :         this.addAttribute(element, "attribute-type", true);
     141           0 :         this.addAttribute(element, "calc-mode", true);
     142           0 :         this.addAttribute(element, "from", true);
     143           0 :         this.addAttribute(element, "to");
     144             :       }
     145           0 :       else if (this.type === "color") {
     146           0 :         this.addAttribute(element, "from");
     147           0 :         this.addAttribute(element, "to");
     148           0 :         element.setAttribute("attribute-name", "fill");
     149           0 :         element.setAttribute("attribute-type", "CSS");
     150             :       }
     151           0 :       else if (this.type === "opacity") {
     152           0 :         element.setAttribute("from", this.$refs["from"]?.editValue ||
     153             :           ((Number(this.$refs["to"]?.editValue) >= 0.5) ? "0" : "1"));
     154           0 :         this.addAttribute(element, "to");
     155           0 :         element.setAttribute("attribute-name", "opacity");
     156           0 :         element.setAttribute("attribute-type", "CSS");
     157             :       }
     158           0 :       else if (this.type === "width" || this.type === "height") {
     159           0 :         element.setAttribute("from", this.$refs["from"]?.editValue ||
     160             :           this.getCurrentValue(this.type, "0"));
     161           0 :         this.addAttribute(element, "to");
     162           0 :         element.setAttribute("attribute-name", this.type);
     163           0 :         element.setAttribute("attribute-type", "CSS");
     164             :       }
     165           0 :       debug("new relation:", element);
     166           0 :       return element;
     167             :     },
     168             :     /**
     169             :      * @template T
     170             :      * @param {string} style style attribute for lookup
     171             :      * @param {T} defaultValue fallback value
     172             :      * @return {T|string}
     173             :      */
     174             :     getCurrentValue(style, defaultValue) {
     175           0 :       if (this.selection?.node()) {
     176           0 :         return this.selection.style(style) || defaultValue;
     177             :       }
     178           0 :       return defaultValue;
     179             :     }
     180             :   }
     181             : });
     182             : export default component;

Generated by: LCOV version 1.16