1(function () { 2 function initExtranetProsemirror() { 3 if (window.__extranetProsemirrorInitialized) return; 4 if (!window.Prosemirror || !window.Prosemirror.classes) return; 5 window.__extranetProsemirrorInitialized = true; 6 7 const {classes: {MenuItem, AbstractMenuItemDispatcher}} = window.Prosemirror; 8 const polyfill = window.DokuWikiProsemirrorFileStatePolyfill || null; 9 let localMacroState = {noextranet: null, extranet: null}; 10 11 const defaultMode = window.JSINFO && JSINFO.plugin_extranet_default_mode 12 ? String(JSINFO.plugin_extranet_default_mode).toLowerCase() 13 : 'allow'; 14 15 function isFixedMode() { 16 return defaultMode === 'force_allow' || defaultMode === 'force_block'; 17 } 18 19 function hiddenMenuItem() { 20 return new MenuItem({ 21 label: '', 22 render: () => { 23 const el = document.createElement('span'); 24 el.style.display = 'none'; 25 return el; 26 }, 27 command: () => false 28 }); 29 } 30 31 function getNoExtranetLabel() { 32 return ( 33 (window.LANG && 34 LANG.plugins && 35 LANG.plugins.prosemirror && 36 LANG.plugins.prosemirror['label:noextranet']) || 37 (window.JSINFO && JSINFO.plugin_extranet_label_noextranet) || 38 'Blocked from Extranet' 39 ); 40 } 41 42 function getExtranetLabel() { 43 return ( 44 (window.JSINFO && JSINFO.plugin_extranet_label_extranet) || 45 'Allowed from Extranet' 46 ); 47 } 48 49 function getManagedMacroKey() { 50 if (isFixedMode()) return null; 51 return defaultMode === 'block' ? 'extranet' : 'noextranet'; 52 } 53 54 function getManagedMacroLabel() { 55 return getManagedMacroKey() === 'extranet' ? getExtranetLabel() : getNoExtranetLabel(); 56 } 57 58 function getSettingsLabel() { 59 return ( 60 (window.LANG && 61 LANG.plugins && 62 LANG.plugins.prosemirror && 63 LANG.plugins.prosemirror['label:settings']) || 64 'Page Settings' 65 ); 66 } 67 68 function getSettingsChildLabels() { 69 const labels = []; 70 const fromLang = window.LANG && 71 LANG.plugins && 72 LANG.plugins.prosemirror 73 ? LANG.plugins.prosemirror 74 : {}; 75 76 labels.push(fromLang['label:nocache'] || 'Deactivate Cache'); 77 labels.push(fromLang['label:notoc'] || 'Hide Table of Contents'); 78 labels.push('NOCACHE'); 79 labels.push('NOTOC'); 80 81 return labels 82 .filter(Boolean) 83 .map((s) => String(s).trim().toLowerCase()); 84 } 85 86 function findSettingsDropdown(menubar) { 87 const settingsLabel = getSettingsLabel(); 88 const knownChildLabels = getSettingsChildLabels(); 89 const dropdowns = menubar.querySelectorAll('.menuitem.dropdown'); 90 let found = null; 91 92 dropdowns.forEach((dropdown) => { 93 if (found) return; 94 const labelEl = dropdown.querySelector(':scope > .menulabel'); 95 const dropdownLabelText = String((labelEl && labelEl.textContent) || '').trim(); 96 const dropdownLabelTitle = String((labelEl && labelEl.getAttribute('title')) || '').trim(); 97 98 const looksLikeSettings = 99 dropdownLabelText === settingsLabel || 100 dropdownLabelTitle === settingsLabel; 101 102 const dropdownContent = dropdown.querySelector('.dropdown_content'); 103 const childTexts = dropdownContent 104 ? Array.from(dropdownContent.querySelectorAll('.menuitem .menulabel')) 105 .map((el) => ( 106 String(el.textContent || el.getAttribute('title') || '') 107 .trim() 108 .toLowerCase() 109 )) 110 : []; 111 112 const hasKnownSettingsChildren = childTexts.some((txt) => knownChildLabels.includes(txt)); 113 114 if (looksLikeSettings || hasKnownSettingsChildren) { 115 found = dropdown; 116 } 117 }); 118 119 return found; 120 } 121 122 123 window.Prosemirror.pluginSchemas.push((nodes, marks) => { 124 const doc = nodes.get('doc'); 125 if (!doc) return {nodes, marks}; 126 127 const attrs = {...(doc.attrs || {})}; 128 let changed = false; 129 130 if (typeof attrs.noextranet === 'undefined') { 131 attrs.noextranet = {default: false}; 132 changed = true; 133 } 134 if (typeof attrs.extranet === 'undefined') { 135 attrs.extranet = {default: false}; 136 changed = true; 137 } 138 139 if (!changed) return {nodes, marks}; 140 141 const updatedDoc = { 142 ...doc, 143 attrs 144 }; 145 nodes = nodes.update('doc', updatedDoc); 146 return {nodes, marks}; 147 }); 148 149 function readMacroStateFromDoc(doc) { 150 if (!doc) return null; 151 152 if (doc.attrs && (typeof doc.attrs.noextranet !== 'undefined' || typeof doc.attrs.extranet !== 'undefined')) { 153 return { 154 noextranet: !!doc.attrs.noextranet, 155 extranet: !!doc.attrs.extranet 156 }; 157 } 158 159 try { 160 const raw = doc.textBetween(0, doc.content.size, '\n', '\n'); 161 return { 162 noextranet: /~~\s*NOEXTRANET\s*~~/i.test(raw), 163 extranet: /~~\s*EXTRANET\s*~~/i.test(raw) 164 }; 165 } catch (e) { 166 return null; 167 } 168 } 169 170 function readMacroStateFromJsonField() { 171 const input = document.querySelector('#dw__editform [name="prosemirror_json"]'); 172 if (!input || !input.value) return null; 173 174 try { 175 const json = JSON.parse(input.value); 176 if (json && json.attrs) { 177 if (typeof json.attrs.noextranet !== 'undefined' || typeof json.attrs.extranet !== 'undefined') { 178 return { 179 noextranet: !!json.attrs.noextranet, 180 extranet: !!json.attrs.extranet 181 }; 182 } 183 } 184 } catch (e) { 185 // fallback below 186 } 187 188 const raw = String(input.value || ''); 189 return { 190 noextranet: /~~\s*NOEXTRANET\s*~~/i.test(raw), 191 extranet: /~~\s*EXTRANET\s*~~/i.test(raw) 192 }; 193 } 194 195 function readMacroStateFromWikiTextarea() { 196 const textarea = document.querySelector('#dw__editform textarea[name="wikitext"], #dw__editform #wiki__text'); 197 if (!textarea) return null; 198 const raw = String(textarea.value || ''); 199 return { 200 noextranet: /~~\s*NOEXTRANET\s*~~/i.test(raw), 201 extranet: /~~\s*EXTRANET\s*~~/i.test(raw) 202 }; 203 } 204 205 function persistMacroStateInJsonField(state, macroState) { 206 const input = document.querySelector('#dw__editform [name="prosemirror_json"]'); 207 if (!input || !state || !state.doc) return false; 208 209 try { 210 const json = polyfill && typeof polyfill.mergeAttrsFromHiddenJson === 'function' 211 ? polyfill.mergeAttrsFromHiddenJson(state, input, { 212 noextranet: !!macroState.noextranet, 213 extranet: !!macroState.extranet 214 }) 215 : state.doc.toJSON(); 216 217 if (!json) return false; 218 if (!(polyfill && typeof polyfill.mergeAttrsFromHiddenJson === 'function')) { 219 json.attrs = { 220 ...(json.attrs || {}), 221 noextranet: !!macroState.noextranet, 222 extranet: !!macroState.extranet 223 }; 224 input.value = JSON.stringify(json); 225 } 226 return true; 227 } catch (e) { 228 return false; 229 } 230 } 231 232 function getCurrentEditorView() { 233 return window.Prosemirror && window.Prosemirror.view ? window.Prosemirror.view : null; 234 } 235 236 function syncCurrentMacroStateToJsonField() { 237 const view = getCurrentEditorView(); 238 if (!view || !view.state) return false; 239 const macroState = getMacroState(view.state); 240 return persistMacroStateInJsonField(view.state, macroState); 241 } 242 243 function getMacroState(state) { 244 // Prefer local state right after interactions to avoid one-tick lag from editor state. 245 if (localMacroState.noextranet !== null || localMacroState.extranet !== null) { 246 return { 247 noextranet: !!localMacroState.noextranet, 248 extranet: !!localMacroState.extranet 249 }; 250 } 251 252 const fromDoc = state && state.doc ? readMacroStateFromDoc(state.doc) : null; 253 if (fromDoc) { 254 return fromDoc; 255 } 256 257 const fromJson = readMacroStateFromJsonField(); 258 if (fromJson) return fromJson; 259 260 return {noextranet: false, extranet: false}; 261 } 262 263 function applyMacroStateToText(text, macroState) { 264 const cleaned = String(text || '').replace(/^[\t ]*~~[\t ]*(NOEXTRANET|EXTRANET)[\t ]*~~[\t ]*(\r?\n)?/gimu, '').replace(/\s+$/, ''); 265 266 if (macroState && macroState.noextranet) { 267 return cleaned ? `${cleaned}\n\n~~NOEXTRANET~~\n` : '~~NOEXTRANET~~\n'; 268 } 269 270 if (macroState && macroState.extranet) { 271 return cleaned ? `${cleaned}\n\n~~EXTRANET~~\n` : '~~EXTRANET~~\n'; 272 } 273 274 return cleaned ? `${cleaned}\n` : ''; 275 } 276 277 function ensureShowDefaultEditorWrapped() { 278 if (window.__extranetWrappedShowDefaultEditor) return true; 279 if (typeof window.showDefaultEditor !== 'function') return false; 280 281 const originalShowDefaultEditor = window.showDefaultEditor; 282 window.showDefaultEditor = function extranetShowDefaultEditor(text) { 283 const view = getCurrentEditorView(); 284 const nextText = applyMacroStateToText(text, getMacroState(view && view.state)); 285 return originalShowDefaultEditor.call(this, nextText); 286 }; 287 window.__extranetWrappedShowDefaultEditor = true; 288 return true; 289 } 290 291 function renderCheckboxIcon(checked) { 292 const ns = 'http://www.w3.org/2000/svg'; 293 const svg = document.createElementNS(ns, 'svg'); 294 svg.setAttribute('viewBox', '0 0 24 24'); 295 296 const path = document.createElementNS(ns, 'path'); 297 if (checked) { 298 path.setAttribute('d', 'M19,19H5V5H15V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V11H19M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z'); 299 } else { 300 path.setAttribute('d', 'M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z'); 301 } 302 svg.appendChild(path); 303 304 const span = document.createElement('span'); 305 span.className = 'menuicon'; 306 span.appendChild(svg); 307 return span; 308 } 309 310 function syncProxyCheckboxIcon(proxy, checked) { 311 const next = checked ? '1' : '0'; 312 if (proxy.dataset.extranetChecked === next && proxy.querySelector(':scope > .menuicon')) { 313 return; 314 } 315 const oldIcon = proxy.querySelector(':scope > .menuicon'); 316 const newIcon = renderCheckboxIcon(checked); 317 if (oldIcon) { 318 oldIcon.replaceWith(newIcon); 319 } else { 320 proxy.insertBefore(newIcon, proxy.firstChild || null); 321 } 322 proxy.dataset.extranetChecked = next; 323 } 324 325 function refreshAllExtranetMenuItemsFromMacroState(macroState) { 326 const items = document.querySelectorAll('.prosemirror_wrapper .menuitem.extranet-page-setting-item'); 327 const modifiedElements = []; 328 const itemStates = []; 329 items.forEach((item) => { 330 const isNoextranet = item.classList.contains('extranet-noextranet-item'); 331 const key = isNoextranet ? 'noextranet' : 'extranet'; 332 const active = !!macroState[key]; 333 const wasActive = item.dataset.extranetChecked === '1'; 334 item.classList.remove('is-active'); 335 item.setAttribute('aria-checked', active ? 'true' : 'false'); 336 const expectedDataset = active ? '1' : '0'; 337 const hasIcon = !!item.querySelector(':scope > .menuicon'); 338 if (wasActive !== active || !hasIcon || item.dataset.extranetChecked !== expectedDataset) { 339 syncProxyCheckboxIcon(item, active); 340 } 341 itemStates.push({ 342 key, 343 before: wasActive, 344 after: active, 345 changed: wasActive !== active, 346 element: item 347 }); 348 if (wasActive !== active) { 349 modifiedElements.push(item); 350 } 351 }); 352 // Debug logging disabled: this runs frequently during editor updates. 353 } 354 355 function refreshAllExtranetMenuItems(state) { 356 refreshAllExtranetMenuItemsFromMacroState(getMacroState(state)); 357 } 358 359 function setDocMacroState(view, macroState) { 360 const desired = { 361 noextranet: !!macroState.noextranet, 362 extranet: !!macroState.extranet 363 }; 364 localMacroState = desired; 365 366 const hasDesiredAttrs = () => { 367 const attrs = view.state && view.state.doc && view.state.doc.attrs ? view.state.doc.attrs : {}; 368 return !!attrs.noextranet === desired.noextranet && !!attrs.extranet === desired.extranet; 369 }; 370 371 const {state} = view; 372 let tr = state.tr; 373 374 if (typeof tr.setDocAttr === 'function') { 375 tr = tr.setDocAttr('noextranet', desired.noextranet); 376 tr = tr.setDocAttr('extranet', desired.extranet); 377 view.dispatch(tr); 378 if (hasDesiredAttrs()) { 379 persistMacroStateInJsonField(view.state, desired); 380 return true; 381 } 382 } 383 384 const classes = (window.Prosemirror && window.Prosemirror.classes) || {}; 385 const stepCtor = Object.values(classes).find( 386 (Ctor) => typeof Ctor === 'function' && /DocAttr/i.test(Ctor.name || '') 387 ); 388 if (stepCtor) { 389 try { 390 tr = state.tr; 391 tr = tr.step(new stepCtor('noextranet', desired.noextranet, 'setDocAttr')); 392 tr = tr.step(new stepCtor('extranet', desired.extranet, 'setDocAttr')); 393 view.dispatch(tr); 394 if (hasDesiredAttrs()) { 395 persistMacroStateInJsonField(view.state, desired); 396 return true; 397 } 398 } catch (e) { 399 // fallback below 400 } 401 } 402 403 return persistMacroStateInJsonField(state, desired); 404 } 405 406 function getSourceMacroState() { 407 const fromJson = readMacroStateFromJsonField(); 408 if (fromJson) return fromJson; 409 const fromTextarea = readMacroStateFromWikiTextarea(); 410 if (fromTextarea) return fromTextarea; 411 return null; 412 } 413 414 function bootstrapStateFromSource() { 415 const view = getCurrentEditorView(); 416 if (!view || !view.state || !view.state.doc) return; 417 418 const sourceState = getSourceMacroState(); 419 const docState = readMacroStateFromDoc(view.state.doc); 420 if (!sourceState) return; 421 422 const current = docState || {noextranet: false, extranet: false}; 423 if (!!current.noextranet === !!sourceState.noextranet && !!current.extranet === !!sourceState.extranet) return; 424 425 setDocMacroState(view, sourceState); 426 } 427 428 function getMacroLabel(key) { 429 return key === 'extranet' ? getExtranetLabel() : getNoExtranetLabel(); 430 } 431 432 function isMacroActive(state, key) { 433 const macroState = getMacroState(state); 434 return !!macroState[key]; 435 } 436 437 function buildStateForToggle(state, key) { 438 const active = isMacroActive(state, key); 439 if (key === 'noextranet') { 440 return active 441 ? {noextranet: false, extranet: false} 442 : {noextranet: true, extranet: false}; 443 } 444 445 return active 446 ? {noextranet: false, extranet: false} 447 : {noextranet: false, extranet: true}; 448 } 449 450 function createMacroDispatcher(key) { 451 return class extends AbstractMenuItemDispatcher { 452 static isAvailable(schema) { 453 return !!(schema && schema.nodes && schema.nodes.doc); 454 } 455 456 static getMenuItem(schema) { 457 if (!this.isAvailable(schema)) return hiddenMenuItem(); 458 459 return new MenuItem({ 460 label: getMacroLabel(key), 461 render: (view) => { 462 const label = getMacroLabel(key); 463 const item = document.createElement('span'); 464 item.className = `menuitem extranet-page-setting-item extranet-${key}-item`; 465 466 item.appendChild(renderCheckboxIcon(isMacroActive(view.state, key))); 467 468 const title = document.createElement('span'); 469 title.className = 'menulabel'; 470 title.setAttribute('title', label); 471 title.textContent = label; 472 item.appendChild(title); 473 474 return item; 475 }, 476 update: (view, dom) => { 477 const checked = isMacroActive(view.state, key); 478 if (dom) { 479 syncProxyCheckboxIcon(dom, checked); 480 const labelEl = dom.querySelector('.menulabel'); 481 if (labelEl) { 482 const label = getMacroLabel(key); 483 labelEl.textContent = label; 484 labelEl.setAttribute('title', label); 485 } 486 } 487 }, 488 command: (state, dispatch, view) => { 489 if (!dispatch || !view) return true; 490 const currentState = getMacroState(state); 491 const nextState = buildStateForToggle(state, key); 492 const applied = setDocMacroState(view, nextState); 493 if (applied) { 494 // Force immediate UI sync from the intended target state. 495 refreshAllExtranetMenuItemsFromMacroState(nextState); 496 scheduleMoveBurst(); 497 } 498 return applied; 499 }, 500 // Keep parent "Modules" menu from turning blue for extranet page settings. 501 isActive: () => false 502 }); 503 } 504 }; 505 } 506 507 const managedMacroKey = getManagedMacroKey(); 508 if (managedMacroKey) { 509 window.Prosemirror.pluginMenuItemDispatchers.push(createMacroDispatcher(managedMacroKey)); 510 } 511 512 function moveExtranetItemsToSettingsMenu() { 513 const menubars = document.querySelectorAll('.prosemirror_wrapper .menubar'); 514 515 menubars.forEach((menubar) => { 516 const sourceItems = menubar.querySelectorAll('.menuitem.extranet-page-setting-item'); 517 if (!sourceItems.length) return; 518 519 const settingsDropdown = findSettingsDropdown(menubar); 520 521 if (!settingsDropdown) return; 522 523 const target = settingsDropdown.querySelector('.dropdown_content'); 524 if (!target) return; 525 526 sourceItems.forEach((sourceItem) => { 527 if (sourceItem.parentElement !== target) { 528 target.appendChild(sourceItem); 529 } 530 }); 531 532 const view = window.Prosemirror && window.Prosemirror.view; 533 const currentState = view && view.state ? view.state : null; 534 535 sourceItems.forEach((sourceItem) => { 536 const isNoextranet = sourceItem.classList.contains('extranet-noextranet-item'); 537 const key = isNoextranet ? 'noextranet' : 'extranet'; 538 const active = isMacroActive(currentState, key); 539 sourceItem.classList.remove('is-active'); 540 syncProxyCheckboxIcon(sourceItem, !!active); 541 }); 542 }); 543 544 const view = window.Prosemirror && window.Prosemirror.view; 545 if (view && view.state) { 546 refreshAllExtranetMenuItems(view.state); 547 } 548 } 549 550 const scheduleMoveBurst = polyfill && typeof polyfill.makeBurstScheduler === 'function' 551 ? polyfill.makeBurstScheduler(moveExtranetItemsToSettingsMenu) 552 : (function () { 553 let scheduled = false; 554 function scheduleMove() { 555 if (scheduled) return; 556 scheduled = true; 557 window.requestAnimationFrame(() => { 558 scheduled = false; 559 moveExtranetItemsToSettingsMenu(); 560 }); 561 } 562 563 return function () { 564 scheduleMove(); 565 window.setTimeout(scheduleMove, 0); 566 window.setTimeout(scheduleMove, 100); 567 window.setTimeout(scheduleMove, 300); 568 }; 569 })(); 570 571 if (polyfill && typeof polyfill.bindCommonEditorLifecycle === 'function') { 572 polyfill.bindCommonEditorLifecycle({ 573 onSwitchToText: function () { 574 ensureShowDefaultEditorWrapped(); 575 syncCurrentMacroStateToJsonField(); 576 }, 577 onSubmit: syncCurrentMacroStateToJsonField, 578 onEditorToggle: scheduleMoveBurst, 579 onAfterSwitch: scheduleMoveBurst 580 }); 581 } else { 582 document.addEventListener('mousedown', (event) => { 583 const target = event.target instanceof Element ? event.target : null; 584 if (!target) return; 585 if (!target.closest('.plugin_prosemirror_useWYSIWYG')) return; 586 ensureShowDefaultEditorWrapped(); 587 syncCurrentMacroStateToJsonField(); 588 }, true); 589 590 document.addEventListener('click', (event) => { 591 const target = event.target instanceof Element ? event.target : null; 592 if (!target) return; 593 if (target.closest('.plugin_prosemirror_useWYSIWYG')) { 594 scheduleMoveBurst(); 595 } 596 597 const submitControl = target.closest('#dw__editform button[type="submit"], #dw__editform input[type="submit"]'); 598 if (!submitControl) return; 599 syncCurrentMacroStateToJsonField(); 600 }, true); 601 602 document.addEventListener('submit', (event) => { 603 const form = event.target instanceof HTMLFormElement ? event.target : null; 604 if (!form || form.id !== 'dw__editform') return; 605 syncCurrentMacroStateToJsonField(); 606 }, true); 607 608 jQuery(window).on('fastwiki:afterSwitch', function () { 609 scheduleMoveBurst(); 610 }); 611 } 612 613 ensureShowDefaultEditorWrapped(); 614 window.setTimeout(ensureShowDefaultEditorWrapped, 0); 615 if (!isFixedMode()) { 616 bootstrapStateFromSource(); 617 window.setTimeout(bootstrapStateFromSource, 0); 618 scheduleMoveBurst(); 619 } 620 } 621 622 jQuery(document).on('PROSEMIRROR_API_INITIALIZED', initExtranetProsemirror); 623 initExtranetProsemirror(); 624})(); 625