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