1(function () { 2 function initializeCatmenuProsemirror() { 3 if (window.__catmenuProsemirrorInitialized) return; 4 if (!window.Prosemirror || !window.Prosemirror.classes) return; 5 window.__catmenuProsemirrorInitialized = true; 6 7 const {classes: {MenuItem, AbstractMenuItemDispatcher}} = window.Prosemirror; 8 const i18n = (window.LANG && LANG.plugins && LANG.plugins.catmenu) ? LANG.plugins.catmenu : {}; 9 function hiddenMenuItem() { 10 return new MenuItem({ 11 label: '', 12 render: () => { 13 const el = document.createElement('span'); 14 el.style.display = 'none'; 15 return el; 16 }, 17 command: () => false 18 }); 19 } 20 21 function t(key, fallback) { 22 return i18n[key] || fallback; 23 } 24 25 function shouldShowInEditorMenu() { 26 const raw = window.JSINFO && 27 JSINFO.plugins && 28 JSINFO.plugins.catmenu 29 ? JSINFO.plugins.catmenu.show_in_editor_menu 30 : true; 31 32 if (typeof raw === 'boolean') return raw; 33 const normalized = String(raw).trim().toLowerCase(); 34 return !(normalized === '0' || normalized === 'false' || normalized === 'off' || normalized === 'no'); 35 } 36 37 window.Prosemirror.pluginSchemas.push((nodes, marks) => { 38 nodes = nodes.addToEnd('catmenu', { 39 group: 'protected_block', 40 inline: false, 41 selectable: true, 42 draggable: true, 43 defining: true, 44 isolating: true, 45 code: true, 46 attrs: { 47 syntax: {default: '{{catmenu>.}}'} 48 }, 49 toDOM: (node) => ['pre', {class: 'dwplugin', 'data-pluginname': 'catmenu'}, node.attrs.syntax], 50 parseDOM: [{ 51 tag: 'pre.dwplugin[data-pluginname="catmenu"]', 52 getAttrs: (dom) => ({syntax: (dom.textContent || '{{catmenu>.}}').trim()}) 53 }] 54 }); 55 return {nodes, marks}; 56 }); 57 58 function parseCatmenuSyntax(syntax) { 59 const m = (syntax || '').match(/^\{\{catmenu>(.*?)\}\}$/i); 60 if (!m) return null; 61 return {namespace: (m[1] || '.').trim() || '.'}; 62 } 63 64 function buildCatmenuSyntax(values) { 65 return '{{catmenu>' + ((values && values.namespace) ? values.namespace : '.') + '}}'; 66 } 67 68 function formatCatmenuLabel(values) { 69 return 'CatMenu: ' + (values.namespace || '.'); 70 } 71 72 function getFolderIconUrl() { 73 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>"; 74 return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg); 75 } 76 77 function getFolderMenuIcon() { 78 const ns = 'http://www.w3.org/2000/svg'; 79 const svg = document.createElementNS(ns, 'svg'); 80 svg.setAttribute('viewBox', '0 0 24 24'); 81 82 const path1 = document.createElementNS(ns, 'path'); 83 path1.setAttribute('d', 'M10 4l2 2h8a2 2 0 0 1 2 2v2H2V6a2 2 0 0 1 2-2h6z'); 84 path1.setAttribute('fill', 'currentColor'); 85 svg.appendChild(path1); 86 87 const path2 = document.createElementNS(ns, 'path'); 88 path2.setAttribute('d', 'M2 10h20v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8z'); 89 path2.setAttribute('fill', 'currentColor'); 90 svg.appendChild(path2); 91 return svg; 92 } 93 94 function isLegacyCatmenuPluginNode(node) { 95 return !!( 96 node && 97 node.type && 98 (node.type.name === 'dwplugin_inline' || node.type.name === 'dwplugin_block') && 99 node.attrs && 100 node.attrs['data-pluginname'] === 'catmenu' 101 ); 102 } 103 104 function isCatmenuNode(node) { 105 return !!(node && node.type && node.type.name === 'catmenu') || isLegacyCatmenuPluginNode(node); 106 } 107 108 function syntaxFromNode(node) { 109 if (!node) return '{{catmenu>.}}'; 110 if (node.type && node.type.name === 'catmenu') { 111 return String((node.attrs && node.attrs.syntax) || '{{catmenu>.}}'); 112 } 113 return String(node.textContent || '{{catmenu>.}}'); 114 } 115 116 function createCatmenuNode(schema, syntax) { 117 const normalized = String(syntax || '{{catmenu>.}}').trim() || '{{catmenu>.}}'; 118 if (schema.nodes.catmenu) { 119 return schema.nodes.catmenu.createChecked({syntax: normalized}); 120 } 121 const fallback = schema.nodes.dwplugin_block; 122 if (!fallback) return null; 123 return fallback.createChecked( 124 {class: 'dwplugin', 'data-pluginname': 'catmenu'}, 125 schema.text(normalized) 126 ); 127 } 128 129 function findCatmenuAtSelection(state) { 130 const {selection} = state; 131 if (isCatmenuNode(selection.node)) { 132 return {node: selection.node, pos: selection.from}; 133 } 134 135 const $from = selection.$from; 136 if ($from.depth > 0 && isCatmenuNode($from.parent)) { 137 return {node: $from.parent, pos: $from.before($from.depth)}; 138 } 139 if (isCatmenuNode($from.nodeBefore)) { 140 return {node: $from.nodeBefore, pos: $from.pos - $from.nodeBefore.nodeSize}; 141 } 142 if (isCatmenuNode($from.nodeAfter)) { 143 return {node: $from.nodeAfter, pos: $from.pos}; 144 } 145 146 for (let depth = $from.depth; depth > 0; depth -= 1) { 147 const ancestor = $from.node(depth); 148 if (isCatmenuNode(ancestor)) { 149 return {node: ancestor, pos: $from.before(depth)}; 150 } 151 } 152 return null; 153 } 154 155 function insertParagraphAfterSelectedCatmenu(view) { 156 if (!view || !view.state) return false; 157 const selected = findCatmenuAtSelection(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 try { 169 const SelectionClass = view.state.selection.constructor; 170 const $target = view.state.doc.resolve(insertPos + 1); 171 const selection = SelectionClass.near($target, 1); 172 view.dispatch(view.state.tr.setSelection(selection).scrollIntoView()); 173 } catch (e) { 174 // Keep default selection on fallback. 175 } 176 177 view.focus(); 178 return true; 179 } 180 181 function insertCatmenuBlock(view, pluginNode) { 182 const state = view.state; 183 const {$from} = state.selection; 184 const index = $from.index(); 185 186 if ($from.parent.canReplaceWith(index, index, pluginNode.type)) { 187 view.dispatch(state.tr.replaceSelectionWith(pluginNode)); 188 return true; 189 } 190 191 for (let depth = $from.depth; depth > 0; depth -= 1) { 192 const insertPos = $from.after(depth); 193 try { 194 view.dispatch(state.tr.insert(insertPos, pluginNode)); 195 return true; 196 } catch (e) { 197 // try a higher ancestor 198 } 199 } 200 return false; 201 } 202 203 function showCatmenuDialog(initialValues, onSubmit) { 204 const values = {namespace: '.', ...initialValues}; 205 const $dialog = jQuery('<div class="plugin_catmenu_form" title="' + t('toolbar_popup_title', 'CatMenu') + '"></div>'); 206 207 $dialog.append('<label>' + t('toolbar_namespace', 'Namespace') + '</label>'); 208 const $namespace = jQuery('<input type="text" class="edit" style="width:100%;" />').val(values.namespace); 209 $dialog.append($namespace); 210 $dialog.append('<div style="font-size:.9em;color:#555;margin-top:4px;">' + t('toolbar_namespace_help', 'Folder. "." = current folder.') + '</div>'); 211 212 $dialog.dialog({ 213 modal: true, 214 width: 460, 215 close: function () { 216 jQuery(this).dialog('destroy').remove(); 217 }, 218 buttons: [ 219 { 220 text: t('toolbar_insert', 'Insert'), 221 click: function () { 222 onSubmit({namespace: String($namespace.val() || '.').trim() || '.'}); 223 jQuery(this).dialog('close'); 224 } 225 }, 226 { 227 text: t('toolbar_cancel', 'Cancel'), 228 click: function () { 229 jQuery(this).dialog('close'); 230 } 231 } 232 ] 233 }); 234 } 235 236 class CatmenuNodeView { 237 constructor(node, view, getPos) { 238 this.node = node; 239 this.view = view; 240 this.getPos = getPos; 241 this.dom = document.createElement('div'); 242 const typeClass = (node.type && node.type.name === 'dwplugin_inline') ? 'pm_catmenu_inline' : 'pm_catmenu_block'; 243 this.dom.className = 'plugin_catmenu pm_catmenu_node nodeHasForm ' + typeClass; 244 this.dom.setAttribute('contenteditable', 'false'); 245 this.render(); 246 247 this.dom.addEventListener('click', (event) => { 248 event.preventDefault(); 249 event.stopPropagation(); 250 this.openEditor(); 251 }); 252 } 253 254 render() { 255 const syntax = syntaxFromNode(this.node); 256 const parsed = parseCatmenuSyntax(syntax); 257 const label = parsed ? formatCatmenuLabel(parsed) : syntax; 258 this.dom.textContent = ''; 259 260 const icon = document.createElement('img'); 261 icon.className = 'pm_catmenu_icon'; 262 icon.src = getFolderIconUrl(); 263 icon.alt = ''; 264 icon.setAttribute('aria-hidden', 'true'); 265 this.dom.appendChild(icon); 266 267 const text = document.createElement('span'); 268 text.textContent = label; 269 this.dom.appendChild(text); 270 this.dom.setAttribute('title', syntax); 271 } 272 273 openEditor() { 274 const parsed = parseCatmenuSyntax(syntaxFromNode(this.node)) || {namespace: '.'}; 275 showCatmenuDialog(parsed, (values) => { 276 const syntax = buildCatmenuSyntax(values); 277 const replacement = createCatmenuNode(this.view.state.schema, syntax); 278 if (!replacement) return; 279 280 const pos = this.getPos(); 281 this.view.dispatch(this.view.state.tr.replaceWith(pos, pos + this.node.nodeSize, replacement)); 282 this.view.focus(); 283 }); 284 } 285 286 update(node) { 287 if (!isCatmenuNode(node)) return false; 288 this.node = node; 289 const typeClass = (node.type && node.type.name === 'dwplugin_inline') ? 'pm_catmenu_inline' : 'pm_catmenu_block'; 290 this.dom.className = 'plugin_catmenu pm_catmenu_node nodeHasForm ' + typeClass; 291 this.render(); 292 return true; 293 } 294 295 selectNode() { this.dom.classList.add('ProseMirror-selectednode'); } 296 deselectNode() { this.dom.classList.remove('ProseMirror-selectednode'); } 297 stopEvent() { return true; } 298 ignoreMutation() { return true; } 299 } 300 301 class CatmenuMenuItemDispatcher extends AbstractMenuItemDispatcher { 302 static isAvailable(schema) { 303 return !!(schema.nodes.catmenu || schema.nodes.dwplugin_block); 304 } 305 306 static getIcon() { 307 const wrapper = document.createElement('span'); 308 wrapper.className = 'menuicon'; 309 wrapper.appendChild(getFolderMenuIcon()); 310 return wrapper; 311 } 312 313 static getMenuItem(schema) { 314 if (!this.isAvailable(schema)) return hiddenMenuItem(); 315 316 return new MenuItem({ 317 label: t('toolbar_button', 'CatMenu'), 318 icon: this.getIcon(), 319 command: (state, dispatch, view) => { 320 const existing = findCatmenuAtSelection(state); 321 if (!dispatch || !view) return true; 322 323 const initialValues = existing 324 ? (parseCatmenuSyntax(syntaxFromNode(existing.node)) || {namespace: '.'}) 325 : {namespace: '.'}; 326 327 showCatmenuDialog(initialValues, (values) => { 328 const syntax = buildCatmenuSyntax(values); 329 const pluginNode = createCatmenuNode(schema, syntax); 330 if (!pluginNode) return; 331 332 if (existing) { 333 view.dispatch(view.state.tr.replaceWith(existing.pos, existing.pos + existing.node.nodeSize, pluginNode)); 334 } else if (!insertCatmenuBlock(view, pluginNode)) { 335 const endPos = view.state.doc.content.size; 336 view.dispatch(view.state.tr.insert(endPos, pluginNode)); 337 } 338 view.focus(); 339 }); 340 341 return true; 342 } 343 }); 344 } 345 } 346 347 window.Prosemirror.pluginNodeViews.catmenu = (node, view, getPos) => new CatmenuNodeView(node, view, getPos); 348 349 const originalInline = window.Prosemirror.pluginNodeViews.dwplugin_inline; 350 window.Prosemirror.pluginNodeViews.dwplugin_inline = (node, view, getPos) => { 351 if (isLegacyCatmenuPluginNode(node)) return new CatmenuNodeView(node, view, getPos); 352 return typeof originalInline === 'function' ? originalInline(node, view, getPos) : undefined; 353 }; 354 355 const originalBlock = window.Prosemirror.pluginNodeViews.dwplugin_block; 356 window.Prosemirror.pluginNodeViews.dwplugin_block = (node, view, getPos) => { 357 if (isLegacyCatmenuPluginNode(node)) return new CatmenuNodeView(node, view, getPos); 358 return typeof originalBlock === 'function' ? originalBlock(node, view, getPos) : undefined; 359 }; 360 361 if (shouldShowInEditorMenu()) { 362 window.Prosemirror.pluginMenuItemDispatchers.push(CatmenuMenuItemDispatcher); 363 } 364 365 if (!window.__catmenuKeyboardGuardInstalled) { 366 window.__catmenuKeyboardGuardInstalled = true; 367 document.addEventListener('keydown', (event) => { 368 if (event.key !== 'Enter') return; 369 const view = window.Prosemirror && window.Prosemirror.view; 370 if (!view || !view.state) return; 371 if (!findCatmenuAtSelection(view.state)) return; 372 event.preventDefault(); 373 event.stopPropagation(); 374 insertParagraphAfterSelectedCatmenu(view); 375 }, true); 376 } 377 } 378 379 jQuery(document).on('PROSEMIRROR_API_INITIALIZED', initializeCatmenuProsemirror); 380 initializeCatmenuProsemirror(); 381})(); 382