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