/** * Mode toggle bar - Edit/Preview mode switcher UI panel. * * Reads (via globals): * SFE.Context - .isInlineUIEnabled (r/w), .activeEditor, * .activeMode, .actionBar * SFE.OverlayManager * SFE.closeAnyActiveMode - set by frontend-inline-edit.js * SFE.hoverTracker - set by frontend-inline-edit.js * SFE.ManagerData - .permissions * SFE.FloatingUiMoveManager - shared movement utility for floating UI * SFE.ActionBarDock - shared batch-session dock positioning API * * Exposes: SFE.ModeToggleBar { init, update, setInlineUIEnabled } */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const DRAG_OWNER_NAME = 'mode-toggle-bar'; const DEFAULT_TOP = 42; const DEFAULT_RIGHT = 10; const POSITION_STORAGE_KEY = 'mwpSfeModeToggleBarPosition'; let dragController = null; let barPosition = null; /** * Resolve the live mode toggle bar element. * * @returns {HTMLElement|null} Toggle bar element when mounted. */ function getBar() { return document.querySelector('.mwp-sfe-mode-toggle-bar'); } /** * Derive the current viewport size in CSS pixels. * * @returns {{ width:number, height:number }} Viewport size. */ function getViewportSize() { return { width: document.documentElement.clientWidth || window.innerWidth || 0, height: document.documentElement.clientHeight || window.innerHeight || 0, }; } /** * Read the tab-scoped saved position for the mode toggle bar. * * sessionStorage survives same-tab refreshes and same-origin page navigations, * but it is cleared automatically when the tab/window closes, which matches * the intended persistence lifetime for this control. * * @returns {{ left:number, top:number }|null} Saved position when valid. */ function readStoredBarPosition() { const rawValue = window.sessionStorage.getItem(POSITION_STORAGE_KEY); if (!rawValue) return null; const parsed = JSON.parse(rawValue); if (!parsed || !Number.isFinite(parsed.left) || !Number.isFinite(parsed.top)) { window.sessionStorage.removeItem(POSITION_STORAGE_KEY); return null; } return { left: parsed.left, top: parsed.top, }; } /** * Persist the current mode toggle bar position for the active browser tab. * * @param {{ left:number, top:number }} position Bar position to persist. * @returns {void} */ function writeStoredBarPosition(position) { window.sessionStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify({ left: position.left, top: position.top, })); } /** * Compute the in-memory left/top position for the floating bar. * * On first init the bar still uses CSS top/right defaults, so the DOM rect is * the source of truth until we convert the bar to explicit left/top inline * positioning for the current page session. * * @param {HTMLElement} bar Toggle bar element. * @returns {{ left:number, top:number }} Current bar position. */ function getCurrentBarPosition(bar) { if (barPosition && Number.isFinite(barPosition.left) && Number.isFinite(barPosition.top)) { return { left: barPosition.left, top: barPosition.top, }; } const rect = bar.getBoundingClientRect(); return { left: rect.left, top: rect.top, }; } /** * Compute the original default position from the bar's top/right offsets. * * This preserves the intended default of "42px from the top and 10px from * the right edge" instead of seeding the first session position from an * already-shifted DOM rect. * * @param {HTMLElement} bar Toggle bar element. * @returns {{ left:number, top:number }} Default left/top position. */ function getDefaultBarPosition(bar) { const viewport = getViewportSize(); const rect = bar.getBoundingClientRect(); return { left: Math.max(0, viewport.width - rect.width - DEFAULT_RIGHT), top: DEFAULT_TOP, }; } /** * Resolve the target element's viewport box from either explicit session * position or the live DOM rect. * * This keeps detached grip positioning stable during initial bar animations by * preferring the known logical position when it exists. * * @param {HTMLElement} bar Toggle bar element. * @returns {{ left:number, top:number, width:number, height:number }} Viewport box. */ function getBarViewportBox(bar) { const rect = bar.getBoundingClientRect(); if (barPosition && Number.isFinite(barPosition.left) && Number.isFinite(barPosition.top)) { return { left: barPosition.left, top: barPosition.top, width: rect.width, height: rect.height, }; } return { left: rect.left, top: rect.top, width: rect.width, height: rect.height, }; } /** * Apply the current session's explicit left/top position to the toggle bar. * * @param {HTMLElement} bar Toggle bar element. * @param {{ left:number, top:number }} position New left/top position. * @param {Object} [meta] Rendering metadata. * @returns {void} */ function applyBarPosition(bar, position, meta = {}) { const nextPosition = { left: Math.round(position.left), top: Math.round(position.top), }; barPosition = nextPosition; bar.style.left = nextPosition.left + 'px'; bar.style.top = nextPosition.top + 'px'; bar.style.right = 'auto'; bar.style.bottom = 'auto'; if (meta.persist !== false) { writeStoredBarPosition(nextPosition); } if (dragController) { dragController.syncGripPosition(); dragController.syncGripVisibility(); } if (meta.refreshDock === false) return; SFE.ActionBarDock?.refreshPosition?.(); } /** * Resolve whether the detached grip should currently be visible for the mode * toggle bar. The FloatingUiMoveManager owns the grip element; the toggle bar owns the * bar-specific hover policy, including suppressing the grip while its buttons * are hovered and keeping the grip visible during active drags. * * @param {HTMLElement} bar Toggle bar element. * @param {HTMLElement|null} grip Shared detached grip element. * @returns {boolean} True when the grip should be visible. */ function shouldShowGrip(bar, grip) { const buttons = bar.querySelector('.mwp-sfe-mode-toggle-buttons'); return bar.classList.contains('mwp-sfe-is-dragging') || grip.matches(':hover') || (bar.matches(':hover') && !(buttons && buttons.matches(':hover'))); } /** * Restore or seed the toggle bar's explicit left/top position for this tab, * then clamp it to the current viewport. * * @param {HTMLElement} bar Toggle bar element. * @returns {void} */ function initializeFloatingPosition(bar) { if (!barPosition) { barPosition = readStoredBarPosition() || getDefaultBarPosition(bar); applyBarPosition(bar, barPosition, { source: 'init', }); } if (dragController) { dragController.syncToBounds({ source: 'init' }); } } /** * Create the reusable floating-UI movement controller for the toggle bar. * * The bar position is tab-scoped by design: it lives in runtime state plus * sessionStorage, so refreshes and same-tab page navigations reuse the last * placement while closing the tab/window clears it automatically. * * @param {HTMLElement} bar Toggle bar element. * @returns {void} */ function ensureDragBehavior(bar) { if (dragController || !SFE.FloatingUiMoveManager?.createDetachedGripMover) return; dragController = SFE.FloatingUiMoveManager.createDetachedGripMover({ element: bar, owner: DRAG_OWNER_NAME, getPosition() { return getCurrentBarPosition(bar); }, getGripAnchorBox() { return getBarViewportBox(bar); }, getBounds(args) { return SFE.FloatingUiMoveManager.buildDetachedGripBounds(args); }, visibilityTargets() { return [ bar, bar.querySelector('.mwp-sfe-mode-toggle-buttons'), ].filter(Boolean); }, shouldShowGrip({ grip }) { return shouldShowGrip(bar, grip); }, gripOptions: { ariaLabel: 'Move mode toggle bar', }, applyPosition(position, meta) { applyBarPosition(bar, position, meta); }, onDragEnd() { requestAnimationFrame(function() { if (dragController) { dragController.syncGripVisibility(); } }); }, }); } /** * Ensure all one-time interactive behavior is attached to the toggle bar. * * @param {HTMLElement} bar Toggle bar element. * @returns {void} */ function ensureInteractiveBehavior(bar) { ensureDragBehavior(bar); initializeFloatingPosition(bar); dragController.activateGrip(); dragController.syncGripPosition(); dragController.syncGripVisibility(); } /** * Resolve the current inline entry button label from permissions. * * @returns {string} Entry label text. */ function getInlineEntryLabel() { const perms = SFE.ManagerData.permissions || {}; return (perms.can_publish || perms.can_draft) ? 'Edit' : 'Comment'; } /** * Normalize a DOM node for selection containment checks. * * @param {Node|null} node Candidate node. * @returns {Node|null} Containment-safe node. */ function getNodeForContainment(node) { if (!node) return null; return node.nodeType === Node.TEXT_NODE ? node.parentNode : node; } /** * Resolve the editor focus target for preview selection restoration. * * @param {Object|null} editorState Active editor state. * @returns {HTMLElement|null} Focus target element. */ function getEditorFocusTarget(editorState) { if (!editorState) return null; const activeComponentEl = editorState.activeEditableComponent?.element || null; if (activeComponentEl) return activeComponentEl; return editorState.element || null; } /** * Resolve the editor element used to anchor floating chrome. * * @param {Object|null} editorState Active editor state. * @returns {HTMLElement|null} Chrome anchor element. */ function getEditorChromeAnchor(editorState) { if (!editorState) return null; return editorState.element || null; } /** * Resolve the active overlay mode for an editor session. * * Draft editing clears ctx.activeMode after handing off from draft preview, * so the live editor session must consult the shared draftEditState contract * to preserve the orange draft-editing outline when returning from preview. * * @param {Object|null} editorState Active editor state from SFE.Context. * @param {Object|null} ctx Shared FrontEdit context object. * @returns {string} Overlay mode for the active editor. */ function getEditorOverlayMode(editorState, ctx) { if (!editorState || !ctx) return 'editing'; const draftEditState = ctx.draftEditState; const isDraftEditor = !!( draftEditState && draftEditState.draftElement && draftEditState.draftElement === editorState.element ); return isDraftEditor ? 'draft-editing' : 'editing'; } /** * Check whether the current selection range lives inside the target element. * * @param {Selection|null} selection Browser selection object. * @param {HTMLElement|null} element Candidate container element. * @returns {boolean} True when the selection is inside the element. */ function isSelectionWithinElement(selection, element) { if (!selection || !element || selection.rangeCount < 1) return false; const range = selection.getRangeAt(0); const anchorNode = getNodeForContainment(range.commonAncestorContainer); return !!(anchorNode && (anchorNode === element || element.contains(anchorNode))); } /** * Snapshot the live selection before entering preview mode. * * @param {Object|null} editorState Active editor state. * @returns {void} */ function savePreviewSelection(editorState) { if (!editorState) return; editorState._previewSavedComponentId = editorState.activeComponentId || null; const focusTarget = getEditorFocusTarget(editorState); try { const selection = window.getSelection(); if (selection && selection.rangeCount > 0 && isSelectionWithinElement(selection, focusTarget)) { editorState._previewSavedRange = selection.getRangeAt(0).cloneRange(); } } catch (_) { // Ignore cross-browser selection edge cases. } } /** * Restore the saved selection after leaving preview mode. * * @param {Object|null} editorState Active editor state. * @returns {void} */ function restorePreviewSelection(editorState) { if (!editorState) return; const savedRange = editorState._previewSavedRange; delete editorState._previewSavedRange; const savedComponentId = editorState._previewSavedComponentId; delete editorState._previewSavedComponentId; let focusTarget = null; if (savedComponentId && Array.isArray(editorState.editableComponents)) { focusTarget = editorState.editableComponents.find(component => component?.id === savedComponentId)?.element || null; } if (!focusTarget) { focusTarget = getEditorFocusTarget(editorState); } if (!focusTarget) return; try { focusTarget.focus({ preventScroll: true }); const selection = window.getSelection(); if (!selection) return; if (savedRange) { selection.removeAllRanges(); selection.addRange(savedRange); return; } const fallbackRange = document.createRange(); fallbackRange.selectNodeContents(focusTarget); fallbackRange.collapse(false); selection.removeAllRanges(); selection.addRange(fallbackRange); } catch (_) { // Ignore selection restore failures and leave focus state as-is. } } /** * Reposition active editor chrome after returning from preview mode. * * @param {Object|null} editorState Active editor state. * @returns {void} */ function repositionActiveEditorUI(editorState) { if (!editorState) return; const positionMgr = SFE.PositionManager || {}; const positionNow = positionMgr.positionFloatingElements; const schedule = positionMgr.schedulePosition || positionMgr.debouncedPosition; if (typeof positionNow !== 'function') return; const targetElement = getEditorChromeAnchor(editorState) || getEditorFocusTarget(editorState) || null; const toolbar = editorState.toolbarContainer || null; const actions = editorState.actionsContainer || null; if (!targetElement || (!toolbar && !actions)) return; // Snap immediately when returning from preview. positionNow(targetElement, toolbar, actions, true); // Run one frame later for layout changes that settle right after mode switch. requestAnimationFrame(() => { if (SFE.Context?.activeEditor !== editorState) return; if (typeof schedule === 'function') { schedule(targetElement, toolbar, actions, true); } else { positionNow(targetElement, toolbar, actions, true); } }); // Final one-shot settle pass for CSS transitions (e.g. accordion opening). setTimeout(() => { if (SFE.Context?.activeEditor !== editorState) return; positionNow(targetElement, toolbar, actions, true); }, 180); } // Mode Toggle Bar UI // A panel with a state header and two buttons: // - The ACTIVE mode button -> secondary + disabled // - The INACTIVE mode button -> primary (call to action) /** * Refresh the mode toggle bar labels and button state. * * @returns {void} */ function update() { const ctx = SFE.Context; const bar = getBar(); if (!bar) return; const isPreview = !ctx.isInlineUIEnabled || document.body.classList.contains('mwp-sfe-active-preview'); const isSaving = !!ctx.isSaving; const entryLabel = getInlineEntryLabel(); const header = bar.querySelector('.mwp-sfe-mode-toggle-header'); const perms = SFE.ManagerData.permissions || {}; const canEdit = (perms.can_publish || perms.can_draft); const modeLabelText = canEdit ? 'Edit Mode' : 'Comment Mode'; if (header) { header.textContent = isPreview ? `Back to ${modeLabelText}` : 'Enter Preview Mode'; } const editBtn = bar.querySelector('.mwp-sfe-mode-toggle-edit-btn'); const previewBtn = bar.querySelector('.mwp-sfe-mode-toggle-preview-btn'); if (!editBtn || !previewBtn) return; if (isPreview) { editBtn.className = 'mwp-sfe-btn mwp-sfe-btn-primary-inline mwp-sfe-mode-toggle-edit-btn'; previewBtn.className = 'mwp-sfe-btn mwp-sfe-btn-secondary-inline mwp-sfe-mode-toggle-preview-btn'; } else { editBtn.className = 'mwp-sfe-btn mwp-sfe-btn-secondary-inline mwp-sfe-mode-toggle-edit-btn'; previewBtn.className = 'mwp-sfe-btn mwp-sfe-btn-primary-inline mwp-sfe-mode-toggle-preview-btn'; } if (isSaving) { editBtn.disabled = true; previewBtn.disabled = true; } else { editBtn.disabled = !isPreview; previewBtn.disabled = isPreview; } editBtn.textContent = entryLabel; previewBtn.textContent = 'Preview'; editBtn.setAttribute('aria-pressed', String(!isPreview)); previewBtn.setAttribute('aria-pressed', String(isPreview)); } /** * Create the mode toggle bar if needed and attach its behavior. * * @returns {void} */ function init() { const ctx = SFE.Context; let bar = getBar(); if (!bar) { bar = document.createElement('div'); bar.className = 'mwp-sfe-mode-toggle-bar'; bar.setAttribute('data-mwp-sfe-control', 'true'); bar.setAttribute('role', 'group'); bar.setAttribute('aria-label', 'Page editing mode'); const header = document.createElement('div'); header.className = 'mwp-sfe-mode-toggle-header'; bar.appendChild(header); const buttons = document.createElement('div'); buttons.className = 'mwp-sfe-mode-toggle-buttons'; const editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.className = 'mwp-sfe-btn mwp-sfe-btn-secondary-inline mwp-sfe-mode-toggle-edit-btn'; editBtn.addEventListener('click', function(event) { event.preventDefault(); event.stopPropagation(); if (SFE.Context.isSaving) return; if (!ctx.isInlineUIEnabled) setInlineUIEnabled(true); }); const previewBtn = document.createElement('button'); previewBtn.type = 'button'; previewBtn.className = 'mwp-sfe-btn mwp-sfe-btn-primary-inline mwp-sfe-mode-toggle-preview-btn'; previewBtn.addEventListener('mousedown', function() { if (SFE.Context.isSaving) return; if (!ctx.isInlineUIEnabled) return; savePreviewSelection(ctx.activeEditor); }); previewBtn.addEventListener('click', function(event) { event.preventDefault(); event.stopPropagation(); if (SFE.Context.isSaving) return; if (ctx.isInlineUIEnabled) setInlineUIEnabled(false); }); buttons.appendChild(editBtn); buttons.appendChild(previewBtn); bar.appendChild(buttons); document.body.appendChild(bar); } update(); ensureInteractiveBehavior(bar); } /** * Enable or disable inline UI mode while preserving active editor sessions. * * @param {boolean} enabled Target inline UI state. * @returns {void} */ function setInlineUIEnabled(enabled) { const ctx = SFE.Context; const overlayMgr = SFE.OverlayManager; const shouldEnable = !!enabled; if (ctx.isInlineUIEnabled === shouldEnable) return; ctx.isInlineUIEnabled = shouldEnable; document.body.classList.remove('mwp-sfe-preview-mode'); document.body.classList.remove('mwp-sfe-active-preview'); const activeEditor = ctx.activeEditor; const activeMode = ctx.activeMode; if (!shouldEnable) { if (activeEditor || activeMode) { document.body.classList.add('mwp-sfe-active-preview'); if (overlayMgr) overlayMgr.hideActive(); if (activeEditor) { savePreviewSelection(activeEditor); const active = document.activeElement; const root = activeEditor.element; if (active && root && (active === root || root.contains(active))) { active.blur(); } } } else { document.body.classList.add('mwp-sfe-preview-mode'); SFE.closeAnyActiveMode(); if (overlayMgr) { overlayMgr.hideHover(); overlayMgr.hideActive(); } const ht = SFE.hoverTracker; if (ht) { ht.lastHoveredElements = []; ht.currentGroupId = null; ht.bottommostElement = null; ht.isProcessing = false; } } } else { if (activeEditor || activeMode) { if (activeEditor && overlayMgr) { const overlayTarget = getEditorFocusTarget(activeEditor) || activeEditor.element; overlayMgr.showActive(overlayTarget, getEditorOverlayMode(activeEditor, ctx)); } if (!activeEditor && activeMode === 'comment' && overlayMgr) { const commentEl = document.querySelector('.mwp-sfe-commenting-active'); if (commentEl) overlayMgr.showActive(commentEl, 'commenting'); } if (!activeEditor && activeMode === 'draft' && overlayMgr) { const draftEl = document.querySelector('.mwp-sfe-draft-active'); if (draftEl) overlayMgr.showActive(draftEl, 'draft-preview'); } if (activeEditor) { repositionActiveEditorUI(activeEditor); setTimeout(() => { if (!ctx.activeEditor) return; repositionActiveEditorUI(ctx.activeEditor); restorePreviewSelection(ctx.activeEditor); }, 50); } } else if (overlayMgr) { overlayMgr.hideHover(); overlayMgr.updateAllOverlays(); } } update(); } SFE.ModeToggleBar = { init, update, setInlineUIEnabled }; })(); /** * עברית translation * @author Yaron Shahrabani * @version 2015-11-02 */ (function(root, factory) { if (typeof define === 'function' && define.amd) { define(['elfinder'], factory); } else if (typeof exports !== 'undefined') { module.exports = factory(require('elfinder')); } else { factory(root.elFinder); } }(this, function(elFinder) { elFinder.prototype.i18.he = { translator : 'Yaron Shahrabani ', language : 'עברית', direction : 'rtl', dateFormat : 'd.m.Y H:i', // Mar 13, 2012 05:27 PM fancyDateFormat : '$1 H:i', // will produce smth like: Today 12:25 PM messages : { /********************************** errors **********************************/ 'error' : 'שגיאה', 'errUnknown' : 'שגיאה בלתי מוכרת.', 'errUnknownCmd' : 'פקודה בלתי מוכרת.', 'errJqui' : 'תצורת ה־jQuery UI שגויה. יש לכלול רכיבים הניתנים לבחירה, גרירה והשלכה.', 'errNode' : 'elFinder דורש יצירה של רכיב DOM.', 'errURL' : 'התצורה של elFinder שגויה! אפשרות הכתובת (URL) לא הוגדרה.', 'errAccess' : 'הגישה נדחית.', 'errConnect' : 'לא ניתן להתחבר למנגנון.', 'errAbort' : 'החיבור בוטל.', 'errTimeout' : 'זמן החיבור פג.', 'errNotFound' : 'לא נמצא מנגנון.', 'errResponse' : 'תגובת המנגנון שגויה.', 'errConf' : 'תצורת המנגנון שגויה.', 'errJSON' : 'המודול PHP JSON לא מותקן.', 'errNoVolumes' : 'אין כוננים זמינים לקריאה.', 'errCmdParams' : 'פרמטרים שגויים לפקודה „$1“.', 'errDataNotJSON' : 'הנתונים אינם JSON.', 'errDataEmpty' : 'הנתונים ריקים.', 'errCmdReq' : 'בקשה למנגנון דורשת שם פקודה.', 'errOpen' : 'לא ניתן לפתוח את „$1“.', 'errNotFolder' : 'הפריט אינו תיקייה.', 'errNotFile' : 'הפריט אינו קובץ.', 'errRead' : 'לא ניתן לקרוא את „$1“.', 'errWrite' : 'לא ניתן לכתוב אל „$1“.', 'errPerm' : 'ההרשאה נדחתה.', 'errLocked' : '„$1“ נעול ואין אפשרות לשנות את שמו, להעבירו או להסירו.', 'errExists' : 'קובץ בשם „$1“ כבר קיים.', 'errInvName' : 'שם הקובץ שגוי.', 'errFolderNotFound' : 'התיקייה לא נמצאה.', 'errFileNotFound' : 'הקובץ לא נמצא.', 'errTrgFolderNotFound' : 'תיקיית היעד „$1“ לא נמצאה.', 'errPopup' : 'הדפדפן מנע פתיחת חלון קובץ. כדי לפתוח קובץ יש לאפשר זאת בהגדרות הדפדפן.', 'errMkdir' : 'לא ניתן ליצור את התיקייה „$1“.', 'errMkfile' : 'לא ניתן ליצור את הקובץ „$1“.', 'errRename' : 'לא ניתן לשנות את השם של „$1“.', 'errCopyFrom' : 'העתקת קבצים מהכונן „$1“ אינה מאופשרת.', 'errCopyTo' : 'העתקת קבצים אל הכונן „$1“ אינה מאופשרת.', 'errUpload' : 'שגיאת העלאה.', // old name - errUploadCommon 'errUploadFile' : 'לא ניתן להעלות את „$1“.', // old name - errUpload 'errUploadNoFiles' : 'לא נמצאו קבצים להעלאה.', 'errUploadTotalSize' : 'הנתונים חורגים מהגודל המרבי המותר.', // old name - errMaxSize 'errUploadFileSize' : 'הקובץ חורג מהגודל המרבי המותר.', // old name - errFileMaxSize 'errUploadMime' : 'סוג הקובץ אינו מורשה.', 'errUploadTransfer' : 'שגיאת העברה „$1“.', 'errNotReplace' : 'הפריט „$1“ כבר קיים במיקום זה ואי אפשר להחליפו בפריט מסוג אחר.', // new 'errReplace' : 'לא ניתן להחליף את „$1“.', 'errSave' : 'לא ניתן לשמור את „$1“.', 'errCopy' : 'לא ניתן להעתיק את „$1“.', 'errMove' : 'לא ניתן להעביר את „$1“.', 'errCopyInItself' : 'לא ניתן להעתיק את „$1“ לתוך עצמו.', 'errRm' : 'לא ניתן להסיר את „$1“.', 'errRmSrc' : 'לא ניתן להסיר את קובצי המקור.', 'errExtract' : 'לא ניתן לחלץ קבצים מהארכיון „$1“.', 'errArchive' : 'לא ניתן ליצור ארכיון.', 'errArcType' : 'סוג הארכיון אינו נתמך.', 'errNoArchive' : 'הקובץ אינו ארכיון או שסוג הקובץ שלו אינו נתמך.', 'errCmdNoSupport' : 'המנגנון אינו תומך בפקודה זו.', 'errReplByChild' : 'לא ניתן להחליף את התיקייה „$1“ בפריט מתוכה.', 'errArcSymlinks' : 'מטעמי אבטחה לא ניתן לחלץ ארכיונים שמכילים קישורים סימבוליים או קבצים עם שמות בלתי מורשים.', // edited 24.06.2012 'errArcMaxSize' : 'הארכיון חורג מהגודל המרבי המותר.', 'errResize' : 'לא ניתן לשנות את הגודל של „$1“.', 'errResizeDegree' : 'מעלות ההיפוך שגויות.', // added 7.3.2013 'errResizeRotate' : 'לא ניתן להפוך את התמונה.', // added 7.3.2013 'errResizeSize' : 'גודל התמונה שגוי.', // added 7.3.2013 'errResizeNoChange' : 'גודל התמונה לא השתנה.', // added 7.3.2013 'errUsupportType' : 'סוג הקובץ אינו נתמך.', 'errNotUTF8Content' : 'הקובץ „$1“ הוא לא בתסדיר UTF-8 ולא ניתן לערוך אותו.', // added 9.11.2011 'errNetMount' : 'לא ניתן לעגן את „$1“.', // added 17.04.2012 'errNetMountNoDriver' : 'פרוטוקול בלתי נתמך.', // added 17.04.2012 'errNetMountFailed' : 'העיגון נכשל.', // added 17.04.2012 'errNetMountHostReq' : 'נדרש מארח.', // added 18.04.2012 'errSessionExpires' : 'ההפעלה שלך פגה עקב חוסר פעילות.', 'errCreatingTempDir' : 'לא ניתן ליצור תיקייה זמנית: „$1“', 'errFtpDownloadFile' : 'לא ניתן להוריד קובץ מ־ FTP: „$1“', 'errFtpUploadFile' : 'לא ניתן להעלות קובץ ל־FTP: „$1“', 'errFtpMkdir' : 'לא ניתן ליצור תיקייה מרוחקת ב־FTP: „$1“', 'errArchiveExec' : 'שמירת הקבצים בארכיון נכשלה: „$1“', 'errExtractExec' : 'חילוץ קבצים נכשל: „$1“', /******************************* commands names ********************************/ 'cmdarchive' : 'יצירת ארכיון', 'cmdback' : 'חזרה', 'cmdcopy' : 'העתקה', 'cmdcut' : 'גזירה', 'cmddownload' : 'הורדה', 'cmdduplicate' : 'שכפול', 'cmdedit' : 'עריכת קובץ', 'cmdextract' : 'חילוץ קבצים מארכיון', 'cmdforward' : 'העברה', 'cmdgetfile' : 'בחירת קבצים', 'cmdhelp' : 'פרטים על התכנית הזו', 'cmdhome' : 'בית', 'cmdinfo' : 'קבלת מידע', 'cmdmkdir' : 'תיקייה חדשה', 'cmdmkfile' : 'קובץ חדש', 'cmdopen' : 'פתיחה', 'cmdpaste' : 'הדבקה', 'cmdquicklook' : 'תצוגה מקדימה', 'cmdreload' : 'רענון', 'cmdrename' : 'שינוי שם', 'cmdrm' : 'מחיקה', 'cmdsearch' : 'חיפוש קבצים', 'cmdup' : 'מעבר לתיקיית ההורה', 'cmdupload' : 'העלאת קבצים', 'cmdview' : 'תצוגה', 'cmdresize' : 'שינוי גודל והיפוך', 'cmdsort' : 'מיון', 'cmdnetmount' : 'עיגון כונן רשת', // added 18.04.2012 /*********************************** buttons ***********************************/ 'btnClose' : 'סגירה', 'btnSave' : 'שמירה', 'btnRm' : 'הסרה', 'btnApply' : 'החלה', 'btnCancel' : 'ביטול', 'btnNo' : 'לא', 'btnYes' : 'כן', 'btnDiscard': 'Discard changes', 'btnMount' : 'עיגון', // added 18.04.2012 /******************************** notifications ********************************/ 'ntfopen' : 'פתיחת תיקייה', 'ntffile' : 'פתיחת קובץ', 'ntfreload' : 'רענון תוכן התיקייה', 'ntfmkdir' : 'תיקייה נוצרת', 'ntfmkfile' : 'קבצים נוצרים', 'ntfrm' : 'קבצים נמחקים', 'ntfcopy' : 'קבצים מועתקים', 'ntfmove' : 'קבצים מועברים', 'ntfprepare' : 'העתקת קבצים בהכנה', 'ntfrename' : 'שמות קבצים משתנים', 'ntfupload' : 'קבצים נשלחים', 'ntfdownload' : 'קבצים מתקבלים', 'ntfsave' : 'שמירת קבצים', 'ntfarchive' : 'ארכיון נוצר', 'ntfextract' : 'מחולצים קבצים מארכיון', 'ntfsearch' : 'קבצים בחיפוש', 'ntfresize' : 'גודל קבצים משתנה', 'ntfsmth' : 'מתבצעת פעולה', 'ntfloadimg' : 'נטענת תמונה', 'ntfnetmount' : 'כונן רשת מעוגן', // added 18.04.2012 'ntfdim' : 'ממדי תמונה מתקבלים', // added 20.05.2013 /************************************ dates **********************************/ 'dateUnknown' : 'לא ידוע', 'Today' : 'היום', 'Yesterday' : 'מחר', 'msJan' : 'ינו׳', 'msFeb' : 'פבר׳', 'msMar' : 'מרץ', 'msApr' : 'אפר׳', 'msMay' : 'מאי', 'msJun' : 'יונ׳', 'msJul' : 'יול׳', 'msAug' : 'אוג׳', 'msSep' : 'ספט׳', 'msOct' : 'אוק׳', 'msNov' : 'נוב׳', 'msDec' : 'דצמ׳', 'January' : 'ינואר', 'February' : 'פברואר', 'March' : 'מרץ', 'April' : 'אפריל', 'May' : 'מאי', 'June' : 'יוני', 'July' : 'יולי', 'August' : 'אוגוסט', 'September' : 'ספטמבר', 'October' : 'אוקטובר', 'November' : 'נובמבר', 'December' : 'דצמבר', 'Sunday' : 'יום ראשון', 'Monday' : 'יום שני', 'Tuesday' : 'יום שלישי', 'Wednesday' : 'יום רביעי', 'Thursday' : 'יום חמישי', 'Friday' : 'יום שישי', 'Saturday' : 'שבת', 'Sun' : 'א׳', 'Mon' : 'ב׳', 'Tue' : 'ג׳', 'Wed' : 'ד׳', 'Thu' : 'ה', 'Fri' : 'ו׳', 'Sat' : 'ש׳', /******************************** sort variants ********************************/ 'sortname' : 'לפי שם', 'sortkind' : 'לפי סוג', 'sortsize' : 'לפי גודל', 'sortdate' : 'לפי תאריך', 'sortFoldersFirst' : 'תיקיות תחילה', /********************************** messages **********************************/ 'confirmReq' : 'נדרש אישור', 'confirmRm' : 'להסיר את הקבצים?
פעולה זו בלתי הפיכה!', 'confirmRepl' : 'להחליף קובץ ישן בקובץ חדש?', 'apllyAll' : 'להחיל על הכול', 'name' : 'שם', 'size' : 'גודל', 'perms' : 'הרשאות', 'modify' : 'שינוי', 'kind' : 'סוג', 'read' : 'קריאה', 'write' : 'כתיבה', 'noaccess' : 'אין גישה', 'and' : 'וגם', 'unknown' : 'לא ידוע', 'selectall' : 'בחירת כל הקבצים', 'selectfiles' : 'בחירת קובץ אחד ומעלה', 'selectffile' : 'בחירת הקובץ הראשון', 'selectlfile' : 'בחירת הקובץ האחרון', 'viewlist' : 'תצוגת רשימה', 'viewicons' : 'תצוגת סמלים', 'places' : 'מיקומים', 'calc' : 'חישוב', 'path' : 'נתיב', 'aliasfor' : 'כינוי עבור', 'locked' : 'נעול', 'dim' : 'ממדים', 'files' : 'קבצים', 'folders' : 'תיקיות', 'items' : 'פריטים', 'yes' : 'כן', 'no' : 'לא', 'link' : 'קישור', 'searcresult' : 'תוצאות חיפוש', 'selected' : 'קבצים נבחרים', 'about' : 'על אודות', 'shortcuts' : 'קיצורי דרך', 'help' : 'עזרה', 'webfm' : 'מנהל קבצים בדפדפן', 'ver' : 'גרסה', 'protocolver' : 'גרסת פרוטוקול', 'homepage' : 'דף הבית של המיזם', 'docs' : 'תיעוד', 'github' : 'פילוג עותק ב־Github', 'twitter' : 'לעקוב אחרינו בטוויטר', 'facebook' : 'להצטרף אלינו בפייסבוק', 'team' : 'צוות', 'chiefdev' : 'מפתח ראשי', 'developer' : 'מתכנת', 'contributor' : 'תורם', 'maintainer' : 'מתחזק', 'translator' : 'מתרגם', 'icons' : 'סמלים', 'dontforget' : 'לא לשכוח לקחת את המגבת שלך', 'shortcutsof' : 'קיצורי הדרך מנוטרלים', 'dropFiles' : 'ניתן להשליך את הקבצים לכאן', 'or' : 'או', 'selectForUpload' : 'לבחור קבצים להעלאה', 'moveFiles' : 'העברת קבצים', 'copyFiles' : 'העתקת קבצים', 'rmFromPlaces' : 'הסרה ממיקומים', 'aspectRatio' : 'יחס תצוגה', 'scale' : 'מתיחה', 'width' : 'רוחב', 'height' : 'גובה', 'resize' : 'שינוי הגודל', 'crop' : 'חיתוך', 'rotate' : 'היפוך', 'rotate-cw' : 'היפוך ב־90 מעלות נגד השעון', 'rotate-ccw' : 'היפוך ב־90 מעלות עם השעון CCW', 'degree' : '°', 'netMountDialogTitle' : 'עיגון כונן רשת', // added 18.04.2012 'protocol' : 'פרוטוקול', // added 18.04.2012 'host' : 'מארח', // added 18.04.2012 'port' : 'פתחה', // added 18.04.2012 'user' : 'משתמש', // added 18.04.2012 'pass' : 'ססמה', // added 18.04.2012 /********************************** mimetypes **********************************/ 'kindUnknown' : 'בלתי ידוע', 'kindFolder' : 'תיקייה', 'kindAlias' : 'כינוי', 'kindAliasBroken' : 'כינוי שבור', // applications 'kindApp' : 'יישום', 'kindPostscript' : 'מסמך Postscript', 'kindMsOffice' : 'מסמך Microsoft Office', 'kindMsWord' : 'מסמך Microsoft Word', 'kindMsExcel' : 'מסמך Microsoft Excel', 'kindMsPP' : 'מצגת Microsoft Powerpoint', 'kindOO' : 'מסמך Open Office', 'kindAppFlash' : 'יישום Flash', 'kindPDF' : 'Portable Document Format (PDF)', 'kindTorrent' : 'קובץ Bittorrent', 'kind7z' : 'ארכיון 7z', 'kindTAR' : 'ארכיון TAR', 'kindGZIP' : 'ארכיון GZIP', 'kindBZIP' : 'ארכיון BZIP', 'kindXZ' : 'ארכיון XZ', 'kindZIP' : 'ארכיון ZIP', 'kindRAR' : 'ארכיון RAR', 'kindJAR' : 'קובץ JAR של Java', 'kindTTF' : 'גופן True Type', 'kindOTF' : 'גופן Open Type', 'kindRPM' : 'חבילת RPM', // fonts 'kindFont' : 'גופן', 'kindSFNT' : 'גופן SFNT', 'kindEOT' : 'גופן Embedded Open Type', 'kindWOFF' : 'גופן Web Open Font Format', 'kindWOFF2' : 'גופן Web Open Font Format 2', // texts 'kindText' : 'מסמך טקסט', 'kindTextPlain' : 'טקסט פשוט', 'kindPHP' : 'מקור PHP', 'kindCSS' : 'גיליון סגנון מדורג', 'kindHTML' : 'מסמך HTML', 'kindJS' : 'מקור Javascript', 'kindRTF' : 'תבנית טקסט עשיר', 'kindC' : 'מקור C', 'kindCHeader' : 'מקור כותרת C', 'kindCPP' : 'מקור C++', 'kindCPPHeader' : 'מקור כותרת C++', 'kindShell' : 'תסריט מעטפת יוניקס', 'kindPython' : 'מקור Python', 'kindJava' : 'מקור Java', 'kindRuby' : 'מקור Ruby', 'kindPerl' : 'תסריט Perl', 'kindSQL' : 'מקור SQL', 'kindXML' : 'מקור XML', 'kindAWK' : 'מקור AWK', 'kindCSV' : 'ערכים מופרדים בפסיקים', 'kindDOCBOOK' : 'מסמךDocbook XML', // images 'kindImage' : 'תמונה', 'kindBMP' : 'תמונת BMP', 'kindJPEG' : 'תמונת JPEG', 'kindGIF' : 'תמונת GIF', 'kindPNG' : 'תמונת PNG', 'kindTIFF' : 'תמונת TIFF', 'kindTGA' : 'תמונת TGA', 'kindPSD' : 'תמונת Adobe Photoshop', 'kindXBITMAP' : 'תמונת מפת סיביות X', 'kindPXM' : 'תמונת Pixelmator', // media 'kindAudio' : 'מדיה מסוג שמע', 'kindAudioMPEG' : 'שמע MPEG', 'kindAudioMPEG4' : 'שמע MPEG-4', 'kindAudioMIDI' : 'שמע MIDI', 'kindAudioOGG' : 'שמע Ogg Vorbis', 'kindAudioWAV' : 'שמע WAV', 'AudioPlaylist' : 'רשימת נגינה MP3', 'kindVideo' : 'מדיה מסוג וידאו', 'kindVideoDV' : 'סרטון DV', 'kindVideoMPEG' : 'סרטון MPEG', 'kindVideoMPEG4' : 'סרטון MPEG-4', 'kindVideoAVI' : 'סרטון AVI', 'kindVideoMOV' : 'סרטון Quick Time', 'kindVideoWM' : 'סרטון Windows Media', 'kindVideoFlash' : 'סרטון Flash', 'kindVideoMKV' : 'סרטון Matroska', 'kindVideoOGG' : 'סרטון Ogg' } }; })); /** * Save Helpers - shared utilities for save, batch-save, and comment flows * * Reads (via globals): * SFE.Context - .pageRevisionToken (r/w) * * Exposes: SFE.SaveHelpers * { setButtonLoading, clearButtonLoading, lockSaveUI, unlockSaveUI, * createSuccessElement, handleRevisionConflict, updatePageRevisionToken, * fetchRenderedPageDocument, fetchRenderedHTMLMap, fetchRenderedBlockData, * fetchRenderedBlockHTML, syncWpElementStyles, reloadPageWithGuardBypass, * reloadAfterRefreshFailure } */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const PAGE_DOC_CACHE_TTL_MS = 1500; const PREVIEW_RENDER_ROUTE = '/pro/draft-preview-url'; let cachedPageDoc = null; let cachedPageDocKey = ''; let cachedPageDocAt = 0; let cachedPageDocPromise = null; let cachedPageDocPendingKey = ''; const syncedWpElementClasses = new Set(); /** * Normalize a UUID list into a unique array of trimmed strings. * * @param {Array} uuids Raw UUID values. * @returns {string[]} Unique, non-empty UUIDs. */ function normalizeRequestedUuids(uuids) { if (!Array.isArray(uuids)) return []; return [...new Set( uuids .map(uuid => typeof uuid === 'string' ? uuid.trim() : '') .filter(Boolean) )]; } /** * Normalize an optional draft-preview render request. * * @param {object} options Fetch options passed to SaveHelpers. * @returns {{postId:number, elementUuid:string, rawContent:string, handlerId:string}|null} * Normalized preview request, or null when the caller is fetching * the current published page render. */ function normalizeDraftPreviewRequest(options = {}) { const draftPreview = options && typeof options === 'object' ? options.draftPreview : null; if (!draftPreview || typeof draftPreview !== 'object') { return null; } const postId = Number.parseInt(draftPreview.postId, 10); const elementUuid = typeof draftPreview.elementUuid === 'string' ? draftPreview.elementUuid.trim() : ''; const rawContent = typeof draftPreview.rawContent === 'string' ? draftPreview.rawContent : ''; const handlerId = typeof draftPreview.handlerId === 'string' ? draftPreview.handlerId.trim() : ''; if (!Number.isFinite(postId) || postId <= 0 || !elementUuid || !rawContent.trim()) { return null; } return { postId, elementUuid, rawContent, handlerId }; } /** * Normalize a server-created, user-bound draft preview URL. * * @param {object} options Render options. * @returns {string} Preview URL or an empty string. */ function normalizeDraftPreviewUrl(options = {}) { return typeof options?.draftPreviewUrl === 'string' ? options.draftPreviewUrl.trim() : ''; } /** * Build the standard refresh URL for the current frontend page. * * @returns {URL} Refresh URL for the current page. */ function buildRefreshPageURL() { const url = new URL(window.location.href, window.location.origin); url.hash = ''; url.searchParams.set('mwpsfe_refresh', '1'); return url; } /** * Parse an HTML string into a DOM document. * * @param {string} html Raw response HTML. * @returns {Document} Parsed HTML document. * @throws {Error} When the response cannot be parsed. */ function parsePageDocumentFromHTML(html) { const doc = new DOMParser().parseFromString(String(html || ''), 'text/html'); if (!doc || !doc.documentElement) { throw new Error('BLOCK_HTML_REFRESH_FAILED'); } return doc; } /** * Extract rendered outerHTML strings for the requested UUID nodes. * * @param {Document} doc Parsed page document. * @param {string[]} requestedUuids UUIDs to extract. * @returns {Object} UUID => rendered outerHTML. */ function extractRenderedHTMLMapFromDocument(doc, requestedUuids) { const requested = normalizeRequestedUuids(requestedUuids); if (!requested.length || !doc || typeof doc.querySelectorAll !== 'function') { return {}; } const wanted = new Set(requested); const htmlMap = {}; for (const node of doc.querySelectorAll('[data-mwp-sfe-uuid]')) { const uuid = String(node.getAttribute('data-mwp-sfe-uuid') || '').trim(); if (!uuid || !wanted.has(uuid) || htmlMap[uuid]) continue; if (typeof node.outerHTML === 'string' && node.outerHTML.trim()) { htmlMap[uuid] = node.outerHTML; } } return htmlMap; } /** * Collect every `wp-elements-*` class on an element and its descendants. * * @param {Element} element DOM subtree to inspect. * @returns {string[]} Unique generated class names. */ function collectWpElementClasses(element) { if (!element || typeof element.querySelectorAll !== 'function') { return []; } const classes = new Set(); const nodes = [element, ...element.querySelectorAll('[class]')]; for (const node of nodes) { for (const cls of node.classList) { if (/^wp-elements-/.test(cls)) { classes.add(cls); } } } return [...classes]; } /** * Return true when the current page already contains CSS rules for a class. * * @param {string} className CSS class to locate. * @returns {boolean} True when at least one stylesheet defines it. */ function isCssDefined(className) { for (const sheet of document.styleSheets) { try { for (const rule of sheet.cssRules || []) { if (rule.selectorText && rule.selectorText.includes('.' + className)) { return true; } } } catch (_) { /* cross-origin stylesheet */ } } return false; } /** * Request a short-lived preview URL that renders the current page with one * draft block substituted before the page template runs. * * @param {{postId:number, elementUuid:string, rawContent:string, handlerId:string}} draftPreview * Draft preview render payload. * @returns {Promise} Absolute preview URL. * @throws {Error} When the preview URL cannot be created. */ async function fetchPreviewRenderURL(draftPreview) { const restBase = String(SFE.ManagerData?.restUrl || '').trim(); const nonce = String(SFE.ManagerData?.nonce || '').trim(); if (!restBase || !nonce) { throw new Error('BLOCK_HTML_REFRESH_FAILED'); } let response; try { response = await fetch(restBase + PREVIEW_RENDER_ROUTE, { method: 'POST', credentials: 'same-origin', cache: 'no-store', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce }, body: JSON.stringify({ post_id: draftPreview.postId, element_uuid: draftPreview.elementUuid, preview_raw_content: draftPreview.rawContent, handler_id: draftPreview.handlerId || '' }) }); } catch (error) { console.warn('FrontEdit: Failed to request draft preview render URL', error); throw new Error('BLOCK_HTML_REFRESH_FAILED'); } if (!response.ok) { throw new Error('BLOCK_HTML_REFRESH_FAILED'); } const data = await response.json(); const url = typeof data?.url === 'string' ? data.url.trim() : ''; if (!url) { throw new Error('BLOCK_HTML_REFRESH_FAILED'); } return url; } /** * Resolve the rendered page request for either a normal published refresh or * a draft preview refresh. * * @param {object} options Save-helper fetch options. * @returns {Promise<{url:string, cacheKey:string, cacheable:boolean}>} * Fetch metadata for the requested render. */ async function resolveRenderedPageRequest(options = {}) { const draftPreviewUrl = normalizeDraftPreviewUrl(options); if (draftPreviewUrl) { return { url: draftPreviewUrl, cacheKey: draftPreviewUrl, cacheable: false }; } const draftPreview = normalizeDraftPreviewRequest(options); if (draftPreview) { const url = await fetchPreviewRenderURL(draftPreview); return { url, cacheKey: url, cacheable: false }; } const baseUrl = buildRefreshPageURL(); const cacheKey = baseUrl.toString(); const requestUrl = new URL(cacheKey); requestUrl.searchParams.set('mwpsfe_ts', String(Date.now())); return { url: requestUrl.toString(), cacheKey, cacheable: true }; } /** * Fetch and parse the rendered page document for the requested context. * * @param {object} [options={}] Fetch options. * @param {boolean} [options.force=false] Bypass the short-lived live-page cache. * @param {{postId:number, elementUuid:string, rawContent:string, handlerId:string}} [options.draftPreview] * Optional draft preview override payload. * @param {string} [options.draftPreviewUrl] User-bound draft preview URL returned by Pro. * @returns {Promise} Parsed rendered page document. */ async function fetchRenderedPageDocument(options = {}) { const { force = false } = options || {}; const { url, cacheKey, cacheable } = await resolveRenderedPageRequest(options); const now = Date.now(); if ( cacheable && !force && cachedPageDoc && cachedPageDocKey === cacheKey && (now - cachedPageDocAt) < PAGE_DOC_CACHE_TTL_MS ) { return cachedPageDoc; } if (cachedPageDocPromise && cachedPageDocPendingKey === cacheKey) { return cachedPageDocPromise; } cachedPageDocPendingKey = cacheKey; cachedPageDocPromise = fetch(url, { credentials: 'same-origin', cache: 'no-store' }) .then(response => { if (!response.ok) { throw new Error('BLOCK_HTML_REFRESH_FAILED'); } return response.text(); }) .then(html => parsePageDocumentFromHTML(html)) .then(doc => { if (cacheable) { cachedPageDoc = doc; cachedPageDocKey = cacheKey; cachedPageDocAt = Date.now(); } return doc; }) .catch(error => { console.warn('FrontEdit: Failed to fetch rendered page HTML', error); throw new Error('BLOCK_HTML_REFRESH_FAILED'); }) .finally(() => { cachedPageDocPromise = null; cachedPageDocPendingKey = ''; }); return cachedPageDocPromise; } /** * Fetch rendered HTML for a set of UUIDs from the authoritative page render. * * @param {string[]} uuids Requested UUIDs. * @param {object} [options] Page fetch options. * @returns {Promise>} UUID => rendered outerHTML. */ async function fetchRenderedHTMLMap(uuids, options = {}) { const requested = normalizeRequestedUuids(uuids); if (!requested.length) return {}; const doc = await fetchRenderedPageDocument(options); const htmlMap = extractRenderedHTMLMapFromDocument(doc, requested); const missing = requested.filter(uuid => !htmlMap[uuid]); if (missing.length) { console.warn('FrontEdit: Rendered page fetch did not contain requested UUIDs', { requested, missing }); throw new Error('BLOCK_HTML_REFRESH_FAILED'); } return htmlMap; } /** * Fetch a rendered block and the parsed page document it came from. * * @param {string} uuid Target block UUID. * @param {object} [options] Page fetch options. * @returns {Promise<{html:string, document:Document}>} * Rendered outerHTML and parsed page document. */ async function fetchRenderedBlockData(uuid, options = {}) { const requestedUuid = typeof uuid === 'string' ? uuid.trim() : ''; if (!requestedUuid) { throw new Error('BLOCK_HTML_REFRESH_FAILED'); } const doc = await fetchRenderedPageDocument(options); const htmlMap = extractRenderedHTMLMapFromDocument(doc, [requestedUuid]); const html = htmlMap[requestedUuid] || ''; if (!html) { console.warn('FrontEdit: Rendered page fetch did not contain requested UUID', { requested: requestedUuid }); throw new Error('BLOCK_HTML_REFRESH_FAILED'); } return { html, document: doc }; } /** * Fetch the rendered outerHTML for a single UUID. * * @param {string} uuid Target block UUID. * @param {object} [options] Page fetch options. * @returns {Promise} Rendered outerHTML. */ async function fetchRenderedBlockHTML(uuid, options = {}) { const rendered = await fetchRenderedBlockData(uuid, options); return rendered.html || ''; } /** * Sync missing `wp-elements-*` style rules for an element from a rendered page. * * @param {Element} element DOM element whose generated classes should exist. * @param {object} [options={}] Style-sync options. * @param {Document} [options.document] Pre-fetched rendered page document. * @returns {Promise} */ async function syncWpElementStyles(element, options = {}) { const allClasses = collectWpElementClasses(element); const missing = allClasses.filter( cls => !syncedWpElementClasses.has(cls) && !isCssDefined(cls) ); if (!missing.length) return; try { const doc = options.document || await fetchRenderedPageDocument(options); let css = ''; for (const style of doc.querySelectorAll('style')) { const text = style.textContent || ''; if (missing.some(cls => text.includes(cls))) { css += text + '\n'; } } if (css) { const tag = document.createElement('style'); tag.dataset.mwpSfeSyncedStyles = '1'; tag.textContent = css; document.head.appendChild(tag); missing.forEach(cls => syncedWpElementClasses.add(cls)); } } catch (error) { console.warn('FrontEdit: failed to sync wp-elements styles from rendered page', error); } } /** * Puts a button into a disabled "loading" state. * * This will: * - Store the current button text on a temporary `_mwpOriginalText` property * - Disable the button to prevent further interaction * - Add a `mwp-sfe-btn-loading` attribute for styling/state indication * * The stored text is later restored by {@link clearButtonLoading}. * * Safe to call with `null` or undefined (no-op). * * @param {HTMLButtonElement|null} btn The button element to update. * @return {void} * * @property {string} [_mwpOriginalText] Internal property added to the button element * to preserve its original text content while in loading state. */ function setButtonLoading(btn) { if (!btn) return; btn._mwpOriginalText = btn.textContent; const originalText = String(btn._mwpOriginalText || '').trim(); if (originalText === 'Save Changes') { btn.textContent = 'Saving...'; } else if (originalText === 'Submit for Review') { btn.textContent = 'Submitting...'; } btn.disabled = true; btn.setAttribute('mwp-sfe-btn-loading', 'true'); } /** * Restores a button previously set into a loading state via `setButtonLoading`. * * This will: * - Re-enable the button * - Remove the loading attribute used for styling/state tracking * - Restore the original button text if it was stored * * Safe to call with `null` or undefined (no-op). * * @param {HTMLButtonElement|null} btn The button element to restore. * @return {void} * * @property {string} [_mwpOriginalText] Internal property added to the button element * to preserve its original text content while in loading state. */ function clearButtonLoading(btn) { if (!btn) return; btn.disabled = false; btn.removeAttribute('mwp-sfe-btn-loading'); if (btn._mwpOriginalText !== undefined) { btn.textContent = btn._mwpOriginalText; delete btn._mwpOriginalText; } } /** * Locks the UI during a save operation to prevent duplicate actions * and inconsistent state changes. * * This will: * - Set the global `isSaving` flag in `SFE.Context` * - Put the triggering button into a loading state * - Disable all other buttons in the state dock * - Trigger a refresh/update of the mode toggle UI (if present) * * @param {HTMLButtonElement|null} triggerBtn The button that initiated the save action. * @return {void} */ function lockSaveUI(triggerBtn) { const ctx = SFE.Context; if (ctx) { ctx.isSaving = true; // Stash the button that triggered this save so unlockSaveUI can restore it ctx._saveTriggerBtn = triggerBtn || null; } setButtonLoading(triggerBtn); // Lock dock buttons document.querySelectorAll('.mwp-sfe-state-dock .mwp-sfe-btn').forEach(btn => { if (btn !== triggerBtn) btn.disabled = true; }); // Lock mode toggle if (SFE.ModeToggleBar) { SFE.ModeToggleBar.update(); } // Hide any hover overlay that is currently showing const overlayMgr = SFE.OverlayManager; if (overlayMgr) { overlayMgr.hideHover(); // Hide switchable status overlays if (!ctx || !ctx.activeEditor) { overlayMgr.hideSwitchableStatusOverlays(); } } } /** * Unlocks the UI after a save operation completes, restoring interactivity. * * This will: * - Clear the global `isSaving` flag in `SFE.Context` * - Restore the triggering button from its loading state * - Re-enable all buttons in the state dock * - Trigger a refresh/update of the mode toggle UI (if present) * * @param {HTMLButtonElement|null} triggerBtn The button that initiated the save action. * @return {void} */ function unlockSaveUI(triggerBtn) { const ctx = SFE.Context; if (ctx) ctx.isSaving = false; // Resolve which button to restore. lockSaveUI stashed the real trigger so that // showInlineSuccess (which only knows about the inline editor's save button) can // still clear the dock or approve button's loading state correctly. const effectiveBtn = (ctx && ctx._saveTriggerBtn) ? ctx._saveTriggerBtn : triggerBtn; if (ctx && ctx._saveTriggerBtn) delete ctx._saveTriggerBtn; clearButtonLoading(effectiveBtn); // Unlock dock buttons and strip any residual loading attribute so the dock document.querySelectorAll('.mwp-sfe-state-dock .mwp-sfe-btn').forEach(btn => { btn.disabled = false; btn.removeAttribute('mwp-sfe-btn-loading'); }); // Unlock mode toggle if (SFE.ModeToggleBar) { SFE.ModeToggleBar.update(); } // Restore status overlays that were hidden during the save lock. const overlayMgr = SFE.OverlayManager; if (overlayMgr) { overlayMgr.showAllStatusOverlays(); } } /** * Build the standard centered overlay element. * * @param {string} message Text shown inside the banner. * @param {string} [variant='success'] 'success' (green), 'comment' (blue), 'warning' (orange), or 'discard' (red). * @return {HTMLDivElement} */ function createSuccessElement(message, variant) { variant = variant || 'success'; const isComment = variant === 'comment'; const isDiscard = variant === 'discard'; const isWarning = variant === 'warning'; const iconSvg = isDiscard ? ` ` : ` `; const el = document.createElement('div'); let className = 'mwp-sfe-inline-success'; if (isComment) { className += ' mwp-sfe-inline-comment'; } else if (isWarning) { className += ' mwp-sfe-inline-warning'; } else if (isDiscard) { className += ' mwp-sfe-inline-discard'; } el.className = className; el.setAttribute('data-mwp-sfe-control', 'true'); el.innerHTML = `
${iconSvg} ${message}
`; el.style.cssText = ` position: fixed !important; top: 25% !important; left: 50% !important; transform: translate(-50%, -50%) !important; width: min(600px, 95vw) !important; z-index: 999999 !important; margin: 0 !important; display: flex; justify-content: center; transition: none !important; `; return el; } /** * Reload the current page after suppressing one native beforeunload prompt. * * Frontend save flows sometimes need an authoritative full refresh after the * server has already accepted a change. In those cases the page may still have * an active editor session or dirty-state bookkeeping that would otherwise * trigger the shared unsaved-changes guard during the intentional reload. * * @returns {void} */ function reloadPageWithGuardBypass() { if ( SFE.UnsavedChanges && typeof SFE.UnsavedChanges.suppressNextBeforeUnload === 'function' ) { SFE.UnsavedChanges.suppressNextBeforeUnload(); } const refreshUrl = buildRefreshPageURL(); refreshUrl.searchParams.set('mwpsfe_ts', String(Date.now())); window.location.assign(refreshUrl.toString()); } /** * Handle a REVISION_CONFLICT error from the server. * * Shows a confirmation dialog. If the user cancels, calls onRestoreBtn * (if provided) and reloads the page. If the user confirms, returns true * so the caller can proceed with a forced retry (no token). * * Usage: * const shouldRetry = await handleRevisionConflict(restoreBtn); * if (!shouldRetry) return; * // ... perform retry without the revision token ... * * @param {Function|null} onRestoreBtn Called when user cancels (before reload). * @return {Promise} true → caller should retry; false → reloading. */ async function handleRevisionConflict(onRestoreBtn) { const saveAnyway = confirm( 'This page was updated by another user since you started editing. ' + 'Saving now may overwrite their recent changes.\n\n' + 'Press OK to save anyway, or Cancel to refresh the page.' ); if (!saveAnyway) { if (onRestoreBtn) onRestoreBtn(); reloadPageWithGuardBypass(); return false; } return true; } /** * Update the page revision token stored in the shared context when the * server returns a new one. Safe to call when result is null/undefined. * * @param {Object|null} result API response object. * @return {void} */ function updatePageRevisionToken(result) { if (result && result.page_revision_token) { SFE.Context.pageRevisionToken = result.page_revision_token; } } /** * When the save itself succeeded but the server could not return the * context-accurate replacement HTML, reload the page so the user lands on * the authoritative frontend render instead of a mismatched DOM snapshot. * * @param {string} logMessage * @return {void} */ function reloadAfterRefreshFailure(logMessage) { if (logMessage) { console.error(logMessage); } reloadPageWithGuardBypass(); } SFE.SaveHelpers = { setButtonLoading, clearButtonLoading, lockSaveUI, unlockSaveUI, createSuccessElement, handleRevisionConflict, updatePageRevisionToken, fetchRenderedPageDocument, fetchRenderedHTMLMap, fetchRenderedBlockData, fetchRenderedBlockHTML, syncWpElementStyles, reloadPageWithGuardBypass, reloadAfterRefreshFailure }; })();