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