Line data Source code
1 : // @ts-check
2 :
3 : import Vuex from "vuex";
4 : import Vue from "vue";
5 : import { forEach, isString, merge } from "lodash";
6 : import createPersistedState from "vuex-persistedstate";
7 : import d from "debug";
8 : import {
9 : createStore,
10 : storeOptions
11 : } from "@cern/base-vue";
12 :
13 : import ui from "./modules/ui";
14 : import {
15 : default as selection,
16 : plugin as selectionPlugin
17 : } from "./modules/selection";
18 : import loggerPlugin from "./modules/logger";
19 : import engine from "./modules/engine";
20 : import VuexHistory from "./modules/history";
21 :
22 : import { setAttribute } from "../utils/element";
23 :
24 1 : const debug = d("app:store");
25 :
26 1 : Vue.use(Vuex);
27 :
28 : /**
29 : * @typedef {V.ActionContext<AppStore.State>} ActionContext
30 : * @typedef {{
31 : * stateIndex: number, propertyName: string, relationIndex: number,
32 : * isTransform: boolean
33 : * }} SavedSelection
34 : */
35 :
36 : /**
37 : * @param {Element|string} value
38 : */
39 : function createFragment(value) {
40 1 : if (isString(value)) {
41 : // document.createRange().createContextualFragment(value) would use HTML NS
42 0 : const frag = document.createElementNS("http://www.w3.org/2000/svg", "svg");
43 0 : frag.innerHTML = value;
44 0 : if (!frag || !frag.firstElementChild) { throw new Error(`invalid fragment: ${value}`); }
45 0 : return frag.firstElementChild;
46 : }
47 1 : return value;
48 : }
49 :
50 : /** @type {V.Store<AppStore.State>} */
51 1 : merge(storeOptions, /** @type {V.Module<AppStore.State>} */({
52 : state: {
53 : },
54 : modules: {
55 : engine, selection, ui
56 : },
57 : actions: {
58 : /**
59 : * @brief add element to currently selected state
60 : * @param {Element|string} value
61 : */
62 : async add(context, value) {
63 1 : return Promise.resolve()
64 1 : .then(() => createFragment(value))
65 : .then((value) => {
66 1 : const frag = /** @type {Element} */ (context.state.engine.fragment?.cloneNode(true));
67 1 : const selection = context.getters["selection/getFragmentSelection"](frag);
68 1 : if (value.tagName === "property" || value.tagName === "computed") {
69 1 : if (!selection?.state) { throw new Error("no state selected"); }
70 1 : selection.state.append(value);
71 1 : debug("adding property");
72 : }
73 0 : else if (value.tagName === "transition") {
74 0 : if (!selection?.state) { throw new Error("no state selected"); }
75 0 : if (selection?.relation) {
76 0 : selection.relation.append(value);
77 : }
78 : else {
79 0 : selection.state.prepend(value);
80 : }
81 0 : debug("adding transition");
82 : }
83 : else {
84 0 : if (!selection?.property) { throw new Error("no property selected"); }
85 0 : selection.property.append(value);
86 0 : debug("adding relation");
87 : }
88 1 : return context.dispatch("engine/loadFragment", frag);
89 : });
90 : },
91 : /**
92 : * @brief remove currently selected property or relation
93 : * @param {"property"|"relation"|"transition"|"computed"} what
94 : */
95 : async remove(context, what) {
96 0 : return Promise.resolve()
97 : .then(() => {
98 0 : const frag = /** @type {Element} */ (context.state.engine.fragment?.cloneNode(true));
99 0 : const selection = context.getters["selection/getFragmentSelection"](frag);
100 0 : if (what === "property" || what === "computed") {
101 0 : if (!selection.property) { throw new Error("no property selected"); }
102 0 : debug("removing property");
103 0 : selection.property.remove();
104 : }
105 0 : else if (what === "relation") {
106 0 : if (!selection.relation) { throw new Error("no relation selected"); }
107 0 : debug("removing relation");
108 0 : selection.relation.remove();
109 : }
110 0 : else if (what === "transition") {
111 0 : if (!selection.transition) { throw new Error("no transition selected"); }
112 0 : debug("removing transition");
113 0 : selection.transition.remove();
114 : }
115 : else {
116 0 : throw new Error(`unknown remove request: ${what}`);
117 : }
118 0 : return context.dispatch("engine/loadFragment", frag);
119 : });
120 : },
121 : /**
122 : * @brief edit properties on currently selected property or relation
123 : * @param {{ what: "property"|"relation"|"transition"|"computed", values: any }} params
124 : */
125 : async edit(context, params) {
126 0 : return Promise.resolve()
127 : .then(() => {
128 0 : const frag = /** @type {Element} */ (context.state.engine.fragment?.cloneNode(true));
129 0 : const selection = context.getters["selection/getFragmentSelection"](frag);
130 0 : if (params.what === "property" || params.what === "computed") {
131 0 : if (!selection.property) { throw new Error("no property selected"); }
132 0 : forEach(params.values, (value, name) => {
133 0 : setAttribute(selection.property, name, value);
134 : });
135 : }
136 0 : else if (params.what === "relation") {
137 0 : if (!selection.relation) { throw new Error("no relation selected"); }
138 0 : forEach(params.values, (value, name) => {
139 0 : setAttribute(selection.relation, name, value);
140 : });
141 : }
142 : else {
143 0 : throw new Error(`unknown edit request: ${params.what}`);
144 : }
145 0 : debug("editing $o", params);
146 0 : return context.dispatch("engine/loadFragment", frag);
147 : });
148 : },
149 : /**
150 : *
151 : * @param {Element|string} value
152 : */
153 : async replace(context, value) {
154 0 : return Promise.resolve()
155 0 : .then(() => createFragment(value))
156 : .then((value) => {
157 0 : const frag = /** @type {Element} */ (context.state.engine.fragment?.cloneNode(true));
158 0 : const selection = context.getters["selection/getFragmentSelection"](frag);
159 0 : if (value.tagName === "property" || value.tagName === "computed") {
160 0 : if (!selection?.property) { throw new Error("no property selected"); }
161 0 : debug("replacing property");
162 0 : selection.property.replaceWith(value);
163 : }
164 0 : else if (value.tagName === "transition") {
165 0 : if (!selection?.transition) { throw new Error("no transition selected"); }
166 0 : debug("replacing transition");
167 0 : selection.transition.replaceWith(value);
168 : }
169 : else {
170 0 : if (!selection?.relation) { throw new Error("no relation selected"); }
171 0 : debug("replacing relation");
172 0 : selection.relation.replaceWith(value);
173 : }
174 :
175 0 : return context.dispatch("engine/loadFragment", frag);
176 : });
177 : }
178 : },
179 : plugins: [
180 : createPersistedState({
181 : key: `${window.location.pathname}/ssvg-editor/vuex`,
182 : paths: [ "ui.showKeyHints", "ui.viewMode", "engine.text", "selection.path" ],
183 : rehydrated(store) {
184 0 : store.dispatch("engine/init");
185 : }
186 : }),
187 : selectionPlugin,
188 : loggerPlugin
189 : ]
190 : }));
191 1 : const store = createStore();
192 1 : export const History = new VuexHistory(store, "engine.text", 20);
193 1 : History.on("item", (data) => store.dispatch("engine/load", data));
194 :
195 : export default store;
|