Line data Source code
1 : // @ts-check
2 :
3 : import Vue from "vue";
4 : import { mapState } from "vuex";
5 : import { get, map, noop } from "lodash";
6 :
7 : import { xml } from "@codemirror/lang-xml";
8 : import { Compartment, StateEffect } from "@codemirror/state";
9 : import { foldAll } from "@codemirror/language";
10 : import { closeBrackets, insertCompletionText } from "@codemirror/autocomplete";
11 : import { linter, openLintPanel } from "@codemirror/lint";
12 :
13 : import CMColorPlugin from "../../utils/CMColorPlugin";
14 : import { makeSelectorPlugin, selectorTheme } from "../../utils/CMSelectorPlugin";
15 : import CodeMirrorMixin from "../CodeMirrorMixin";
16 : import { bracketConf } from "../CodeMirrorMixinInit";
17 : import { ssvgElements, ssvgLint, ssvgPrettyPrint } from "../../utils/SSVGLint";
18 : import SVGSelector from "../SVGSelector.vue";
19 :
20 : /**
21 : * @typedef {import("@codemirror/autocomplete").Completion} Completion
22 : * @typedef {import("@codemirror/view").EditorView} EditorView
23 : *
24 : * @typedef {{ syntax: Compartment, schema: typeof schema }} Opts
25 : * @typedef {{ cm: HTMLElement, selector: V.Instance<typeof SVGSelector> }} Refs
26 : */
27 :
28 1 : const schema = [
29 : { ...ssvgElements.relation, top: true },
30 : { ...ssvgElements.transform, top: true },
31 : { ...ssvgElements.direct, top: true },
32 : { ...ssvgElements.transition }
33 : ];
34 1 : const languageCompartment = new Compartment();
35 :
36 1 : const component = /** @type {V.Constructor<Opts, Refs>}*/(Vue).extend({
37 : name: "SSVGCodeEditor",
38 : components: { SVGSelector },
39 : mixins: [ CodeMirrorMixin({
40 : extensions: [
41 0 : linter((view) => ssvgLint(view.state?.doc.toString())),
42 : languageCompartment.of([]),
43 : CMColorPlugin
44 : ]
45 : }) ],
46 : /** @return {{ isSelecting: boolean }} */
47 : data() {
48 4 : return { isSelecting: false };
49 : },
50 : computed: {
51 : .../** @type {{ relation(): Element|null }} */(mapState("selection", [ "relation" ])),
52 : .../** @type {{ svgSelectors(): any }} */(mapState("engine", {
53 4 : svgSelectors: (state) => get(state, [ "svgSelectors" ])
54 : })),
55 : /** @return {string} */
56 : currentContent() {
57 5 : return (this.relation) ? ssvgPrettyPrint(this.relation.outerHTML) : "";
58 : }
59 : },
60 : watch: {
61 2 : currentContent(val) { this.setContent(val); }
62 : },
63 : mounted() {
64 4 : const cm = this.getEditor();
65 4 : cm?.dispatch({ effects: bracketConf.reconfigure(closeBrackets()) });
66 4 : cm?.dispatch({
67 : effects: StateEffect.appendConfig.of([
68 : makeSelectorPlugin(this.getSelector), selectorTheme
69 : ])
70 : });
71 :
72 4 : this.$options.schema = schema;
73 4 : this.updateLanguage();
74 4 : this.setContent(this.currentContent);
75 4 : foldAll(cm);
76 : },
77 : methods: {
78 : updateLanguage() {
79 5 : const attributes = [
80 : {
81 : name: "query-selector", values: [
82 : { label: "select", detail: "on document", apply: this.updateSelector, type: "selector", boost: 1 },
83 18 : ...map(this.svgSelectors, (s) => ({ label: s, type: "text" }))
84 : ]
85 : }
86 : ];
87 5 : this.getEditor().dispatch({ effects: languageCompartment.reconfigure(xml({ elements: this.$options.schema, attributes })) });
88 : },
89 : /** @returns {Promise<string>} */
90 : async getSelector() {
91 0 : this.isSelecting = true;
92 0 : let selector = "";
93 0 : try {
94 0 : selector = await this.$refs.selector?.select();
95 0 : if (!selector) {
96 0 : throw new Error("No selector found");
97 : }
98 : }
99 0 : catch (e) { console.log("getSelector error", e); }
100 0 : this.isSelecting = false;
101 0 : return selector;
102 : },
103 : /**
104 : * @param {EditorView} view
105 : * @param {Completion} completion
106 : * @param {number} from
107 : * @param {number} to
108 : */
109 : async updateSelector(view, completion, from, to) {
110 0 : let selector = await this.getSelector();
111 0 : if (selector) {
112 0 : selector = `"${selector}"`;
113 0 : view.dispatch(insertCompletionText(view.state, selector, from, to));
114 : }
115 : },
116 : async doApply() {
117 0 : if (!this.relation) { /* noop */ }
118 0 : else if (this.$refs.cm?.getElementsByClassName("cm-lint-marker-error").length > 0) {
119 0 : openLintPanel(this.getEditor());
120 0 : return false;
121 : }
122 : else {
123 0 : await this.$store.dispatch("replace", this.editValue).catch(noop);
124 0 : return true;
125 : }
126 : }
127 : }
128 : });
129 : export default component;
|