1jQuery(document).on('PROSEMIRROR_API_INITIALIZED', function () {
2
3    /**
4     * Add our node to the document schema of the Prosemirror plugin
5     *
6     * @param {OrderedMap} nodes the nodes already in the schema
7     * @param {OrderedMap} marks the marks already in the schema
8     *
9     * @returns {{nodes: OrderedMap, marks: OrderedMap}} the updated nodes and marks
10     */
11    function addGallerySchema(nodes, marks) {
12        nodes = nodes.addToEnd('dwplugin_gallery', {
13            content: '', // there is no content here -- it is all attributes
14            marks: '',
15            attrs: jQuery.extend(
16                {
17                    renderedHTML: {default: null},
18                },
19                JSINFO.plugins.gallery.defaults
20            ),
21            group: 'protected_block', // may go into a block quote or list, but not into a table
22        });
23
24        return {nodes: nodes, marks: marks};
25    }
26
27    window.Prosemirror.pluginSchemas.push(addGallerySchema);
28
29    /**
30     * Get the fields for the key value form with values
31     *
32     * @param {object} attrs the values to use
33     * @return {Object[]} array with parameters
34     */
35    function getGalleryFormFields(attrs) {
36        return [
37            {
38                name: 'namespace',
39                label: LANG.plugins.gallery.label_namespace,
40                type: 'text',
41                value: attrs['namespace'],
42            },
43            {
44                name: 'thumbnailsize',
45                label: LANG.plugins.gallery.label_thumbnailsize,
46                type: 'text',
47                pattern: '\\d+x\\d+',
48                title: LANG.plugins.gallery.pattern_hint_thumbnailsize,
49                value: attrs['thumbnailsize'],
50            },
51            {
52                name: 'imagesize',
53                label: LANG.plugins.gallery.label_imagesize,
54                type: 'text',
55                pattern: '\\d+X\\d+',
56                title: LANG.plugins.gallery.pattern_hint_imagesize,
57                value: attrs['imagesize'],
58            },
59            {
60                name: 'cache',
61                label: LANG.plugins.gallery.label_cache,
62                type: 'checkbox',
63                value: '1',
64                checked: attrs['cache'],
65            },
66            {
67                name: 'filter',
68                label: LANG.plugins.gallery.label_filter,
69                type: 'text',
70                value: attrs['filter'],
71            },
72            {
73                name: 'showname',
74                label: LANG.plugins.gallery.label_showname,
75                type: 'checkbox',
76                value: '1',
77                checked: attrs['showname'],
78            },
79            {
80                name: 'showtitle',
81                label: LANG.plugins.gallery.label_showtitle,
82                type: 'checkbox',
83                value: '1',
84                checked: attrs['showtitle'],
85            },
86            {
87                name: 'crop',
88                label: LANG.plugins.gallery.label_crop,
89                type: 'checkbox',
90                value: '1',
91                checked: attrs['crop'],
92            },
93            {
94                name: 'direct',
95                label: LANG.plugins.gallery.label_direct,
96                type: 'checkbox',
97                value: '1',
98                checked: attrs['direct'],
99            },
100            {
101                name: 'lightbox',
102                label: LANG.plugins.gallery.label_lightbox,
103                type: 'checkbox',
104                value: '1',
105                checked: attrs['lightbox'],
106            },
107            {
108                name: 'reverse',
109                label: LANG.plugins.gallery.label_reverse,
110                type: 'checkbox',
111                value: '1',
112                checked: attrs['reverse'],
113            },
114            {
115                name: 'recursive',
116                label: LANG.plugins.gallery.label_recursive,
117                type: 'checkbox',
118                value: '1',
119                checked: attrs['recursive'],
120            },
121            {
122                name: 'align',
123                label: LANG.plugins.gallery.label_align_left,
124                type: 'radio',
125                value: 'left',
126                checked: attrs['align'] === 'left',
127            },
128            {
129                name: 'align',
130                label: LANG.plugins.gallery.label_align_center,
131                type: 'radio',
132                value: 'center',
133                checked: attrs['align'] === 'center',
134            },
135            {
136                name: 'align',
137                label: LANG.plugins.gallery.label_align_right,
138                type: 'radio',
139                value: 'right',
140                checked: attrs['align'] === 'right',
141            },
142            {
143                name: 'cols',
144                label: LANG.plugins.gallery.label_cols,
145                type: 'number',
146                value: attrs['cols'],
147                min: 0,
148            },
149            {
150                name: 'limit',
151                label: LANG.plugins.gallery.label_limit,
152                type: 'number',
153                value: attrs['limit'],
154                min: 0,
155            },
156            {
157                name: 'offset',
158                label: LANG.plugins.gallery.label_offset,
159                type: 'number',
160                value: attrs['offset'],
161                min: 0,
162            },
163            {
164                name: 'paginate',
165                label: LANG.plugins.gallery.label_paginate,
166                type: 'number',
167                value: attrs['paginate'],
168                min: 0,
169            },
170            {
171                name: 'sort',
172                label: LANG.plugins.gallery.label_sort_file,
173                type: 'radio',
174                value: 'filesort',
175                checked: attrs['sort'] === 'filesort',
176            },
177            {
178                name: 'sort',
179                label: LANG.plugins.gallery.label_sort_random,
180                type: 'radio',
181                value: 'random',
182                checked: attrs['sort'] === 'random',
183            },
184            {
185                name: 'sort',
186                label: LANG.plugins.gallery.label_sort_mod,
187                type: 'radio',
188                value: 'modsort',
189                checked: attrs['sort'] === 'modsort',
190            },
191            {
192                name: 'sort',
193                label: LANG.plugins.gallery.label_sort_exif_date,
194                type: 'radio',
195                value: 'datesort',
196                checked: attrs['sort'] === 'datesort',
197            },
198            {
199                name: 'sort',
200                label: LANG.plugins.gallery.label_sort_exif_title,
201                type: 'radio',
202                value: 'titlesort',
203                checked: attrs['sort'] === 'titlesort',
204            },
205        ];
206    }
207
208
209    /**
210     * Handle our submitted form
211     *
212     * @param {Event} event the submit event
213     *
214     * @return {void}
215     */
216    function handleFormSubmit(event) {
217        event.preventDefault();
218        event.stopPropagation();
219        var newAttrs = jQuery(event.target).serializeArray().reduce(function (acc, cur) {
220            acc[cur['name']] = cur['value'];
221            return acc;
222        }, {});
223        var nodeStartPos = this.getPos();
224
225        // we must have the unchecked checkboxes with default = true
226        // explicitly listed as false, they cannot just be missing
227        // similar might be (multi-)selects that have options selected by default
228        newAttrs = Object.entries(this.node.type.attrs).reduce(
229            function (acc, cur) {
230                if (acc[cur[0]]) {
231                    return acc;
232                }
233                if (cur[1].default && cur[1].default === true) {
234                    acc[cur[0]] = false;
235                }
236                return acc;
237            },
238            newAttrs
239        );
240
241        this.form.hide();
242        this.outerView.dispatch(
243            this.outerView.state.tr
244                .setNodeMarkup(
245                    nodeStartPos,
246                    null,
247                    newAttrs,
248                    this.node.marks
249                )
250        );
251    }
252
253    /**
254     * Send this node's attributes to the server to get the rendered html back
255     *
256     * @param {object} attrs
257     * @param {GalleryNodeView} nodeview
258     */
259    function retrieveRenderedHTML(attrs, nodeview) {
260        const ajaxEndpoint = DOKU_BASE + 'lib/exe/ajax.php';
261        jQuery.post(ajaxEndpoint, {
262            'call': 'plugin_gallery_prosemirror',
263            'attrs': JSON.stringify(attrs),
264        }).done(function (data) {
265            var newAttrs = jQuery.extend({}, attrs);
266            newAttrs.renderedHTML = data;
267            nodeview.outerView.dispatch(
268                nodeview.outerView.state.tr
269                    .setNodeMarkup(
270                        nodeview.getPos(),
271                        null,
272                        newAttrs,
273                        nodeview.node.marks
274                    )
275            );
276        });
277    }
278
279    /**
280     * Callback returning our NodeView
281     *
282     * See https://prosemirror.net/docs/ref/#view.NodeView
283     *
284     * @param {Node}       node
285     * @param {EditorView} outerview
286     * @param {function}   getPos
287     * @return {GalleryNodeView}
288     */
289    function dwplugin_gallery(node, outerview, getPos) {
290
291        // Inheritance in an IE compatible way without "class" keyword
292        function GalleryNodeView(node, outerview, getPos) {
293            this.form = new window.Prosemirror.classes.KeyValueForm(
294                LANG.plugins.gallery.title_dialog,
295                getGalleryFormFields(node.attrs)
296            );
297            AbstractNodeView.call(this, node, outerview, getPos);
298        }
299
300        GalleryNodeView.prototype = Object.create(AbstractNodeView.prototype);
301        GalleryNodeView.prototype.constructor = GalleryNodeView;
302
303        /**
304         * This renders the node into the editor
305         *
306         * This method is called from the AbstractNodeView constructor and from our update method below
307         *
308         * @param attrs
309         */
310        GalleryNodeView.prototype.renderNode = function (attrs) {
311            var thisView = this;
312            if (!this.dom) {
313                this.dom = document.createElement('div');
314                var $settingsButton = jQuery('<button>', {type: 'button', class: 'settings'}).text('settings');
315                $settingsButton.on('click', function () {
316                    thisView.form.show();
317                });
318                jQuery(this.dom)
319                    .text('GalleryPlugin')
320                    .append($settingsButton)
321                ;
322                this.form.on('submit', handleFormSubmit.bind(this));
323            }
324            if (!attrs.renderedHTML) {
325                retrieveRenderedHTML(attrs, this);
326                jQuery(this.dom).addClass('dwplugin dwplugin_gallery');
327            } else {
328                var $renderedWrapper = jQuery(attrs.renderedHTML);
329                this.dom.innerHTML = $renderedWrapper.html();
330                this.dom.classList = $renderedWrapper.attr('class') + ' dwplugin_gallery nodeHasForm';
331                jQuery(this.dom).children().css('pointer-events', 'none');
332                jQuery(this.dom).on('click', function () {
333                    thisView.form.show();
334                });
335            }
336        };
337
338        /**
339         * This method is called by the prosemirror framework, if it exists
340         *
341         * see https://prosemirror.net/docs/ref/#view.NodeView.update
342         *
343         * @param {Node} node
344         * @return {boolean}
345         */
346        GalleryNodeView.prototype.update = function (node) {
347            this.node = node;
348            this.renderNode(node.attrs);
349
350            return true;
351        };
352
353        GalleryNodeView.prototype.selectNode = function () {
354            this.dom.classList.add('ProseMirror-selectednode');
355        };
356
357        GalleryNodeView.prototype.deselectNode = function () {
358            this.dom.classList.remove('ProseMirror-selectednode');
359            this.form.hide();
360        };
361
362        return new GalleryNodeView(node, outerview, getPos);
363    }
364
365    window.Prosemirror.pluginNodeViews.dwplugin_gallery = dwplugin_gallery;
366
367    /**
368     * Create MenuItemDispatcher that produces an MenuItem if available in a schema
369     *
370     * @constructor
371     */
372    function GalleryMenuItemDispatcher() {
373        this.prototype = Object.create(window.Prosemirror.classes.AbstractMenuItemDispatcher.prototype);
374        this.prototype.constructor = this;
375
376        this.isAvailable = function (schema) {
377            return Boolean(schema.nodes.dwplugin_gallery);
378        };
379        this.getMenuItem = function (schema) {
380            return new window.Prosemirror.classes.MenuItem({
381                command: function (state, dispatch) {
382                    var isAllowed = window.Prosemirror.commands.setBlockTypeNoAttrCheck(schema.nodes.dwplugin_gallery)(state);
383                    if (!isAllowed) {
384                        return false;
385                    }
386                    if (dispatch) {
387                        var defaultAttributes = Object.entries(schema.nodes.dwplugin_gallery.attrs)
388                            .reduce(
389                                function (acc, attr) {
390                                    acc[attr[0]] = attr[1].default;
391                                    return acc;
392                                },
393                                {}
394                            )
395                        ;
396                        var form = new window.Prosemirror.classes.KeyValueForm(
397                            LANG.plugins.gallery.title_dialog,
398                            getGalleryFormFields(defaultAttributes)
399                        );
400                        form.show();
401
402                        // ToDo: offer ready-made command
403                        form.on('submit', function (event) {
404                            event.preventDefault();
405                            event.stopPropagation();
406                            var newAttrs = jQuery(this).serializeArray().reduce(function (acc, cur) {
407                                acc[cur['name']] = cur['value'];
408                                return acc;
409                            }, {});
410                            form.destroy();
411                            dispatch(state.tr.replaceSelectionWith(schema.nodes.dwplugin_gallery.createChecked(newAttrs)));
412                        });
413                    }
414                    return true;
415                },
416                label: LANG.plugins.gallery.label_toolbar_button,
417                icon: (function () {
418                    var puzzleSVG = '<svg viewBox="0 0 24 24"><path d="M22,16V4A2,2 0 0,0 20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16M11,12L13.03,14.71L16,11L20,16H8M2,6V20A2,2 0 0,0 4,22H18V20H4V6" /></svg>';
419                    return jQuery('<span>').html(puzzleSVG).get(0);
420                })(),
421            });
422        };
423    }
424
425    window.Prosemirror.pluginMenuItemDispatchers.push(new GalleryMenuItemDispatcher());
426});
427