xref: /plugin/visualindex/script/prosemirror.js (revision 3c9c7f3beeea1dce712c368cb507b309c63f5d06)
1*3c9c7f3bSLORTET(function () {
2*3c9c7f3bSLORTET    function initializeVisualIndexProsemirror() {
3*3c9c7f3bSLORTET        if (window.__visualindexProsemirrorInitialized) return;
4*3c9c7f3bSLORTET        if (!window.Prosemirror || !window.Prosemirror.classes) return;
5*3c9c7f3bSLORTET        window.__visualindexProsemirrorInitialized = true;
6*3c9c7f3bSLORTET
7*3c9c7f3bSLORTET    const {classes: {MenuItem, AbstractMenuItemDispatcher}} = window.Prosemirror;
8*3c9c7f3bSLORTET    function hiddenMenuItem() {
9*3c9c7f3bSLORTET        return new MenuItem({
10*3c9c7f3bSLORTET            label: '',
11*3c9c7f3bSLORTET            render: () => {
12*3c9c7f3bSLORTET                const el = document.createElement('span');
13*3c9c7f3bSLORTET                el.style.display = 'none';
14*3c9c7f3bSLORTET                return el;
15*3c9c7f3bSLORTET            },
16*3c9c7f3bSLORTET            command: () => false
17*3c9c7f3bSLORTET        });
18*3c9c7f3bSLORTET    }
19*3c9c7f3bSLORTET
20*3c9c7f3bSLORTET    function shouldShowInEditorMenu() {
21*3c9c7f3bSLORTET        const raw = window.JSINFO &&
22*3c9c7f3bSLORTET            JSINFO.plugins &&
23*3c9c7f3bSLORTET            JSINFO.plugins.visualindex
24*3c9c7f3bSLORTET            ? JSINFO.plugins.visualindex.show_in_editor_menu
25*3c9c7f3bSLORTET            : true;
26*3c9c7f3bSLORTET
27*3c9c7f3bSLORTET        if (typeof raw === 'boolean') return raw;
28*3c9c7f3bSLORTET        const normalized = String(raw).trim().toLowerCase();
29*3c9c7f3bSLORTET        return !(normalized === '0' || normalized === 'false' || normalized === 'off' || normalized === 'no');
30*3c9c7f3bSLORTET    }
31*3c9c7f3bSLORTET
32*3c9c7f3bSLORTET    window.Prosemirror.pluginSchemas.push((nodes, marks) => {
33*3c9c7f3bSLORTET        nodes = nodes.addToEnd('visualindex', {
34*3c9c7f3bSLORTET            group: 'protected_block',
35*3c9c7f3bSLORTET            inline: false,
36*3c9c7f3bSLORTET            selectable: true,
37*3c9c7f3bSLORTET            draggable: true,
38*3c9c7f3bSLORTET            defining: true,
39*3c9c7f3bSLORTET            isolating: true,
40*3c9c7f3bSLORTET            code: true,
41*3c9c7f3bSLORTET            attrs: {
42*3c9c7f3bSLORTET                syntax: {default: '{{visualindex>.}}'}
43*3c9c7f3bSLORTET            },
44*3c9c7f3bSLORTET            toDOM: (node) => ['pre', {class: 'dwplugin', 'data-pluginname': 'visualindex'}, node.attrs.syntax],
45*3c9c7f3bSLORTET            parseDOM: [{
46*3c9c7f3bSLORTET                tag: 'pre.dwplugin[data-pluginname="visualindex"]',
47*3c9c7f3bSLORTET                getAttrs: (dom) => ({syntax: (dom.textContent || '{{visualindex>.}}').trim()})
48*3c9c7f3bSLORTET            }]
49*3c9c7f3bSLORTET        });
50*3c9c7f3bSLORTET        return {nodes, marks};
51*3c9c7f3bSLORTET    });
52*3c9c7f3bSLORTET
53*3c9c7f3bSLORTET    function parseVisualIndexSyntax(syntax) {
54*3c9c7f3bSLORTET        const m = (syntax || '').match(/^\{\{visualindex>(.*?)\}\}$/i);
55*3c9c7f3bSLORTET        if (!m) return null;
56*3c9c7f3bSLORTET
57*3c9c7f3bSLORTET        const parts = m[1].split(';').map((p) => p.trim()).filter(Boolean);
58*3c9c7f3bSLORTET        const namespace = parts.shift() || '.';
59*3c9c7f3bSLORTET        const options = {namespace, filter: '', desc: false, medias: false};
60*3c9c7f3bSLORTET
61*3c9c7f3bSLORTET        parts.forEach((part) => {
62*3c9c7f3bSLORTET            const [keyRaw, valRaw] = part.split('=', 2);
63*3c9c7f3bSLORTET            const key = (keyRaw || '').trim().toLowerCase();
64*3c9c7f3bSLORTET            const val = valRaw === undefined ? '1' : String(valRaw).trim();
65*3c9c7f3bSLORTET            if (key === 'filter') options.filter = val;
66*3c9c7f3bSLORTET            if (key === 'desc') options.desc = (val !== '0' && val !== 'false' && val !== '');
67*3c9c7f3bSLORTET            if (key === 'medias') options.medias = (val !== '0' && val !== 'false' && val !== '');
68*3c9c7f3bSLORTET        });
69*3c9c7f3bSLORTET
70*3c9c7f3bSLORTET        return options;
71*3c9c7f3bSLORTET    }
72*3c9c7f3bSLORTET
73*3c9c7f3bSLORTET    function buildVisualIndexSyntax(values) {
74*3c9c7f3bSLORTET        let syntax = `{{visualindex>${values.namespace || '.'}`;
75*3c9c7f3bSLORTET        if (values.filter) syntax += `;filter=${values.filter}`;
76*3c9c7f3bSLORTET        if (values.desc) syntax += ';desc=1';
77*3c9c7f3bSLORTET        if (values.medias) syntax += ';medias=1';
78*3c9c7f3bSLORTET        syntax += '}}';
79*3c9c7f3bSLORTET        return syntax;
80*3c9c7f3bSLORTET    }
81*3c9c7f3bSLORTET
82*3c9c7f3bSLORTET    function formatVisualIndexLabel(values) {
83*3c9c7f3bSLORTET        const parts = [`VisualIndex: ${values.namespace || '.'}`];
84*3c9c7f3bSLORTET        if (values.filter) parts.push(`filter=${values.filter}`);
85*3c9c7f3bSLORTET        if (values.desc) parts.push('desc');
86*3c9c7f3bSLORTET        if (values.medias) parts.push('medias');
87*3c9c7f3bSLORTET        return parts.join(' | ');
88*3c9c7f3bSLORTET    }
89*3c9c7f3bSLORTET
90*3c9c7f3bSLORTET    function getFolderIconUrl() {
91*3c9c7f3bSLORTET        const svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%232f6fae' d='M10 4l2 2h8a2 2 0 0 1 2 2v2H2V6a2 2 0 0 1 2-2h6z'/><path fill='%233f88c8' d='M2 10h20v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8z'/></svg>";
92*3c9c7f3bSLORTET        return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
93*3c9c7f3bSLORTET    }
94*3c9c7f3bSLORTET
95*3c9c7f3bSLORTET    function getFolderMenuIcon() {
96*3c9c7f3bSLORTET        const ns = 'http://www.w3.org/2000/svg';
97*3c9c7f3bSLORTET        const svg = document.createElementNS(ns, 'svg');
98*3c9c7f3bSLORTET        svg.setAttribute('viewBox', '0 0 24 24');
99*3c9c7f3bSLORTET
100*3c9c7f3bSLORTET        const path1 = document.createElementNS(ns, 'path');
101*3c9c7f3bSLORTET        path1.setAttribute('d', 'M10 4l2 2h8a2 2 0 0 1 2 2v2H2V6a2 2 0 0 1 2-2h6z');
102*3c9c7f3bSLORTET        path1.setAttribute('fill', 'currentColor');
103*3c9c7f3bSLORTET        svg.appendChild(path1);
104*3c9c7f3bSLORTET
105*3c9c7f3bSLORTET        const path2 = document.createElementNS(ns, 'path');
106*3c9c7f3bSLORTET        path2.setAttribute('d', 'M2 10h20v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8z');
107*3c9c7f3bSLORTET        path2.setAttribute('fill', 'currentColor');
108*3c9c7f3bSLORTET        svg.appendChild(path2);
109*3c9c7f3bSLORTET
110*3c9c7f3bSLORTET        return svg;
111*3c9c7f3bSLORTET    }
112*3c9c7f3bSLORTET
113*3c9c7f3bSLORTET    function isLegacyVisualIndexPluginNode(node) {
114*3c9c7f3bSLORTET        return !!(
115*3c9c7f3bSLORTET            node &&
116*3c9c7f3bSLORTET            node.type &&
117*3c9c7f3bSLORTET            (node.type.name === 'dwplugin_inline' || node.type.name === 'dwplugin_block') &&
118*3c9c7f3bSLORTET            node.attrs &&
119*3c9c7f3bSLORTET            node.attrs['data-pluginname'] === 'visualindex'
120*3c9c7f3bSLORTET        );
121*3c9c7f3bSLORTET    }
122*3c9c7f3bSLORTET
123*3c9c7f3bSLORTET    function isVisualIndexNode(node) {
124*3c9c7f3bSLORTET        return !!(node && node.type && node.type.name === 'visualindex') || isLegacyVisualIndexPluginNode(node);
125*3c9c7f3bSLORTET    }
126*3c9c7f3bSLORTET
127*3c9c7f3bSLORTET    function syntaxFromNode(node) {
128*3c9c7f3bSLORTET        if (!node) return '{{visualindex>.}}';
129*3c9c7f3bSLORTET        if (node.type && node.type.name === 'visualindex') {
130*3c9c7f3bSLORTET            return String((node.attrs && node.attrs.syntax) || '{{visualindex>.}}');
131*3c9c7f3bSLORTET        }
132*3c9c7f3bSLORTET        return String(node.textContent || '{{visualindex>.}}');
133*3c9c7f3bSLORTET    }
134*3c9c7f3bSLORTET
135*3c9c7f3bSLORTET    function createVisualIndexNode(schema, syntax) {
136*3c9c7f3bSLORTET        const normalized = String(syntax || '{{visualindex>.}}').trim() || '{{visualindex>.}}';
137*3c9c7f3bSLORTET        if (schema.nodes.visualindex) {
138*3c9c7f3bSLORTET            return schema.nodes.visualindex.createChecked({syntax: normalized});
139*3c9c7f3bSLORTET        }
140*3c9c7f3bSLORTET
141*3c9c7f3bSLORTET        const fallback = schema.nodes.dwplugin_block;
142*3c9c7f3bSLORTET        if (!fallback) return null;
143*3c9c7f3bSLORTET
144*3c9c7f3bSLORTET        return fallback.createChecked(
145*3c9c7f3bSLORTET            {class: 'dwplugin', 'data-pluginname': 'visualindex'},
146*3c9c7f3bSLORTET            schema.text(normalized)
147*3c9c7f3bSLORTET        );
148*3c9c7f3bSLORTET    }
149*3c9c7f3bSLORTET
150*3c9c7f3bSLORTET    function selectionIsVisualIndex(state) {
151*3c9c7f3bSLORTET        const selected = findVisualIndexAtSelection(state);
152*3c9c7f3bSLORTET        return !!selected;
153*3c9c7f3bSLORTET    }
154*3c9c7f3bSLORTET
155*3c9c7f3bSLORTET    function insertParagraphAfterSelectedVisualIndex(view) {
156*3c9c7f3bSLORTET        if (!view || !view.state) return false;
157*3c9c7f3bSLORTET        const selected = findVisualIndexAtSelection(view.state);
158*3c9c7f3bSLORTET        if (!selected) return false;
159*3c9c7f3bSLORTET
160*3c9c7f3bSLORTET        const {schema} = view.state;
161*3c9c7f3bSLORTET        const paragraph = schema.nodes.paragraph && schema.nodes.paragraph.createAndFill();
162*3c9c7f3bSLORTET        if (!paragraph) return false;
163*3c9c7f3bSLORTET
164*3c9c7f3bSLORTET        const insertPos = selected.pos + selected.node.nodeSize;
165*3c9c7f3bSLORTET        let tr = view.state.tr.insert(insertPos, paragraph).scrollIntoView();
166*3c9c7f3bSLORTET        view.dispatch(tr);
167*3c9c7f3bSLORTET
168*3c9c7f3bSLORTET        // Move cursor into the newly inserted paragraph.
169*3c9c7f3bSLORTET        try {
170*3c9c7f3bSLORTET            const SelectionClass = view.state.selection.constructor;
171*3c9c7f3bSLORTET            const $target = view.state.doc.resolve(insertPos + 1);
172*3c9c7f3bSLORTET            const selection = SelectionClass.near($target, 1);
173*3c9c7f3bSLORTET            view.dispatch(view.state.tr.setSelection(selection).scrollIntoView());
174*3c9c7f3bSLORTET        } catch (e) {
175*3c9c7f3bSLORTET            // Keep default selection if we can't safely resolve a text position.
176*3c9c7f3bSLORTET        }
177*3c9c7f3bSLORTET
178*3c9c7f3bSLORTET        view.focus();
179*3c9c7f3bSLORTET        return true;
180*3c9c7f3bSLORTET    }
181*3c9c7f3bSLORTET
182*3c9c7f3bSLORTET    function findVisualIndexAtSelection(state) {
183*3c9c7f3bSLORTET        const {selection} = state;
184*3c9c7f3bSLORTET
185*3c9c7f3bSLORTET        if (isVisualIndexNode(selection.node)) {
186*3c9c7f3bSLORTET            return {node: selection.node, pos: selection.from};
187*3c9c7f3bSLORTET        }
188*3c9c7f3bSLORTET
189*3c9c7f3bSLORTET        const $from = selection.$from;
190*3c9c7f3bSLORTET
191*3c9c7f3bSLORTET        if ($from.depth > 0 && isVisualIndexNode($from.parent)) {
192*3c9c7f3bSLORTET            return {node: $from.parent, pos: $from.before($from.depth)};
193*3c9c7f3bSLORTET        }
194*3c9c7f3bSLORTET
195*3c9c7f3bSLORTET        if (isVisualIndexNode($from.nodeBefore)) {
196*3c9c7f3bSLORTET            return {node: $from.nodeBefore, pos: $from.pos - $from.nodeBefore.nodeSize};
197*3c9c7f3bSLORTET        }
198*3c9c7f3bSLORTET
199*3c9c7f3bSLORTET        if (isVisualIndexNode($from.nodeAfter)) {
200*3c9c7f3bSLORTET            return {node: $from.nodeAfter, pos: $from.pos};
201*3c9c7f3bSLORTET        }
202*3c9c7f3bSLORTET
203*3c9c7f3bSLORTET        for (let depth = $from.depth; depth > 0; depth -= 1) {
204*3c9c7f3bSLORTET            const ancestor = $from.node(depth);
205*3c9c7f3bSLORTET            if (isVisualIndexNode(ancestor)) {
206*3c9c7f3bSLORTET                return {node: ancestor, pos: $from.before(depth)};
207*3c9c7f3bSLORTET            }
208*3c9c7f3bSLORTET        }
209*3c9c7f3bSLORTET
210*3c9c7f3bSLORTET        return null;
211*3c9c7f3bSLORTET    }
212*3c9c7f3bSLORTET
213*3c9c7f3bSLORTET    function insertVisualIndexBlock(view, pluginNode) {
214*3c9c7f3bSLORTET        const state = view.state;
215*3c9c7f3bSLORTET        const {$from} = state.selection;
216*3c9c7f3bSLORTET        const index = $from.index();
217*3c9c7f3bSLORTET
218*3c9c7f3bSLORTET        if ($from.parent.canReplaceWith(index, index, pluginNode.type)) {
219*3c9c7f3bSLORTET            view.dispatch(state.tr.replaceSelectionWith(pluginNode));
220*3c9c7f3bSLORTET            return true;
221*3c9c7f3bSLORTET        }
222*3c9c7f3bSLORTET
223*3c9c7f3bSLORTET        for (let depth = $from.depth; depth > 0; depth -= 1) {
224*3c9c7f3bSLORTET            const insertPos = $from.after(depth);
225*3c9c7f3bSLORTET            try {
226*3c9c7f3bSLORTET                view.dispatch(state.tr.insert(insertPos, pluginNode));
227*3c9c7f3bSLORTET                return true;
228*3c9c7f3bSLORTET            } catch (e) {
229*3c9c7f3bSLORTET                // try a higher ancestor
230*3c9c7f3bSLORTET            }
231*3c9c7f3bSLORTET        }
232*3c9c7f3bSLORTET
233*3c9c7f3bSLORTET        return false;
234*3c9c7f3bSLORTET    }
235*3c9c7f3bSLORTET
236*3c9c7f3bSLORTET    function showVisualIndexDialog(initialValues, onSubmit) {
237*3c9c7f3bSLORTET        const values = {
238*3c9c7f3bSLORTET            namespace: '.',
239*3c9c7f3bSLORTET            filter: '',
240*3c9c7f3bSLORTET            desc: false,
241*3c9c7f3bSLORTET            medias: false,
242*3c9c7f3bSLORTET            ...initialValues
243*3c9c7f3bSLORTET        };
244*3c9c7f3bSLORTET
245*3c9c7f3bSLORTET        const $dialog = jQuery('<div class="plugin_visualindex_form" title="Visualindex"></div>');
246*3c9c7f3bSLORTET        $dialog.append('<label>Namespace</label>');
247*3c9c7f3bSLORTET        const $namespace = jQuery('<input type="text" class="edit" style="width:100%;" />').val(values.namespace);
248*3c9c7f3bSLORTET        $dialog.append($namespace);
249*3c9c7f3bSLORTET        $dialog.append('<div style="font-size:.9em;color:#555;margin-top:4px;">Dossier. "." = dossier courant.</div>');
250*3c9c7f3bSLORTET
251*3c9c7f3bSLORTET        $dialog.append('<label style="display:block;margin-top:8px;">Filtre</label>');
252*3c9c7f3bSLORTET        const $filter = jQuery('<input type="text" class="edit" style="width:100%;" />').val(values.filter);
253*3c9c7f3bSLORTET        $dialog.append($filter);
254*3c9c7f3bSLORTET
255*3c9c7f3bSLORTET        const $descWrap = jQuery('<label style="display:block;margin-top:10px;"></label>');
256*3c9c7f3bSLORTET        const $desc = jQuery('<input type="checkbox" />').prop('checked', !!values.desc);
257*3c9c7f3bSLORTET        $descWrap.append($desc).append(' Ordre descendant');
258*3c9c7f3bSLORTET        $dialog.append($descWrap);
259*3c9c7f3bSLORTET
260*3c9c7f3bSLORTET        const $mediasWrap = jQuery('<label style="display:block;margin-top:6px;"></label>');
261*3c9c7f3bSLORTET        const $medias = jQuery('<input type="checkbox" />').prop('checked', !!values.medias);
262*3c9c7f3bSLORTET        $mediasWrap.append($medias).append(' Afficher les medias');
263*3c9c7f3bSLORTET        $dialog.append($mediasWrap);
264*3c9c7f3bSLORTET
265*3c9c7f3bSLORTET        $dialog.dialog({
266*3c9c7f3bSLORTET            modal: true,
267*3c9c7f3bSLORTET            width: 460,
268*3c9c7f3bSLORTET            close: function () {
269*3c9c7f3bSLORTET                jQuery(this).dialog('destroy').remove();
270*3c9c7f3bSLORTET            },
271*3c9c7f3bSLORTET            buttons: [
272*3c9c7f3bSLORTET                {
273*3c9c7f3bSLORTET                    text: 'Insérer',
274*3c9c7f3bSLORTET                    click: function () {
275*3c9c7f3bSLORTET                        onSubmit({
276*3c9c7f3bSLORTET                            namespace: String($namespace.val() || '.').trim() || '.',
277*3c9c7f3bSLORTET                            filter: String($filter.val() || '').trim(),
278*3c9c7f3bSLORTET                            desc: $desc.is(':checked'),
279*3c9c7f3bSLORTET                            medias: $medias.is(':checked')
280*3c9c7f3bSLORTET                        });
281*3c9c7f3bSLORTET                        jQuery(this).dialog('close');
282*3c9c7f3bSLORTET                    }
283*3c9c7f3bSLORTET                },
284*3c9c7f3bSLORTET                {
285*3c9c7f3bSLORTET                    text: 'Annuler',
286*3c9c7f3bSLORTET                    click: function () {
287*3c9c7f3bSLORTET                        jQuery(this).dialog('close');
288*3c9c7f3bSLORTET                    }
289*3c9c7f3bSLORTET                }
290*3c9c7f3bSLORTET            ]
291*3c9c7f3bSLORTET        });
292*3c9c7f3bSLORTET    }
293*3c9c7f3bSLORTET
294*3c9c7f3bSLORTET    class VisualIndexNodeView {
295*3c9c7f3bSLORTET        constructor(node, view, getPos) {
296*3c9c7f3bSLORTET            this.node = node;
297*3c9c7f3bSLORTET            this.view = view;
298*3c9c7f3bSLORTET            this.getPos = getPos;
299*3c9c7f3bSLORTET            this.dom = document.createElement('div');
300*3c9c7f3bSLORTET            const typeClass = (node.type && node.type.name === 'dwplugin_inline') ? 'pm_visualindex_inline' : 'pm_visualindex_block';
301*3c9c7f3bSLORTET            this.dom.className = 'plugin_visualindex pm_visualindex_node nodeHasForm ' + typeClass;
302*3c9c7f3bSLORTET            this.dom.setAttribute('contenteditable', 'false');
303*3c9c7f3bSLORTET            this.render();
304*3c9c7f3bSLORTET
305*3c9c7f3bSLORTET            this.dom.addEventListener('click', (event) => {
306*3c9c7f3bSLORTET                event.preventDefault();
307*3c9c7f3bSLORTET                event.stopPropagation();
308*3c9c7f3bSLORTET                this.openEditor();
309*3c9c7f3bSLORTET            });
310*3c9c7f3bSLORTET        }
311*3c9c7f3bSLORTET
312*3c9c7f3bSLORTET        render() {
313*3c9c7f3bSLORTET            const syntax = syntaxFromNode(this.node);
314*3c9c7f3bSLORTET            const parsed = parseVisualIndexSyntax(syntax);
315*3c9c7f3bSLORTET            const label = parsed ? formatVisualIndexLabel(parsed) : syntax;
316*3c9c7f3bSLORTET            this.dom.textContent = '';
317*3c9c7f3bSLORTET
318*3c9c7f3bSLORTET            const icon = document.createElement('img');
319*3c9c7f3bSLORTET            icon.className = 'pm_visualindex_icon';
320*3c9c7f3bSLORTET            icon.src = getFolderIconUrl();
321*3c9c7f3bSLORTET            icon.alt = '';
322*3c9c7f3bSLORTET            icon.setAttribute('aria-hidden', 'true');
323*3c9c7f3bSLORTET            this.dom.appendChild(icon);
324*3c9c7f3bSLORTET
325*3c9c7f3bSLORTET            const text = document.createElement('span');
326*3c9c7f3bSLORTET            text.textContent = label;
327*3c9c7f3bSLORTET            this.dom.appendChild(text);
328*3c9c7f3bSLORTET            this.dom.setAttribute('title', syntax);
329*3c9c7f3bSLORTET        }
330*3c9c7f3bSLORTET
331*3c9c7f3bSLORTET        openEditor() {
332*3c9c7f3bSLORTET            const parsed = parseVisualIndexSyntax(syntaxFromNode(this.node)) || {
333*3c9c7f3bSLORTET                namespace: '.',
334*3c9c7f3bSLORTET                filter: '',
335*3c9c7f3bSLORTET                desc: false,
336*3c9c7f3bSLORTET                medias: false
337*3c9c7f3bSLORTET            };
338*3c9c7f3bSLORTET
339*3c9c7f3bSLORTET            showVisualIndexDialog(parsed, (values) => {
340*3c9c7f3bSLORTET                const syntax = buildVisualIndexSyntax(values);
341*3c9c7f3bSLORTET                const replacement = createVisualIndexNode(this.view.state.schema, syntax);
342*3c9c7f3bSLORTET                if (!replacement) return;
343*3c9c7f3bSLORTET
344*3c9c7f3bSLORTET                const pos = this.getPos();
345*3c9c7f3bSLORTET                this.view.dispatch(this.view.state.tr.replaceWith(pos, pos + this.node.nodeSize, replacement));
346*3c9c7f3bSLORTET                this.view.focus();
347*3c9c7f3bSLORTET            });
348*3c9c7f3bSLORTET        }
349*3c9c7f3bSLORTET
350*3c9c7f3bSLORTET        update(node) {
351*3c9c7f3bSLORTET            if (!isVisualIndexNode(node)) return false;
352*3c9c7f3bSLORTET            this.node = node;
353*3c9c7f3bSLORTET            const typeClass = (node.type && node.type.name === 'dwplugin_inline') ? 'pm_visualindex_inline' : 'pm_visualindex_block';
354*3c9c7f3bSLORTET            this.dom.className = 'plugin_visualindex pm_visualindex_node nodeHasForm ' + typeClass;
355*3c9c7f3bSLORTET            this.render();
356*3c9c7f3bSLORTET            return true;
357*3c9c7f3bSLORTET        }
358*3c9c7f3bSLORTET
359*3c9c7f3bSLORTET        selectNode() { this.dom.classList.add('ProseMirror-selectednode'); }
360*3c9c7f3bSLORTET        deselectNode() { this.dom.classList.remove('ProseMirror-selectednode'); }
361*3c9c7f3bSLORTET        stopEvent() { return true; }
362*3c9c7f3bSLORTET        ignoreMutation() { return true; }
363*3c9c7f3bSLORTET    }
364*3c9c7f3bSLORTET
365*3c9c7f3bSLORTET    class VisualIndexMenuItemDispatcher extends AbstractMenuItemDispatcher {
366*3c9c7f3bSLORTET        static isAvailable(schema) {
367*3c9c7f3bSLORTET            return !!(schema.nodes.visualindex || schema.nodes.dwplugin_block);
368*3c9c7f3bSLORTET        }
369*3c9c7f3bSLORTET
370*3c9c7f3bSLORTET        static getIcon() {
371*3c9c7f3bSLORTET            const wrapper = document.createElement('span');
372*3c9c7f3bSLORTET            wrapper.className = 'menuicon';
373*3c9c7f3bSLORTET            wrapper.appendChild(getFolderMenuIcon());
374*3c9c7f3bSLORTET            return wrapper;
375*3c9c7f3bSLORTET        }
376*3c9c7f3bSLORTET
377*3c9c7f3bSLORTET        static getMenuItem(schema) {
378*3c9c7f3bSLORTET            if (!this.isAvailable(schema)) return hiddenMenuItem();
379*3c9c7f3bSLORTET
380*3c9c7f3bSLORTET            return new MenuItem({
381*3c9c7f3bSLORTET                label: 'VisualIndex',
382*3c9c7f3bSLORTET                icon: this.getIcon(),
383*3c9c7f3bSLORTET                command: (state, dispatch, view) => {
384*3c9c7f3bSLORTET                    const existing = findVisualIndexAtSelection(state);
385*3c9c7f3bSLORTET                    if (!dispatch || !view) return true;
386*3c9c7f3bSLORTET
387*3c9c7f3bSLORTET                    let initialValues = {namespace: '.', filter: '', desc: false, medias: false};
388*3c9c7f3bSLORTET                    if (existing) {
389*3c9c7f3bSLORTET                        const parsed = parseVisualIndexSyntax(syntaxFromNode(existing.node));
390*3c9c7f3bSLORTET                        if (parsed) initialValues = parsed;
391*3c9c7f3bSLORTET                    }
392*3c9c7f3bSLORTET
393*3c9c7f3bSLORTET                    showVisualIndexDialog(initialValues, (values) => {
394*3c9c7f3bSLORTET                        const syntax = buildVisualIndexSyntax(values);
395*3c9c7f3bSLORTET                        const pluginNode = createVisualIndexNode(schema, syntax);
396*3c9c7f3bSLORTET                        if (!pluginNode) return;
397*3c9c7f3bSLORTET
398*3c9c7f3bSLORTET                        if (existing) {
399*3c9c7f3bSLORTET                            view.dispatch(view.state.tr.replaceWith(existing.pos, existing.pos + existing.node.nodeSize, pluginNode));
400*3c9c7f3bSLORTET                        } else if (!insertVisualIndexBlock(view, pluginNode)) {
401*3c9c7f3bSLORTET                            const endPos = view.state.doc.content.size;
402*3c9c7f3bSLORTET                            view.dispatch(view.state.tr.insert(endPos, pluginNode));
403*3c9c7f3bSLORTET                        }
404*3c9c7f3bSLORTET
405*3c9c7f3bSLORTET                        view.focus();
406*3c9c7f3bSLORTET                    });
407*3c9c7f3bSLORTET
408*3c9c7f3bSLORTET                    return true;
409*3c9c7f3bSLORTET                }
410*3c9c7f3bSLORTET            });
411*3c9c7f3bSLORTET        }
412*3c9c7f3bSLORTET    }
413*3c9c7f3bSLORTET
414*3c9c7f3bSLORTET    if (shouldShowInEditorMenu()) {
415*3c9c7f3bSLORTET        window.Prosemirror.pluginMenuItemDispatchers.push(VisualIndexMenuItemDispatcher);
416*3c9c7f3bSLORTET    }
417*3c9c7f3bSLORTET    window.Prosemirror.pluginNodeViews.visualindex = (node, view, getPos) => new VisualIndexNodeView(node, view, getPos);
418*3c9c7f3bSLORTET
419*3c9c7f3bSLORTET    const originalInline = window.Prosemirror.pluginNodeViews.dwplugin_inline;
420*3c9c7f3bSLORTET    window.Prosemirror.pluginNodeViews.dwplugin_inline = (node, view, getPos) => {
421*3c9c7f3bSLORTET        if (isLegacyVisualIndexPluginNode(node)) return new VisualIndexNodeView(node, view, getPos);
422*3c9c7f3bSLORTET        return typeof originalInline === 'function' ? originalInline(node, view, getPos) : undefined;
423*3c9c7f3bSLORTET    };
424*3c9c7f3bSLORTET
425*3c9c7f3bSLORTET    const originalBlock = window.Prosemirror.pluginNodeViews.dwplugin_block;
426*3c9c7f3bSLORTET    window.Prosemirror.pluginNodeViews.dwplugin_block = (node, view, getPos) => {
427*3c9c7f3bSLORTET        if (isLegacyVisualIndexPluginNode(node)) return new VisualIndexNodeView(node, view, getPos);
428*3c9c7f3bSLORTET        return typeof originalBlock === 'function' ? originalBlock(node, view, getPos) : undefined;
429*3c9c7f3bSLORTET    };
430*3c9c7f3bSLORTET
431*3c9c7f3bSLORTET    if (!window.__visualindexKeyboardGuardInstalled) {
432*3c9c7f3bSLORTET        window.__visualindexKeyboardGuardInstalled = true;
433*3c9c7f3bSLORTET        document.addEventListener('keydown', (event) => {
434*3c9c7f3bSLORTET            const view = window.Prosemirror && window.Prosemirror.view;
435*3c9c7f3bSLORTET            if (!view || !view.state) return;
436*3c9c7f3bSLORTET            if (!selectionIsVisualIndex(view.state)) return;
437*3c9c7f3bSLORTET
438*3c9c7f3bSLORTET            if (event.key === 'Enter') {
439*3c9c7f3bSLORTET                event.preventDefault();
440*3c9c7f3bSLORTET                event.stopPropagation();
441*3c9c7f3bSLORTET                insertParagraphAfterSelectedVisualIndex(view);
442*3c9c7f3bSLORTET                return;
443*3c9c7f3bSLORTET            }
444*3c9c7f3bSLORTET
445*3c9c7f3bSLORTET            if (event.key === 'Backspace' || event.key === 'Delete') {
446*3c9c7f3bSLORTET                event.preventDefault();
447*3c9c7f3bSLORTET                event.stopPropagation();
448*3c9c7f3bSLORTET            }
449*3c9c7f3bSLORTET        }, true);
450*3c9c7f3bSLORTET    }
451*3c9c7f3bSLORTET    }
452*3c9c7f3bSLORTET
453*3c9c7f3bSLORTET    jQuery(document).on('PROSEMIRROR_API_INITIALIZED', initializeVisualIndexProsemirror);
454*3c9c7f3bSLORTET    initializeVisualIndexProsemirror();
455*3c9c7f3bSLORTET})();
456