LCOV - code coverage report
Current view: top level - src/components/SSVGPropList - CreateTransformDialog.vue.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 7 99 7.1 %
Date: 2025-06-29 02:18:36 Functions: 5 22 22.7 %

          Line data    Source code
       1             : // @ts-check
       2             : 
       3             : import Vue from "vue";
       4             : import { mapState } from "vuex";
       5             : import draggable from "vuedraggable";
       6             : import { parser } from "@cern/ssvg-engine";
       7             : import { select } from "d3-selection";
       8             : import d from "debug";
       9             : import { cloneDeep, fill, forEach, get, toString } from "lodash";
      10             : import { BaseKeyboardEventMixin as KBMixin } from "@cern/base-vue";
      11             : 
      12             : import { genId } from "../../utils";
      13             : import { transformArgsStr } from "../../utils/ssvg";
      14             : import { checkTransformRange, transformList } from "../../utils/SSVGLint";
      15             : import Input from "../Input.vue";
      16             : import ErrorList from "../ErrorList.vue";
      17             : import InfoAlert from "../InfoAlert.vue";
      18             : 
      19           1 : const debug = d("app:edit");
      20             : 
      21             : /**
      22             :  * @typedef {{ custom?: boolean }} TransformOpts
      23             :  * @typedef {import("d3-selection").BaseType} BaseType
      24             :  * @typedef {import("d3-selection").Selection<BaseType, unknown, Element, any>} Selection
      25             :  */
      26             : 
      27             : /**
      28             :  * @typedef {typeof import("@cern/base-vue").BaseDialog} BaseDialog
      29             :  * @typedef {{
      30             :  *  dialog: V.Instance<BaseDialog>,
      31             :  *  from: V.Instance<Input>[],
      32             :  *  to: V.Instance<Input>[],
      33             :  *  errList: V.Instance<ErrorList>
      34             :  * }} Refs
      35             :  * @typedef {{ transformList: string[] }} Opts
      36             :  */
      37             : 
      38           1 : const component = /** @type {V.Constructor<any, Refs>} */(Vue).extend({
      39             :   name: "CreateTransformDialog",
      40             :   /* eslint-disable-next-line vue/no-reserved-component-names -- Input */
      41             :   components: { Input, ErrorList, draggable, InfoAlert },
      42             :   mixins: [ KBMixin({ local: true }) ],
      43             :   props: { query: { type: String, default: null } },
      44             :   ...{ transformList },
      45             :   /**
      46             :    * @return {{
      47             :    * svgAttributesId: string, isValid: boolean,
      48             :    * selection: Selection|null,
      49             :    * reqId: number,
      50             :    * transforms: (ssvg.$TransformRange & TransformOpts)[],
      51             :    * showInfo: boolean, isFocused: boolean }}
      52             :    */
      53             :   data() {
      54          12 :     return {
      55             :       svgAttributesId: genId(),
      56             :       isValid: true,
      57             :       selection: null,
      58             :       reqId: 0,
      59             :       transforms: [],
      60             :       showInfo: false,
      61             :       isFocused: false
      62             :     };
      63             :   },
      64             :   computed: {
      65          17 :     .../** @type {{ svg(): Element|null }} */(mapState("engine", { svg(state) { return get(state, "directEngine.svg"); } })),
      66             :     .../** @type {{ showKeyHints(): boolean }} */(mapState("ui", [ "showKeyHints" ]))
      67             :   },
      68             :   watch: {
      69           5 :     svg() { this.$nextTick(this.onUpdate); },
      70           0 :     query() { this.$nextTick(this.onUpdate); }
      71             :   },
      72             :   mounted() {
      73          12 :     this.onKey("ctrl-h-keydown", (/** @type {Event} */ e) => {
      74           0 :       if (this.isFocused) {
      75           0 :         e.stopImmediatePropagation();
      76           0 :         e.preventDefault();
      77           0 :         this.showInfo = !this.showInfo;
      78             :       }
      79             :     });
      80             :   },
      81             :   methods: {
      82             :     async request() {
      83           0 :       this.reqId += 1;
      84           0 :       this.loadTransform();
      85           0 :       if (!(await this.$refs.dialog.request())) { return null; }
      86           0 :       return this.createTransform();
      87             :     },
      88             :     async checkValidity() { /* eslint-disable-line complexity */
      89           0 :       if (!this.query) { return; }
      90           0 :       let valid = true;
      91             : 
      92           0 :       for (const w of this.$refs["from"] ?? []) {
      93           0 :         valid = await (w?.checkValidity() ?? true) && valid;
      94             :       }
      95           0 :       for (const w of this.$refs["to"] ?? []) {
      96           0 :         valid = await (w?.checkValidity() ?? true) && valid;
      97             :       }
      98             : 
      99           0 :       if (!this.transforms) {
     100           0 :         this.$refs["errList"]?.addError("x-empty", "please add a transform");
     101           0 :         valid = false;
     102             :       }
     103             :       else {
     104           0 :         this.$refs["errList"]?.removeError("x-empty");
     105             :       }
     106             : 
     107           0 :       this.isValid = valid;
     108             :     },
     109             :     loadTransform() {
     110           0 :       const current = this.getCurrentValue();
     111           0 :       if (current) {
     112           0 :         this.transforms = parser.normalizeTransform([ current, [] ]);
     113             :       }
     114             :       else {
     115           0 :         this.addTransform();
     116             :       }
     117             :     },
     118             :     onUpdate() {
     119           5 :       if (!this.svg || !this.query) { return; }
     120           0 :       this.selection = select(this.svg).selectAll(this.query);
     121           0 :       if (!this.selection?.node()) {
     122           0 :         this.$refs["errList"]?.addWarning("x-warn", "failed to select nodes");
     123             :       }
     124           0 :       this.checkValidity();
     125             :     },
     126             :     /**
     127             :      * @param {Element} element
     128             :      * @param {string} name
     129             :      * @param {string | null} defaultValue
     130             :      */
     131             :     addAttribute(element, name, defaultValue = null) {
     132           0 :       const value = /** @type {V.Instance<Input>} */(this.$refs[name])?.editValue;
     133           0 :       if (value || defaultValue !== null) {
     134           0 :         element.setAttribute(name, value ?? defaultValue ?? "");
     135             :       }
     136           0 :       return element;
     137             :     },
     138             :     createTransform() {
     139           0 :       const element = document.createElementNS("http://www.w3.org/2000/svg", "transform");
     140           0 :       element.setAttribute("query-selector", this.query);
     141           0 :       element.setAttribute("attribute-name", "transform");
     142           0 :       this.addAttribute(element, "attribute-type", "CSS");
     143           0 :       this.addAttribute(element, "calc-mode");
     144           0 :       element.setAttribute("from", this.genTransformStep(0));
     145           0 :       element.setAttribute("to", this.genTransformStep(1));
     146           0 :       return element;
     147             :     },
     148             :     /**
     149             :      * @return {ssvg.$Transform[]|null}
     150             :      */
     151             :     getCurrentValue() {
     152           0 :       if (!this.selection?.node()) { return null; }
     153             : 
     154           0 :       try {
     155           0 :         return parser.parseTTransform(
     156             :           this.selection.style("transform") ?? this.selection.attr("transform"));
     157             :       }
     158             :       catch (err) {
     159           0 :         console.warn("Failed to parse transform: " + err);
     160             :       }
     161           0 :       return null;
     162             :     },
     163             :     /**
     164             :      * @param  {number[]} args
     165             :      * @param  {(string|null)[]} units
     166             :      * @return {string}
     167             :      */
     168             :     transformArgsStr(args, units) {
     169           0 :       return transformArgsStr(args, units);
     170             :     },
     171             :     /**
     172             :      * @param {number} transformIndex
     173             :      * @param {number} argIndex
     174             :      * @param {string} value
     175             :      */
     176             :     updateArgs(transformIndex, argIndex, value) {
     177           0 :       const transform = cloneDeep(this.transforms[transformIndex]);
     178             :       /** @type {V.Instance<Input>} */
     179           0 :       const argWidget = get(this.$refs, [ argIndex ? "to" : "from", transformIndex ]);
     180             : 
     181           0 :       if (!value) { // widget reset
     182           0 :         argWidget?.removeError("x-parse");
     183           0 :         argWidget?.removeWarning("x-parse");
     184           0 :         this.checkValidity();
     185           0 :         return;
     186             :       }
     187             : 
     188           0 :       try {
     189           0 :         const args = parser.parseTTransform(`${transform.transform}(${value})`)?.[0];
     190           0 :         if (!args) { throw new Error("unknown error"); }
     191             : 
     192           0 :         transform.args[argIndex] = args.args;
     193           0 :         transform.units = args.units;
     194           0 :         forEach(transform.args, (arg) => {
     195           0 :           const len = arg.length;
     196           0 :           arg.length = args.args.length;
     197           0 :           fill(arg, 0, len, args.args.length);
     198             :         });
     199           0 :         try {
     200           0 :           if (!transform.custom) {
     201           0 :             checkTransformRange(transform);
     202             :           }
     203           0 :           argWidget?.removeWarning("x-parse");
     204             :         }
     205             :         catch (err) {
     206           0 :           argWidget?.addWarning("x-parse", toString(err));
     207             :         }
     208           0 :         argWidget?.removeError("x-parse");
     209           0 :         this.checkValidity();
     210           0 :         Vue.set(this.transforms, transformIndex, transform);
     211             :       }
     212             :       catch (err) {
     213           0 :         argWidget?.removeWarning("x-parse");
     214           0 :         argWidget?.addError("x-parse", "Failed to parse transform: " + err);
     215           0 :         this.isValid = false;
     216           0 :         debug("Failed to parse transform: ", err);
     217             :       }
     218             :     },
     219             :     /**
     220             :      * @param {number} transformIndex
     221             :      * @param {string} value
     222             :      */
     223             :     updateTransform(transformIndex, value) {
     224           0 :       const transform = cloneDeep(this.transforms[transformIndex]);
     225             : 
     226           0 :       if (value === "custom") {
     227           0 :         transform.custom = true;
     228           0 :         this.transforms[transformIndex] = transform;
     229           0 :         this.updateArgs(transformIndex, 0, "");
     230           0 :         this.updateArgs(transformIndex, 1, "");
     231           0 :         return;
     232             :       }
     233             :       else {
     234           0 :         transform.transform = value;
     235           0 :         this.transforms[transformIndex] = transform;
     236           0 :         this.updateArgs(transformIndex, 0,
     237             :           this.transformArgsStr(transform.args[0], transform.units));
     238             :       }
     239             :     },
     240             :     /**
     241             :      * @param {number} transformIndex
     242             :      */
     243             :     removeAt(transformIndex) {
     244           0 :       const transforms = cloneDeep(this.transforms);
     245           0 :       transforms?.splice(transformIndex, 1);
     246           0 :       this.transforms = transforms;
     247             :     },
     248             :     addTransform() {
     249           0 :       const transforms = cloneDeep(this.transforms);
     250           0 :       transforms.push({
     251             :         transform: "translate",
     252             :         args: [ [ 0, 0 ], [ 100, 100 ] ],
     253             :         units: [ "px", "px" ]
     254             :       });
     255           0 :       this.transforms = transforms;
     256             :     },
     257             :     /**
     258             :      * @param {number} step
     259             :      */
     260             :     genTransformStep(step) {
     261           0 :       return this.transforms.map(
     262           0 :         (tr) => `${tr.transform}(${tr.units.map((u, idx) => (tr.args[step][idx] + (u ?? ""))).join(", ")})`
     263             :       ).join(", ");
     264             :     }
     265             :   }
     266             : });
     267             : export default component;

Generated by: LCOV version 1.16