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