#wpadminbar #wp-admin-bar-new_draft > .ab-item::before { content: url("data:image/svg+xml;utf8,"); top: 2px; } @media screen and (max-width: 782px){ #wpadminbar li#wp-admin-bar-new_draft{ display: block; } #wpadminbar #wp-admin-bar-new_draft > .ab-item { text-indent: 100%; white-space: nowrap; overflow: hidden; width: 52px; padding: 0; color: #999; position: relative; } #wpadminbar #wp-admin-bar-new_draft > .ab-item::before { display: block; text-indent: 0; font: 400 32px/1 dashicons; speak: none; top: 0px; width: 52px; text-align: center; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } }/** * Action bar - floating UI strip that appears above editable elements * * Reads (via globals): * SFE.Context - .actionBar, .activeEditor * SFE.BatchEditManager - .isSessionActive, .isEnabled, .startInlineCommenting * SFE.OverlayManager - .showHover * SFE.hoverTracker - .currentGroupId, .bottommostElement, * .lastHoveredElements, .isProcessing, .currentMousePos * SFE.loadPendingDraft - set by DraftManager * SFE.startEditing - set by frontend-inline-edit.js * SFE.startCommenting - set by CommentManager * SFE.exitCommentMode - set by CommentManager * SFE.closeDraftPreview - set by DraftManager * * Exposes: SFE.ActionBar (class - instantiated by frontend-inline-edit.js) * { manage, show, hide, updateState, reset, showMultiple, * setMultiElementHoverAnchor, isPointerInHoverTransferCorridor, * getOrCreate } */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; class ActionBar { constructor(dependencies) { // Dependencies this.TIMING = dependencies.TIMING; this.perms = dependencies.perms; this.postId = dependencies.postId; this.restBase = dependencies.restBase; this.apiCall = dependencies.apiCall; this.getApplicableHandlers = dependencies.getApplicableHandlers; this.positionFloatingElements = dependencies.positionFloatingElements; this.schedulePosition = dependencies.schedulePosition || dependencies.debouncedPosition || dependencies.positionFloatingElements; this.ElementState = dependencies.ElementState; this.ElementPrep = dependencies.ElementPrep; this.attachEventListener = dependencies.attachEventListener; this.removeEventListener = dependencies.removeEventListener; // State this.activeBar = null; this.activeElement = null; this.hoverTimeout = null; } /** * UNIFIED ACTION BAR MANAGER * Centralizes all action bar operations for consistency * * @param {string} action - 'show' | 'hide' | 'updateState' | 'reset' | 'showMultiple' * @param {Object} options - Configuration object * @returns {HTMLElement|void} The action bar element or void */ manage(action, options = {}) { const { element = null, anchorElement = null, handlers = [], uuid = null, state = 'hover', bar = null, modeCleanupCallback = null } = options; switch(action) { case 'show': return this._showActionBar(element, handlers, uuid); case 'hide': return this._hideActionBar(); case 'updateState': return this._updateActionBarState({ bar: bar || this.activeBar, element, handlers, uuid, state, content: options.content || null }); case 'reset': return this._resetActionBar(bar || this.activeBar, element, uuid, modeCleanupCallback); case 'showMultiple': return this._showMultipleActionBar(options.elements, anchorElement); default: console.warn('ActionBar.manage: Unknown action', action); return null; } } /** * Public API - Convenience wrappers */ show(element, handlers, uuid) { return this.manage('show', { element, handlers, uuid }); } hide() { return this.manage('hide'); } updateState(config) { return this.manage('updateState', config); } reset(bar, element, uuid, modeCleanupCallback) { return this.manage('reset', { bar, element, uuid, modeCleanupCallback }); } /** * Show the multi-element hover bar for directly overlapping elements. * * @param {HTMLElement[]} elements Overlap-group elements displayed as rows. * @param {HTMLElement} anchorElement Element currently under the pointer; * it controls the bar position and initial focus. * @returns {HTMLElement} The visible multi-element action bar. */ showMultiple(elements, anchorElement) { return this.manage('showMultiple', { elements, anchorElement }); } /** * Move an open multi-element hover bar to the editable element directly * under the pointer without changing the overlap rows or their focus state. * * @param {HTMLElement} anchorElement Directly hovered overlap-group element. * @returns {void} */ setMultiElementHoverAnchor(anchorElement) { const bar = this.activeBar; if ( !bar || !bar._multiElements || !bar._multiElements.includes(anchorElement) || bar._targetElement === anchorElement ) { return; } this.activeElement = anchorElement; bar._targetElement = anchorElement; // This is a pointer-driven state change, not a scroll update. Restore the // normal position transition before writing the new coordinates; the // PositionManager's scroll listener will suppress transitions again later. bar.classList.remove('mwp-sfe-no-position-transition'); this._markTransitioning(bar); this.positionFloatingElements(anchorElement, null, bar, true); this._attachScrollListener(bar, anchorElement); } /** * Return whether a pointer is crossing the short gap between the current * hover anchor and its action bar. * * The action bar normally sits eight pixels below its anchor. When an inner * block overlaps an outer block, that gap belongs to the outer block's hit * area, which would otherwise cause HoverManager to re-anchor the bar before * the pointer reaches it. This geometric corridor preserves the current * hover state only for that deliberate block-to-bar transfer. * * The corridor is intentionally virtual: an invisible DOM element would * either fail to affect hit testing or intercept page clicks. It is also * limited to the action bar's normal adjacent placement, so a viewport * fallback position never creates a large area that suppresses real hovers. * * @param {number} clientX Pointer client X coordinate. * @param {number} clientY Pointer client Y coordinate. * @returns {boolean} True when the pointer is in the protected transfer corridor. */ isPointerInHoverTransferCorridor(clientX, clientY) { const bar = this.activeBar; const anchor = bar?._targetElement || this.activeElement; const isHoverState = !!( bar && ( bar.classList.contains('mwp-sfe-state-hover') || bar.classList.contains('mwp-sfe-state-hover-multi') ) ); if (!isHoverState || !(anchor instanceof HTMLElement)) { return false; } const anchorRect = anchor.getBoundingClientRect(); const barRect = bar.getBoundingClientRect(); const gap = barRect.top - anchorRect.bottom; const maxGap = 24; // Only protect the normal below-anchor handoff. Sticky/viewport fallback // positions are intentionally excluded because their separation can be // large and should not hide a real block hover. if (gap <= 0 || gap > maxGap || clientY < anchorRect.bottom || clientY > barRect.top) { return false; } // Connect the anchor's bottom edge to the action bar's top edge as a // narrow funnel. This allows a natural diagonal approach when the bar was // clamped horizontally while excluding unrelated blocks beside the path. const progress = (clientY - anchorRect.bottom) / gap; const left = anchorRect.left + ((barRect.left - anchorRect.left) * progress); const right = anchorRect.right + ((barRect.right - anchorRect.right) * progress); return clientX >= left && clientX <= right; } /** * Internal: Show action bar in hover state */ _showActionBar(element, handlers, uuid) { // Clear any pending timeouts immediately if (this.hoverTimeout) { clearTimeout(this.hoverTimeout); this.hoverTimeout = null; } const bar = this.getOrCreate(); // Set the target element IMMEDIATELY so the dock can // verify hover status during the transition this.activeElement = element; // Cancel any in-progress dock animation (e.g. user clicks while bar is // sliding between dock and element). Matches the existing behavior of // hovering an element while the editor-close animation is playing. const dock = SFE.ActionBarDock; dock?.cancelAnimation?.(); // If the bar is currently docked, play the two-phase undock animation // (slide-down-reverse out of dock, slide-up into element) before // rendering hover state. The dock calls _continueShowActionBar as its // onDone callback once the exit animation completes. if (dock?.shouldHandleShow?.()) { dock.handleShow(bar, this, () => this._continueShowActionBar(bar, element, handlers, uuid)); return bar; } return this._continueShowActionBar(bar, element, handlers, uuid); } /** * Continue showing the action bar at the given element. * Called directly by _showActionBar (normal path) or as an onDone callback * from the dock's undock animation (dock → element transition). */ _continueShowActionBar(bar, element, handlers, uuid) { if (bar._closingTimeout) { clearTimeout(bar._closingTimeout); bar._closingTimeout = null; } // Stop any closing animation and force visibility bar.classList.remove('mwp-sfe-closing'); bar.style.display = 'flex'; void bar.offsetWidth; // Force reflow bar.style.opacity = '1'; bar.style.pointerEvents = 'auto'; // Position-transition policy: // Normal path → enable transitions (smooth slide between elements). // Dock-to-element → keep transitions suppressed so the bar doesn't // animate across the viewport from the dock's coordinates to the // element's coordinates. The dock sets bar._suppressPositionTransition // before calling this method and clears it in the next rAF. if (!bar._suppressPositionTransition) { bar.classList.remove('mwp-sfe-no-position-transition'); this._markTransitioning(bar); } else { bar.classList.add('mwp-sfe-no-position-transition'); } // If element is already active, just ensure visibility if (element.classList.contains('mwp-sfe-element-active')) { return bar; } // Update reference and state this.activeElement = element; bar._targetElement = element; // Render hover state via the content-based path this._updateActionBarState({ bar, element, handlers, uuid, state: 'hover', content: this._buildHoverContent(element, handlers, uuid, bar) }); // Setup scroll listener for hover state this._attachScrollListener(bar, element); return bar; } /** * Format a handler's elementType + description into a header string */ _formatHandlerHeader(handler) { if (!handler) return 'Element'; const type = handler.elementType || 'Element'; const desc = handler.description || ''; return desc ? `${type} • ${desc}` : type; } /** * Build button config objects for the hover state of an element. * Returns plain config objects (not DOM elements) so they can be consumed * by both the content-based renderer (_buildHoverContent) and the * multi-element bar builder (_showMultipleActionBar). * * Each config: { text, className, onClick, hoverHeader? } */ _getHoverButtonConfigs(element, handlers, uuid, bar) { const isPending = element.classList.contains('mwp-sfe-status-pending'); if (isPending) { return [{ text: 'View Draft', className: 'mwp-sfe-btn mwp-sfe-btn-primary-inline', onClick: (e) => { e.stopPropagation(); const btn = e.currentTarget; btn.disabled = true; btn.setAttribute('mwp-sfe-btn-loading', 'true'); SFE.loadPendingDraft(bar, element, uuid, handlers); } }]; } const editHandler = handlers.find(h => h.capability === 'edit'); const commentHandler = handlers.find(h => h.capability === 'comment'); const configs = []; if (editHandler) { configs.push({ text: editHandler.actionLabel || 'Edit', className: 'mwp-sfe-btn mwp-sfe-btn-primary-inline', hoverHeader: this._formatHandlerHeader(editHandler), onClick: (e) => { e.stopPropagation(); SFE.startEditing(element, editHandler, uuid, null); } }); } if (commentHandler) { configs.push({ text: commentHandler.actionLabel || 'Comment', className: 'mwp-sfe-btn ' + (editHandler ? 'mwp-sfe-btn-secondary-inline' : 'mwp-sfe-btn-primary-inline'), hoverHeader: this._formatHandlerHeader(commentHandler), onClick: (e) => { e.stopPropagation(); this._startCommentMode(bar, element, handlers, uuid); } }); } return configs; } /** * Build a complete content config object for the hover state. * Consumed by _updateActionBarState via the content-based rendering path. */ _buildHoverContent(element, handlers, uuid, bar) { const isPending = element.classList.contains('mwp-sfe-status-pending'); const primaryHandler = isPending ? null : (handlers.find(h => h.capability === 'edit') || handlers[0]); const header = isPending ? `${(handlers[0]?.elementType) || 'Element'} • View pending draft.` : this._formatHandlerHeader(primaryHandler); return { header, buttons: this._getHoverButtonConfigs(element, handlers, uuid, bar) }; } _startCommentMode(bar, element, handlers, uuid) { const batchManager = SFE.BatchEditManager || null; const hasActiveEditor = !!SFE.Context?.activeEditor; if ( batchManager && typeof batchManager.isSessionActive === 'function' && batchManager.isSessionActive() && hasActiveEditor && typeof batchManager.startInlineCommenting === 'function' ) { batchManager.startInlineCommenting(bar, element, handlers, uuid); return; } SFE.startCommenting(bar, element, handlers, uuid); } /** * Internal: Show action bar for multiple overlapping elements * * @param {HTMLElement[]} elements Overlap-group elements displayed as rows. * @param {HTMLElement} anchorElement Element currently under the pointer. * @returns {HTMLElement} The multi-element action bar. */ _showMultipleActionBar(elements, anchorElement) { // Clear any pending timeouts immediately if (this.hoverTimeout) { clearTimeout(this.hoverTimeout); this.hoverTimeout = null; } const bar = this.getOrCreate(); // Cancel docking animations / handle dock redirect - parallel to // _showActionBar so multi-element hovers transition from dock correctly. const dock = SFE.ActionBarDock; dock?.cancelAnimation?.(); if (dock?.shouldHandleShow?.()) { dock.handleShow(bar, this, () => this._continueShowMultipleActionBar(bar, elements, anchorElement)); return bar; } return this._continueShowMultipleActionBar(bar, elements, anchorElement); } /** * Continue showing the multi-element action bar. * Called directly (normal path) or as an onDone callback from the dock's * undock animation (dock -> multi-element transition). * * @param {HTMLElement} bar Action-bar DOM element to render. * @param {HTMLElement[]} elements Overlap-group elements displayed as rows. * @param {HTMLElement} anchorElement Element currently under the pointer. * @returns {HTMLElement} The rendered multi-element action bar. */ _continueShowMultipleActionBar(bar, elements, anchorElement) { if (bar._closingTimeout) { clearTimeout(bar._closingTimeout); bar._closingTimeout = null; } // Stop any closing animation and force visibility bar.classList.remove('mwp-sfe-closing'); bar.style.display = 'flex'; void bar.offsetWidth; bar.style.opacity = '1'; bar.style.pointerEvents = 'auto'; // Position-transition policy - same suppress-flag check as // _continueShowActionBar so dock->multi-element transitions don't // CSS-animate across the viewport between coordinate systems. if (!bar._suppressPositionTransition) { bar.classList.remove('mwp-sfe-no-position-transition'); this._markTransitioning(bar); } else { bar.classList.add('mwp-sfe-no-position-transition'); } // Store elements and set up multi-element state. The overlap group controls // which rows appear; the directly hovered element controls both the initial // focused row and the fixed position of the action bar. const positionElement = elements.includes(anchorElement) ? anchorElement : elements[0]; const initialFocusIndex = elements.indexOf(positionElement); this.activeElement = positionElement; bar._targetElement = positionElement; bar._multiElements = elements; bar._currentFocusIndex = initialFocusIndex; // Clear old content and state bar.innerHTML = ''; Array.from(bar.classList) .filter(className => className.startsWith('mwp-sfe-state-')) .forEach(className => bar.classList.remove(className)); bar.classList.add('mwp-sfe-state-hover-multi'); // Create wrapper const wrapper = document.createElement('div'); wrapper.className = 'mwp-sfe-multi-element-wrapper'; // Build multi-element interface const container = document.createElement('div'); container.className = 'mwp-sfe-multi-element-container'; elements.forEach((element, index) => { const uuid = element._mwpSfeUuid || element.dataset.mwpSfeUuid; const handlers = element._mwpSfeHandlers || this.getApplicableHandlers(element, uuid); // Create row for this element const row = document.createElement('div'); row.className = 'mwp-sfe-multi-element-row'; row.setAttribute('data-element-index', index); if (index === initialFocusIndex) row.classList.add('mwp-sfe-focused'); const isPendingRow = element.classList.contains('mwp-sfe-status-pending'); const primaryHandler = isPendingRow ? null : (handlers.find(h => h.capability === 'edit') || handlers[0]); // Header: element type + context-aware description. // For pending-draft rows the header uses draft-specific wording and // does not change on button hover (single button, purpose is clear). const header = document.createElement('div'); header.className = 'mwp-sfe-multi-element-header'; const defaultHeaderText = isPendingRow ? `${(handlers[0]?.elementType) || 'Element'} • View pending draft.` : this._formatHandlerHeader(primaryHandler); header.textContent = defaultHeaderText; // Buttons row - built from configs to avoid duplicating button-creation // logic and to eliminate the fragile text-matching used in the old approach. const buttonsRow = document.createElement('div'); buttonsRow.className = 'mwp-sfe-multi-element-buttons'; this._getHoverButtonConfigs(element, handlers, uuid, bar).forEach(cfg => { const btn = document.createElement('button'); btn.className = cfg.className; btn.textContent = cfg.text; if (cfg.onClick) btn.onclick = cfg.onClick; if (cfg.hoverHeader) { btn.onmouseenter = () => { header.textContent = cfg.hoverHeader; }; btn.onmouseleave = () => { header.textContent = defaultHeaderText; }; } buttonsRow.appendChild(btn); }); row.appendChild(header); row.appendChild(buttonsRow); // Hover row to update focus row.onmousemove = () => { // Only trigger if we aren't already focusing this row // to avoid redundant heavy calls to OverlayManager if (bar._currentFocusIndex !== index) { this._focusMultiElement(bar, index); } }; // Click row to activate primary action row.onclick = (e) => { if (e.target.tagName === 'BUTTON') return; // Let button handle its own click const primaryBtn = buttonsRow.querySelector('.mwp-sfe-btn-primary-inline'); if (primaryBtn) primaryBtn.click(); }; container.appendChild(row); }); // Instructions const instructions = document.createElement('div'); instructions.className = 'mwp-sfe-multi-element-instructions'; instructions.textContent = 'hover/tab to cycle • click/enter to activate'; wrapper.appendChild(container); wrapper.appendChild(instructions); bar.appendChild(wrapper); // Setup keyboard navigation this._setupMultiElementKeyboard(bar); // Position bar this.positionFloatingElements(positionElement, null, bar, true); // Setup scroll listener this._attachScrollListener(bar, positionElement); // Auto-focus the bar so Tab works immediately setTimeout(() => { if (bar && bar.classList.contains('mwp-sfe-state-hover-multi')) { bar.focus({ preventScroll: true }); } }, 50); return bar; } /** * Focus a specific element in multi-element mode */ _focusMultiElement(bar, index) { if (!bar._multiElements) return; const elements = bar._multiElements; if (index < 0 || index >= elements.length) return; bar._currentFocusIndex = index; // Update row visual states const rows = bar.querySelectorAll('.mwp-sfe-multi-element-row'); rows.forEach((row, i) => { row.classList.toggle('mwp-sfe-focused', i === index); }); // Update hover overlay to show focused element const focusedElement = elements[index]; if (SFE.OverlayManager) { SFE.OverlayManager.showHover(focusedElement); } // Update hover tracker to reflect current focused element if (SFE.hoverTracker) { SFE.hoverTracker.lastHoveredElements = [focusedElement]; } } /** * Setup keyboard navigation for multi-element mode */ _setupMultiElementKeyboard(bar) { // Remove old keyboard handler if exists if (bar._keyboardHandler) { bar.removeEventListener('keydown', bar._keyboardHandler); } const keyboardHandler = (e) => { if (!bar._multiElements) return; if (e.key === 'Tab') { e.preventDefault(); // Cycle through elements const newIndex = e.shiftKey ? (bar._currentFocusIndex - 1 + bar._multiElements.length) % bar._multiElements.length : (bar._currentFocusIndex + 1) % bar._multiElements.length; this._focusMultiElement(bar, newIndex); } else if (e.key === 'Enter') { e.preventDefault(); // Trigger primary button of focused element const rows = bar.querySelectorAll('.mwp-sfe-multi-element-row'); const focusedRow = rows[bar._currentFocusIndex]; if (focusedRow) { const primaryBtn = focusedRow.querySelector('.mwp-sfe-btn-primary-inline'); if (primaryBtn) primaryBtn.click(); } } else if (e.key === 'Escape') { this.hide(); } }; bar._keyboardHandler = keyboardHandler; bar.addEventListener('keydown', keyboardHandler); } /** * Internal: Hide action bar with animation */ _hideActionBar() { if (this.hoverTimeout) clearTimeout(this.hoverTimeout); const isHoverState = this.activeBar && ( this.activeBar.classList.contains('mwp-sfe-state-hover') || this.activeBar.classList.contains('mwp-sfe-state-hover-multi') ); if (isHoverState) { this.hoverTimeout = setTimeout(() => { // Don't hide if element is now being edited/previewed if (this.activeElement && this.activeElement.classList.contains('mwp-sfe-element-active')) { return; } // Check if mouse is currently over the bar or any element const pointerPosition = SFE.hoverTracker?.currentMousePos; const isHoveringTransferCorridor = !!( pointerPosition && this.isPointerInHoverTransferCorridor(pointerPosition.x, pointerPosition.y) ); const isHoveringBar = this.activeBar.matches(':hover') || isHoveringTransferCorridor; let isHoveringElement = false; if (this.activeBar._multiElements) { // Check all elements in multi-element mode isHoveringElement = this.activeBar._multiElements.some(el => el.matches(':hover')); } else if (this.activeElement) { isHoveringElement = this.activeElement.matches(':hover'); } if (!isHoveringBar && !isHoveringElement) { // Clean up multi-element state regardless of where the bar goes next. if (this.activeBar._multiElements) { delete this.activeBar._multiElements; delete this.activeBar._currentFocusIndex; } // Batch-edit dock: transition bar to dock instead of hiding it. const dock = SFE.ActionBarDock; if (dock?.shouldHandleHide?.(this.activeBar)) { dock.handleHide(this.activeBar, this); return; } // Normal close path (free version or dock not active). this.activeBar.classList.add('mwp-sfe-closing'); const closingTimeout = setTimeout(() => { if (this.activeBar.classList.contains('mwp-sfe-closing')) { this.activeBar.style.display = 'none'; this.activeBar.classList.remove('mwp-sfe-closing'); this.activeElement = null; // Clear hover tracker state if (SFE.hoverTracker) { SFE.hoverTracker.currentGroupId = null; SFE.hoverTracker.bottommostElement = null; SFE.hoverTracker.lastHoveredElements = []; } // Clean up scroll listener this._removeScrollListener(this.activeBar); } }, this.TIMING.TRANSITION_DURATION); this.activeBar._closingTimeout = closingTimeout; } }, this.TIMING.HOVER_DELAY); } } /** * Internal: Update action bar content based on state * * @param {Object} config - Configuration object * - bar: The action bar element * - element: The target element * - state: State name for CSS class ('hover', 'edit', 'comment', etc.) * - content: Content config object (required): * - buttons: Array of { text, className, onClick, disabled, loading, storeAs, hoverHeader } * - customElement: A pre-built DOM element to insert directly * - customBuilder: A function (bar, element) => DOM element * - header: Optional header text; when provided, buttons are wrapped in a * mwp-sfe-single-element-row with a mwp-sfe-single-element-header */ _updateActionBarState(config) { const { bar, element, state, content = null } = config; // Cancel any in-progress dock animation and clear dock state so that // PositionManager can reposition the bar freely (e.g. when the user // clicks an element while the bar is docked or mid-animation). // Force=true cancels undocking animations too (not just docking), // because a click-to-edit is a definitive action that must win. const _dock = SFE.ActionBarDock; _dock?.cancelAnimation?.(true); if (bar?._isDocked) { bar._isDocked = false; bar.classList.remove('mwp-sfe-state-dock'); bar.style.position = 'absolute'; bar.style.left = ''; bar.style.right = ''; // Suppress position transition so the bar doesn't fly from dock // coordinates to the element if clicked directly while fully docked. bar._suppressPositionTransition = true; requestAnimationFrame(() => { delete bar._suppressPositionTransition; if (bar) bar.classList.remove('mwp-sfe-no-position-transition'); }); // Retrigger slide-up to animate into the edit position cleanly bar.style.animation = 'none'; void bar.offsetWidth; bar.style.animation = ''; } if (!bar) return; // Cancel any in-flight close animation before rendering new state. // Without this, a stale hoverTimeout or closingTimeout from a prior session // can fire after updateState() and set display:none on the bar mid-edit. if (this.hoverTimeout) { clearTimeout(this.hoverTimeout); this.hoverTimeout = null; } if (bar._closingTimeout) { clearTimeout(bar._closingTimeout); bar._closingTimeout = null; } bar.classList.remove('mwp-sfe-closing'); bar.style.display = 'flex'; // Clear old content bar.innerHTML = ''; // Remove all state classes then add the new one Array.from(bar.classList) .filter(className => className.startsWith('mwp-sfe-state-')) .forEach(className => bar.classList.remove(className)); bar.classList.add('mwp-sfe-state-' + state); // Clear old stored button references delete bar._saveBtn; delete bar._cancelBtn; // Render content if (content) { if (content.customElement) { // Direct DOM element insertion bar.appendChild(content.customElement); } else if (content.customBuilder) { // Builder function const built = content.customBuilder(bar, element); if (built) bar.appendChild(built); } else if (content.buttons) { // Button array -- optionally wrapped in a header row. // When content.header is provided the buttons are placed in a // mwp-sfe-single-element-row and each btnConfig.hoverHeader // (if set) updates the header text on mouseenter. let headerEl = null; let buttonTarget = bar; if (content.header !== undefined) { const rowWrapper = document.createElement('div'); rowWrapper.className = 'mwp-sfe-single-element-row'; headerEl = document.createElement('div'); headerEl.className = 'mwp-sfe-single-element-header'; headerEl.textContent = content.header; rowWrapper.appendChild(headerEl); const buttonRow = document.createElement('div'); buttonRow.className = 'mwp-sfe-single-element-buttons'; rowWrapper.appendChild(buttonRow); bar.appendChild(rowWrapper); buttonTarget = buttonRow; } content.buttons.forEach(btnConfig => { const btn = document.createElement('button'); btn.className = btnConfig.className || 'mwp-sfe-btn'; btn.textContent = btnConfig.text || ''; if (btnConfig.disabled) btn.disabled = true; if (btnConfig.loading) btn.setAttribute('mwp-sfe-btn-loading', 'true'); if (btnConfig.onClick) btn.onclick = btnConfig.onClick; // Update header on hover when a per-button description is provided if (headerEl && btnConfig.hoverHeader) { btn.addEventListener('mouseenter', () => { headerEl.textContent = btnConfig.hoverHeader; }); btn.addEventListener('mouseleave', () => { headerEl.textContent = content.header; }); } // Store reference on the bar if requested (e.g. storeAs: 'saveBtn' → bar._saveBtn) if (btnConfig.storeAs) { bar[`_${btnConfig.storeAs}`] = btn; } buttonTarget.appendChild(btn); }); } } // Reposition after content change. // Apply the same suppress-flag check as _continueShowActionBar so an // edit-state update during a dock->element transition also avoids a // cross-viewport position animation. if (!bar._suppressPositionTransition) { bar.classList.remove('mwp-sfe-no-position-transition'); this._markTransitioning(bar); } else { bar.classList.add('mwp-sfe-no-position-transition'); } this.positionFloatingElements(element, null, bar, true); // Setup scroll listener for active states (edit, comment, preview, etc.) // Hover state handles this in _showActionBar instead if (state !== 'hover') { this._attachScrollListener(bar, element); } } /** * Internal: Reset action bar after editor close. * * Checks cursor position synchronously - attachActionBarToElement was called * synchronously in EditorLifecycle.resetActionBar so data-mwp-sfe-bound is * already stamped and elementsFromPoint reliably finds editable elements. * * - Mouse over editable(s): dispatch synthetic mousemove so HoverManager * rebuilds the correct state (single or multi) through its normal pipeline. * innerHTML is intentionally NOT cleared here - _showActionBar / * _showMultipleActionBar clear it as part of their own rebuild. * - Mouse elsewhere: play close animation immediately, no delay. */ _resetActionBar(bar, element, uuid, modeCleanupCallback) { if (modeCleanupCallback) modeCleanupCallback(); this.ElementState.markInactive(element); this.ElementState.enableAllElements(); if (!bar) return; this._removeScrollListener(bar); if (bar._closingTimeout) { clearTimeout(bar._closingTimeout); delete bar._closingTimeout; } delete bar._saveBtn; delete bar._cancelBtn; if (bar._multiElements) { delete bar._multiElements; delete bar._currentFocusIndex; } // Flush group cache so HoverManager does full re-detection instead // of early-exiting with "same group, no change" if (SFE.hoverTracker) { SFE.hoverTracker.currentGroupId = null; SFE.hoverTracker.bottommostElement = null; SFE.hoverTracker.isProcessing = false; } // Make bar non-interactive so elementsFromPoint sees through it to // the editable elements below. _showActionBar restores 'auto'. bar.style.pointerEvents = 'none'; const pos = SFE.hoverTracker?.currentMousePos; if (pos?.x !== undefined) { const elementsAtPoint = document.elementsFromPoint(pos.x, pos.y); const topEditable = elementsAtPoint.find( el => el.dataset.mwpSfeBound === '1' ); if (topEditable) { // Delegate entirely to HoverManager - it will call _showActionBar // or _showMultipleActionBar which restore pointerEvents, clear // old content, and rebuild the correct hover state topEditable.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: pos.x, clientY: pos.y })); return; } } // Mouse is not over any editable element bar.style.pointerEvents = ''; this.activeElement = null; if (SFE.hoverTracker) { SFE.hoverTracker.lastHoveredElements = []; SFE.hoverTracker.currentGroupId = null; SFE.hoverTracker.bottommostElement = null; } // In an active batch session, dock the bar instead of closing it. const dockReset = SFE.ActionBarDock; if (dockReset?.shouldHandleReset?.(bar)) { dockReset.handleReset(bar, this); return; } // Normal close path (free version or dock not active). Array.from(bar.classList) .filter(c => c.startsWith('mwp-sfe-state-')) .forEach(c => bar.classList.remove(c)); bar.classList.remove('mwp-sfe-no-position-transition'); bar.classList.add('mwp-sfe-closing'); const ct = setTimeout(() => { if (bar.classList.contains('mwp-sfe-closing')) { bar.style.display = 'none'; bar.classList.remove('mwp-sfe-closing'); } }, this.TIMING.TRANSITION_DURATION); bar._closingTimeout = ct; } /** * Get or create the global action bar instance */ getOrCreate() { // Check if bar was removed from DOM if (!this.activeBar || !document.body.contains(this.activeBar)) { this.activeBar = document.createElement('div'); this.activeBar.className = 'mwp-sfe-inline-actions'; this.activeBar.setAttribute('data-mwp-sfe-control', 'true'); this.activeBar.setAttribute('tabindex', '-1'); this.activeBar.style.outline = 'none'; document.body.appendChild(this.activeBar); this.activeBar.addEventListener('mouseenter', () => { if (this.hoverTimeout) clearTimeout(this.hoverTimeout); }); this.activeBar.addEventListener('mouseleave', () => { const isHoverState = this.activeBar.classList.contains('mwp-sfe-state-hover') || this.activeBar.classList.contains('mwp-sfe-state-hover-multi'); if (isHoverState) { this.hide(); } }); } return this.activeBar; } /** * Mark the bar as mid-transition so positionFloatingElements won't suppress * position transitions for the duration of the CSS transition. */ _markTransitioning(bar) { bar._isStateTransitioning = true; clearTimeout(bar._transitioningTimeout); bar._transitioningTimeout = setTimeout(() => { bar._isStateTransitioning = false; delete bar._transitioningTimeout; }, 250); // slightly longer than CSS transition (200ms) for safety } /** * Attach scroll listener to update bar position */ _attachScrollListener(bar, element) { // Clean up any existing listener first this._removeScrollListener(bar); const scrollHandler = () => { this.schedulePosition(element, null, bar); }; bar._scrollHandler = scrollHandler; bar._scrollElement = element; // Delay attaching scroll listener to allow initial transition to complete. // This prevents the scroll handler from firing during the hover transition animation. bar._scrollListenerTimeout = setTimeout(() => { // Catch any scrolling that happened during the transition delay scrollHandler(); window.addEventListener('scroll', scrollHandler, true); delete bar._scrollListenerTimeout; }, 250); } /** * Remove scroll listener from bar */ _removeScrollListener(bar) { if (bar && bar._scrollListenerTimeout) { clearTimeout(bar._scrollListenerTimeout); delete bar._scrollListenerTimeout; } if (bar && bar._scrollHandler) { window.removeEventListener('scroll', bar._scrollHandler, true); delete bar._scrollHandler; delete bar._scrollElement; } } } // Export for use in main file SFE.ActionBar = ActionBar; })(); {"translation-revision-date":"2023-10-17 14:44:58+0000","generator":"WP-CLI\/2.12.0","source":"src\/Assist\/tasks\/upload-site-icon.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fi","plural-forms":"nplurals=2; plural=n != 1;"},"Replace":["Korvaa"],"Upload":["Lataa palvelimelle"],"Upload a site icon":["Lataa sivuston kuvake"],"Ensure your website is on-brand by adding your site icon.":["Varmista, ett\u00e4 sivustosi vastaa br\u00e4ndi\u00e4si lis\u00e4\u00e4m\u00e4ll\u00e4 sivuston kuvake."]}}}/** * @file Contains all dynamic functionality needed on post and term pages. * * @output wp-admin/js/post.js */ /* global ajaxurl, wpAjax, postboxes, pagenow, tinymce, alert, deleteUserSetting, ClipboardJS */ /* global theList:true, theExtraList:true, getUserSetting, setUserSetting, commentReply, commentsBox */ /* global WPSetThumbnailHTML, wptitlehint */ // Backward compatibility: prevent fatal errors. window.makeSlugeditClickable = window.editPermalink = function(){}; // Make sure the wp object exists. window.wp = window.wp || {}; ( function( $ ) { var titleHasFocus = false, __ = wp.i18n.__; /** * Control loading of comments on the post and term edit pages. * * @type {{st: number, get: commentsBox.get, load: commentsBox.load}} * * @namespace commentsBox */ window.commentsBox = { // Comment offset to use when fetching new comments. st : 0, /** * Fetch comments using Ajax and display them in the box. * * @memberof commentsBox * * @param {number} total Total number of comments for this post. * @param {number} num Optional. Number of comments to fetch, defaults to 20. * @return {boolean} Always returns false. */ get : function(total, num) { var st = this.st, data; if ( ! num ) num = 20; this.st += num; this.total = total; $( '#commentsdiv .spinner' ).addClass( 'is-active' ); data = { 'action' : 'get-comments', 'mode' : 'single', '_ajax_nonce' : $('#add_comment_nonce').val(), 'p' : $('#post_ID').val(), 'start' : st, 'number' : num }; $.post( ajaxurl, data, function(r) { r = wpAjax.parseAjaxResponse(r); $('#commentsdiv .widefat').show(); $( '#commentsdiv .spinner' ).removeClass( 'is-active' ); if ( 'object' == typeof r && r.responses[0] ) { $('#the-comment-list').append( r.responses[0].data ); theList = theExtraList = null; $( 'a[className*=\':\']' ).off(); // If the offset is over the total number of comments we cannot fetch any more, so hide the button. if ( commentsBox.st > commentsBox.total ) $('#show-comments').hide(); else $('#show-comments').show().children('a').text( __( 'Show more comments' ) ); return; } else if ( 1 == r ) { $('#show-comments').text( __( 'No more comments found.' ) ); return; } $('#the-comment-list').append(''+wpAjax.broken+''); } ); return false; }, /** * Load the next batch of comments. * * @memberof commentsBox * * @param {number} total Total number of comments to load. */ load: function(total){ this.st = jQuery('#the-comment-list tr.comment:visible').length; this.get(total); } }; /** * Overwrite the content of the Featured Image postbox * * @param {string} html New HTML to be displayed in the content area of the postbox. * * @global */ window.WPSetThumbnailHTML = function(html){ $('.inside', '#postimagediv').html(html); }; /** * Set the Image ID of the Featured Image * * @param {number} id The post_id of the image to use as Featured Image. * * @global */ window.WPSetThumbnailID = function(id){ var field = $('input[value="_thumbnail_id"]', '#list-table'); if ( field.length > 0 ) { $('#meta\\[' + field.attr('id').match(/[0-9]+/) + '\\]\\[value\\]').text(id); } }; /** * Remove the Featured Image * * @param {string} nonce Nonce to use in the request. * * @global */ window.WPRemoveThumbnail = function(nonce){ $.post( ajaxurl, { action: 'set-post-thumbnail', post_id: $( '#post_ID' ).val(), thumbnail_id: -1, _ajax_nonce: nonce, cookie: encodeURIComponent( document.cookie ) }, /** * Handle server response * * @param {string} str Response, will be '0' when an error occurred otherwise contains link to add Featured Image. */ function(str){ if ( str == '0' ) { alert( __( 'Could not set that as the thumbnail image. Try a different attachment.' ) ); } else { WPSetThumbnailHTML(str); } } ); }; /** * Heartbeat locks. * * Used to lock editing of an object by only one user at a time. * * When the user does not send a heartbeat in a heartbeat-time * the user is no longer editing and another user can start editing. */ $(document).on( 'heartbeat-send.refresh-lock', function( e, data ) { var lock = $('#active_post_lock').val(), post_id = $('#post_ID').val(), send = {}; if ( ! post_id || ! $('#post-lock-dialog').length ) return; send.post_id = post_id; if ( lock ) send.lock = lock; data['wp-refresh-post-lock'] = send; }).on( 'heartbeat-tick.refresh-lock', function( e, data ) { // Post locks: update the lock string or show the dialog if somebody has taken over editing. var received, wrap, avatar; if ( data['wp-refresh-post-lock'] ) { received = data['wp-refresh-post-lock']; if ( received.lock_error ) { // Show "editing taken over" message. wrap = $('#post-lock-dialog'); if ( wrap.length && ! wrap.is(':visible') ) { if ( wp.autosave ) { // Save the latest changes and disable. $(document).one( 'heartbeat-tick', function() { wp.autosave.server.suspend(); wrap.removeClass('saving').addClass('saved'); $(window).off( 'beforeunload.edit-post' ); }); wrap.addClass('saving'); wp.autosave.server.triggerSave(); } if ( received.lock_error.avatar_src ) { avatar = $( '', { 'class': 'avatar avatar-64 photo', width: 64, height: 64, alt: '', src: received.lock_error.avatar_src, srcset: received.lock_error.avatar_src_2x ? received.lock_error.avatar_src_2x + ' 2x' : undefined } ); wrap.find('div.post-locked-avatar').empty().append( avatar ); } wrap.show().find('.currently-editing').text( received.lock_error.text ); wrap.find('.wp-tab-first').trigger( 'focus' ); } } else if ( received.new_lock ) { $('#active_post_lock').val( received.new_lock ); } } }).on( 'before-autosave.update-post-slug', function() { titleHasFocus = document.activeElement && document.activeElement.id === 'title'; }).on( 'after-autosave.update-post-slug', function() { /* * Create slug area only if not already there * and the title field was not focused (user was not typing a title) when autosave ran. */ if ( ! $('#edit-slug-box > *').length && ! titleHasFocus ) { $.post( ajaxurl, { action: 'sample-permalink', post_id: $('#post_ID').val(), new_title: $('#title').val(), samplepermalinknonce: $('#samplepermalinknonce').val() }, function( data ) { if ( data != '-1' ) { $('#edit-slug-box').html(data); } } ); } }); }(jQuery)); /** * Heartbeat refresh nonces. */ (function($) { var check, timeout; /** * Only allow to check for nonce refresh every 30 seconds. */ function schedule() { check = false; window.clearTimeout( timeout ); timeout = window.setTimeout( function(){ check = true; }, 300000 ); } $( function() { schedule(); }).on( 'heartbeat-send.wp-refresh-nonces', function( e, data ) { var post_id, $authCheck = $('#wp-auth-check-wrap'); if ( check || ( $authCheck.length && ! $authCheck.hasClass( 'hidden' ) ) ) { if ( ( post_id = $('#post_ID').val() ) && $('#_wpnonce').val() ) { data['wp-refresh-post-nonces'] = { post_id: post_id }; } } }).on( 'heartbeat-tick.wp-refresh-nonces', function( e, data ) { var nonces = data['wp-refresh-post-nonces']; if ( nonces ) { schedule(); if ( nonces.replace ) { $.each( nonces.replace, function( selector, value ) { $( '#' + selector ).val( value ); }); } if ( nonces.heartbeatNonce ) window.heartbeatSettings.nonce = nonces.heartbeatNonce; } }); }(jQuery)); /** * All post and postbox controls and functionality. */ jQuery( function($) { var stamp, visibility, $submitButtons, updateVisibility, updateText, $textarea = $('#content'), $document = $(document), postId = $('#post_ID').val() || 0, $submitpost = $('#submitpost'), releaseLock = true, $postVisibilitySelect = $('#post-visibility-select'), $timestampdiv = $('#timestampdiv'), $postStatusSelect = $('#post-status-select'), isMac = window.navigator.platform ? window.navigator.platform.indexOf( 'Mac' ) !== -1 : false, copyAttachmentURLClipboard = new ClipboardJS( '.copy-attachment-url.edit-media' ), copyAttachmentURLSuccessTimeout, __ = wp.i18n.__, _x = wp.i18n._x; postboxes.add_postbox_toggles(pagenow); /* * Clear the window name. Otherwise if this is a former preview window where the user navigated to edit another post, * and the first post is still being edited, clicking Preview there will use this window to show the preview. */ window.name = ''; // Post locks: contain focus inside the dialog. If the dialog is shown, focus the first item. $('#post-lock-dialog .notification-dialog').on( 'keydown', function(e) { // Don't do anything when [Tab] is pressed. if ( e.which != 9 ) return; var target = $(e.target); // [Shift] + [Tab] on first tab cycles back to last tab. if ( target.hasClass('wp-tab-first') && e.shiftKey ) { $(this).find('.wp-tab-last').trigger( 'focus' ); e.preventDefault(); // [Tab] on last tab cycles back to first tab. } else if ( target.hasClass('wp-tab-last') && ! e.shiftKey ) { $(this).find('.wp-tab-first').trigger( 'focus' ); e.preventDefault(); } }).filter(':visible').find('.wp-tab-first').trigger( 'focus' ); // Set the heartbeat interval to 10 seconds if post lock dialogs are enabled. if ( wp.heartbeat && $('#post-lock-dialog').length ) { wp.heartbeat.interval( 10 ); } // The form is being submitted by the user. $submitButtons = $submitpost.find( ':submit, a.submitdelete, #post-preview' ).on( 'click.edit-post', function( event ) { var $button = $(this); if ( $button.hasClass('disabled') ) { event.preventDefault(); return; } if ( $button.hasClass('submitdelete') || $button.is( '#post-preview' ) ) { return; } // The form submission can be blocked from JS or by using HTML 5.0 validation on some fields. // Run this only on an actual 'submit'. $('form#post').off( 'submit.edit-post' ).on( 'submit.edit-post', function( event ) { if ( event.isDefaultPrevented() ) { return; } // Stop auto save. if ( wp.autosave ) { wp.autosave.server.suspend(); } if ( typeof commentReply !== 'undefined' ) { /* * Warn the user they have an unsaved comment before submitting * the post data for update. */ if ( ! commentReply.discardCommentChanges() ) { return false; } /* * Close the comment edit/reply form if open to stop the form * action from interfering with the post's form action. */ commentReply.close(); } releaseLock = false; $(window).off( 'beforeunload.edit-post' ); $submitButtons.addClass( 'disabled' ); if ( $button.attr('id') === 'publish' ) { $submitpost.find( '#major-publishing-actions .spinner' ).addClass( 'is-active' ); } else { $submitpost.find( '#minor-publishing .spinner' ).addClass( 'is-active' ); } }); }); // Submit the form saving a draft or an autosave, and show a preview in a new tab. $('#post-preview').on( 'click.post-preview', function( event ) { var $this = $(this), $form = $('form#post'), $previewField = $('input#wp-preview'), target = $this.attr('target') || 'wp-preview', ua = navigator.userAgent.toLowerCase(); event.preventDefault(); if ( $this.hasClass('disabled') ) { return; } if ( wp.autosave ) { wp.autosave.server.tempBlockSave(); } $previewField.val('dopreview'); $form.attr( 'target', target ).trigger( 'submit' ).attr( 'target', '' ); // Workaround for WebKit bug preventing a form submitting twice to the same action. // https://bugs.webkit.org/show_bug.cgi?id=28633 if ( ua.indexOf('safari') !== -1 && ua.indexOf('chrome') === -1 ) { $form.attr( 'action', function( index, value ) { return value + '?t=' + ( new Date() ).getTime(); }); } $previewField.val(''); }); // Auto save new posts after a title is typed. if ( $( '#auto_draft' ).val() ) { $( '#title' ).on( 'blur', function() { var cancel; if ( ! this.value || $('#edit-slug-box > *').length ) { return; } // Cancel the auto save when the blur was triggered by the user submitting the form. $('form#post').one( 'submit', function() { cancel = true; }); window.setTimeout( function() { if ( ! cancel && wp.autosave ) { wp.autosave.server.triggerSave(); } }, 200 ); }); } $document.on( 'autosave-disable-buttons.edit-post', function() { $submitButtons.addClass( 'disabled' ); }).on( 'autosave-enable-buttons.edit-post', function() { if ( ! wp.heartbeat || ! wp.heartbeat.hasConnectionError() ) { $submitButtons.removeClass( 'disabled' ); } }).on( 'before-autosave.edit-post', function() { $( '.autosave-message' ).text( __( 'Saving Draft…' ) ); }).on( 'after-autosave.edit-post', function( event, data ) { $( '.autosave-message' ).text( data.message ); if ( $( document.body ).hasClass( 'post-new-php' ) ) { $( '.submitbox .submitdelete' ).show(); } }); /* * When the user is trying to load another page, or reloads current page * show a confirmation dialog when there are unsaved changes. */ $( window ).on( 'beforeunload.edit-post', function( event ) { var editor = window.tinymce && window.tinymce.get( 'content' ); var changed = false; if ( wp.autosave ) { changed = wp.autosave.server.postChanged(); } else if ( editor ) { changed = ( ! editor.isHidden() && editor.isDirty() ); } if ( changed ) { event.preventDefault(); // The return string is needed for browser compat. // See https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event. return __( 'The changes you made will be lost if you navigate away from this page.' ); } }).on( 'pagehide.edit-post', function( event ) { if ( ! releaseLock ) { return; } /* * Unload is triggered (by hand) on removing the Thickbox iframe. * Make sure we process only the main document unload. */ if ( event.target && event.target.nodeName != '#document' ) { return; } var postID = $('#post_ID').val(); var postLock = $('#active_post_lock').val(); if ( ! postID || ! postLock ) { return; } var data = { action: 'wp-remove-post-lock', _wpnonce: $('#_wpnonce').val(), post_ID: postID, active_post_lock: postLock }; if ( window.FormData && window.navigator.sendBeacon ) { var formData = new window.FormData(); $.each( data, function( key, value ) { formData.append( key, value ); }); if ( window.navigator.sendBeacon( ajaxurl, formData ) ) { return; } } // Fall back to a synchronous POST request. // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon $.post({ async: false, data: data, url: ajaxurl }); }); // Multiple taxonomies. if ( $('#tagsdiv-post_tag').length ) { window.tagBox && window.tagBox.init(); } else { $('.meta-box-sortables').children('div.postbox').each(function(){ if ( this.id.indexOf('tagsdiv-') === 0 ) { window.tagBox && window.tagBox.init(); return false; } }); } // Handle categories. $('.categorydiv').each( function(){ var this_id = $(this).attr('id'), catAddBefore, catAddAfter, taxonomyParts, taxonomy, settingName; taxonomyParts = this_id.split('-'); taxonomyParts.shift(); taxonomy = taxonomyParts.join('-'); settingName = taxonomy + '_tab'; if ( taxonomy == 'category' ) { settingName = 'cats'; } // @todo Move to jQuery 1.3+, support for multiple hierarchical taxonomies, see wp-lists.js. $('a', '#' + taxonomy + '-tabs').on( 'click', function( e ) { e.preventDefault(); var t = $(this).attr('href'); $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); $('#' + taxonomy + '-tabs').siblings('.tabs-panel').hide(); $(t).show(); if ( '#' + taxonomy + '-all' == t ) { deleteUserSetting( settingName ); } else { setUserSetting( settingName, 'pop' ); } }); if ( getUserSetting( settingName ) ) $('a[href="#' + taxonomy + '-pop"]', '#' + taxonomy + '-tabs').trigger( 'click' ); // Add category button controls. $('#new' + taxonomy).one( 'focus', function() { $( this ).val( '' ).removeClass( 'form-input-tip' ); }); // On [Enter] submit the taxonomy. $('#new' + taxonomy).on( 'keypress', function(event){ if( 13 === event.keyCode ) { event.preventDefault(); $('#' + taxonomy + '-add-submit').trigger( 'click' ); } }); // After submitting a new taxonomy, re-focus the input field. $('#' + taxonomy + '-add-submit').on( 'click', function() { $('#new' + taxonomy).trigger( 'focus' ); }); /** * Before adding a new taxonomy, disable submit button. * * @param {Object} s Taxonomy object which will be added. * * @return {Object} */ catAddBefore = function( s ) { if ( !$('#new'+taxonomy).val() ) { return false; } s.data += '&' + $( ':checked', '#'+taxonomy+'checklist' ).serialize(); $( '#' + taxonomy + '-add-submit' ).prop( 'disabled', true ); return s; }; /** * Re-enable submit button after a taxonomy has been added. * * Re-enable submit button. * If the taxonomy has a parent place the taxonomy underneath the parent. * * @param {Object} r Response. * @param {Object} s Taxonomy data. * * @return {void} */ catAddAfter = function( r, s ) { var sup, drop = $('#new'+taxonomy+'_parent'); $( '#' + taxonomy + '-add-submit' ).prop( 'disabled', false ); if ( 'undefined' != s.parsed.responses[0] && (sup = s.parsed.responses[0].supplemental.newcat_parent) ) { drop.before(sup); drop.remove(); } }; $('#' + taxonomy + 'checklist').wpList({ alt: '', response: taxonomy + '-ajax-response', addBefore: catAddBefore, addAfter: catAddAfter }); // Add new taxonomy button toggles input form visibility. $('#' + taxonomy + '-add-toggle').on( 'click', function( e ) { e.preventDefault(); $('#' + taxonomy + '-adder').toggleClass( 'wp-hidden-children' ); $('a[href="#' + taxonomy + '-all"]', '#' + taxonomy + '-tabs').trigger( 'click' ); $('#new'+taxonomy).trigger( 'focus' ); }); // Sync checked items between "All {taxonomy}" and "Most used" lists. $('#' + taxonomy + 'checklist, #' + taxonomy + 'checklist-pop').on( 'click', 'li.popular-category > label input[type="checkbox"]', function() { var t = $(this), c = t.is(':checked'), id = t.val(); if ( id && t.parents('#taxonomy-'+taxonomy).length ) { // Fixed for ticket #62504. See https://core.trac.wordpress.org/ticket/62504. $('input#in-' + taxonomy + '-' + id + ', input[id^="in-' + taxonomy + '-' + id + '-"]').prop('checked', c); $('input#in-popular-' + taxonomy + '-' + id).prop('checked', c); } } ); }); // End cats. // Custom Fields postbox. if ( $('#postcustom').length ) { $( '#the-list' ).wpList( { /** * Add current post_ID to request to fetch custom fields * * @ignore * * @param {Object} s Request object. * * @return {Object} Data modified with post_ID attached. */ addBefore: function( s ) { s.data += '&post_id=' + $('#post_ID').val(); return s; }, /** * Show the listing of custom fields after fetching. * * @ignore */ addAfter: function() { $('table#list-table').show(); } }); } /* * Publish Post box (#submitdiv) */ if ( $('#submitdiv').length ) { stamp = $('#timestamp').html(); visibility = $('#post-visibility-display').html(); /** * When the visibility of a post changes sub-options should be shown or hidden. * * @ignore * * @return {void} */ updateVisibility = function() { // Show sticky for public posts. if ( $postVisibilitySelect.find('input:radio:checked').val() != 'public' ) { $('#sticky').prop('checked', false); $('#sticky-span').hide(); } else { $('#sticky-span').show(); } // Show password input field for password protected post. if ( $postVisibilitySelect.find('input:radio:checked').val() != 'password' ) { $('#password-span').hide(); } else { $('#password-span').show(); } }; /** * Make sure all labels represent the current settings. * * @ignore * * @return {boolean} False when an invalid timestamp has been selected, otherwise True. */ updateText = function() { if ( ! $timestampdiv.length ) return true; var attemptedDate, originalDate, currentDate, publishOn, postStatus = $('#post_status'), optPublish = $('option[value="publish"]', postStatus), aa = $('#aa').val(), mm = $('#mm').val(), jj = $('#jj').val(), hh = $('#hh').val(), mn = $('#mn').val(); attemptedDate = new Date( aa, mm - 1, jj, hh, mn ); originalDate = new Date( $('#hidden_aa').val(), $('#hidden_mm').val() -1, $('#hidden_jj').val(), $('#hidden_hh').val(), $('#hidden_mn').val() ); currentDate = new Date( $('#cur_aa').val(), $('#cur_mm').val() -1, $('#cur_jj').val(), $('#cur_hh').val(), $('#cur_mn').val() ); // Catch unexpected date problems. if ( attemptedDate.getFullYear() != aa || (1 + attemptedDate.getMonth()) != mm || attemptedDate.getDate() != jj || attemptedDate.getMinutes() != mn ) { $timestampdiv.find('.timestamp-wrap').addClass('form-invalid'); return false; } else { $timestampdiv.find('.timestamp-wrap').removeClass('form-invalid'); } // Determine what the publish should be depending on the date and post status. if ( attemptedDate > currentDate ) { publishOn = __( 'Schedule for:' ); $('#publish').val( _x( 'Schedule', 'post action/button label' ) ); } else if ( attemptedDate <= currentDate && $('#original_post_status').val() != 'publish' ) { publishOn = __( 'Publish on:' ); $('#publish').val( __( 'Publish' ) ); } else { publishOn = __( 'Published on:' ); $('#publish').val( __( 'Update' ) ); } // If the date is the same, set it to trigger update events. if ( originalDate.toUTCString() == attemptedDate.toUTCString() ) { // Re-set to the current value. $('#timestamp').html(stamp); } else { $('#timestamp').html( '\n' + publishOn + ' ' + // translators: 1: Month, 2: Day, 3: Year, 4: Hour, 5: Minute. __( '%1$s %2$s, %3$s at %4$s:%5$s' ) .replace( '%1$s', $( 'option[value="' + mm + '"]', '#mm' ).attr( 'data-text' ) ) .replace( '%2$s', parseInt( jj, 10 ) ) .replace( '%3$s', aa ) .replace( '%4$s', ( '00' + hh ).slice( -2 ) ) .replace( '%5$s', ( '00' + mn ).slice( -2 ) ) + ' ' ); } // Add "privately published" to post status when applies. if ( $postVisibilitySelect.find('input:radio:checked').val() == 'private' ) { $('#publish').val( __( 'Update' ) ); if ( 0 === optPublish.length ) { postStatus.append(''); } else { optPublish.html( __( 'Privately Published' ) ); } $('option[value="publish"]', postStatus).prop('selected', true); $('#misc-publishing-actions .edit-post-status').hide(); } else { if ( $('#original_post_status').val() == 'future' || $('#original_post_status').val() == 'draft' ) { if ( optPublish.length ) { optPublish.remove(); postStatus.val($('#hidden_post_status').val()); } } else { optPublish.html( __( 'Published' ) ); } if ( postStatus.is(':hidden') ) $('#misc-publishing-actions .edit-post-status').show(); } // Update "Status:" to currently selected status. $('#post-status-display').text( // Remove any potential tags from post status text. wp.sanitize.stripTagsAndEncodeText( $('option:selected', postStatus).text() ) ); // Show or hide the "Save Draft" button. if ( $('option:selected', postStatus).val() == 'private' || $('option:selected', postStatus).val() == 'publish' ) { $('#save-post').hide(); } else { $('#save-post').show(); if ( $('option:selected', postStatus).val() == 'pending' ) { $('#save-post').show().val( __( 'Save as Pending' ) ); } else { $('#save-post').show().val( __( 'Save Draft' ) ); } } return true; }; // Show the visibility options and hide the toggle button when opened. $( '#visibility .edit-visibility').on( 'click', function( e ) { e.preventDefault(); if ( $postVisibilitySelect.is(':hidden') ) { updateVisibility(); $postVisibilitySelect.slideDown( 'fast', function() { $postVisibilitySelect.find( 'input[type="radio"]' ).first().trigger( 'focus' ); } ); $(this).hide(); } }); // Cancel visibility selection area and hide it from view. $postVisibilitySelect.find('.cancel-post-visibility').on( 'click', function( event ) { $postVisibilitySelect.slideUp('fast'); $('#visibility-radio-' + $('#hidden-post-visibility').val()).prop('checked', true); $('#post_password').val($('#hidden-post-password').val()); $('#sticky').prop('checked', $('#hidden-post-sticky').prop('checked')); $('#post-visibility-display').html(visibility); $('#visibility .edit-visibility').show().trigger( 'focus' ); updateText(); event.preventDefault(); }); // Set the selected visibility as current. $postVisibilitySelect.find('.save-post-visibility').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. var visibilityLabel = '', selectedVisibility = $postVisibilitySelect.find('input:radio:checked').val(); $postVisibilitySelect.slideUp('fast'); $('#visibility .edit-visibility').show().trigger( 'focus' ); updateText(); if ( 'public' !== selectedVisibility ) { $('#sticky').prop('checked', false); } switch ( selectedVisibility ) { case 'public': visibilityLabel = $( '#sticky' ).prop( 'checked' ) ? __( 'Public, Sticky' ) : __( 'Public' ); break; case 'private': visibilityLabel = __( 'Private' ); break; case 'password': visibilityLabel = __( 'Password Protected' ); break; } $('#post-visibility-display').text( visibilityLabel ); event.preventDefault(); }); // When the selection changes, update labels. $postVisibilitySelect.find('input:radio').on( 'change', function() { updateVisibility(); }); // Edit publish time click. $timestampdiv.siblings('a.edit-timestamp').on( 'click', function( event ) { if ( $timestampdiv.is( ':hidden' ) ) { $timestampdiv.slideDown( 'fast', function() { $( 'input, select', $timestampdiv.find( '.timestamp-wrap' ) ).first().trigger( 'focus' ); } ); $(this).hide(); } event.preventDefault(); }); // Cancel editing the publish time and hide the settings. $timestampdiv.find('.cancel-timestamp').on( 'click', function( event ) { $timestampdiv.slideUp('fast').siblings('a.edit-timestamp').show().trigger( 'focus' ); $('#mm').val($('#hidden_mm').val()); $('#jj').val($('#hidden_jj').val()); $('#aa').val($('#hidden_aa').val()); $('#hh').val($('#hidden_hh').val()); $('#mn').val($('#hidden_mn').val()); updateText(); event.preventDefault(); }); // Save the changed timestamp. $timestampdiv.find('.save-timestamp').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. if ( updateText() ) { $timestampdiv.slideUp('fast'); $timestampdiv.siblings('a.edit-timestamp').show().trigger( 'focus' ); } event.preventDefault(); }); // Cancel submit when an invalid timestamp has been selected. $('#post').on( 'submit', function( event ) { if ( ! updateText() ) { event.preventDefault(); $timestampdiv.show(); if ( wp.autosave ) { wp.autosave.enableButtons(); } $( '#publishing-action .spinner' ).removeClass( 'is-active' ); } }); // Post Status edit click. $postStatusSelect.siblings('a.edit-post-status').on( 'click', function( event ) { if ( $postStatusSelect.is( ':hidden' ) ) { $postStatusSelect.slideDown( 'fast', function() { $postStatusSelect.find('select').trigger( 'focus' ); } ); $(this).hide(); } event.preventDefault(); }); // Save the Post Status changes and hide the options. $postStatusSelect.find('.save-post-status').on( 'click', function( event ) { $postStatusSelect.slideUp( 'fast' ).siblings( 'a.edit-post-status' ).show().trigger( 'focus' ); updateText(); event.preventDefault(); }); // Cancel Post Status editing and hide the options. $postStatusSelect.find('.cancel-post-status').on( 'click', function( event ) { $postStatusSelect.slideUp( 'fast' ).siblings( 'a.edit-post-status' ).show().trigger( 'focus' ); $('#post_status').val( $('#hidden_post_status').val() ); updateText(); event.preventDefault(); }); } /** * Handle the editing of the post_name. Create the required HTML elements and * update the changes via Ajax. * * @global * * @return {void} */ function editPermalink() { var i, slug_value, slug_label, $el, revert_e, c = 0, real_slug = $('#post_name'), revert_slug = real_slug.val(), permalink = $( '#sample-permalink' ), permalinkOrig = permalink.html(), permalinkInner = $( '#sample-permalink a' ).html(), buttons = $('#edit-slug-buttons'), buttonsOrig = buttons.html(), full = $('#editable-post-name-full'); // Deal with Twemoji in the post-name. full.find( 'img' ).replaceWith( function() { return this.alt; } ); full = full.html(); permalink.html( permalinkInner ); // Save current content to revert to when cancelling. $el = $( '#editable-post-name' ); revert_e = $el.html(); buttons.html( ' ' + '' ); // Save permalink changes. buttons.children( '.save' ).on( 'click', function() { var new_slug = $el.children( 'input' ).val(); if ( new_slug == $('#editable-post-name-full').text() ) { buttons.children('.cancel').trigger( 'click' ); return; } $.post( ajaxurl, { action: 'sample-permalink', post_id: postId, new_slug: new_slug, new_title: $('#title').val(), samplepermalinknonce: $('#samplepermalinknonce').val() }, function(data) { var box = $('#edit-slug-box'); box.html(data); if (box.hasClass('hidden')) { box.fadeIn('fast', function () { box.removeClass('hidden'); }); } buttons.html(buttonsOrig); permalink.html(permalinkOrig); real_slug.val(new_slug); $( '.edit-slug' ).trigger( 'focus' ); wp.a11y.speak( __( 'Permalink saved' ) ); } ); }); // Cancel editing of permalink. buttons.children( '.cancel' ).on( 'click', function() { $('#view-post-btn').show(); $el.html(revert_e); buttons.html(buttonsOrig); permalink.html(permalinkOrig); real_slug.val(revert_slug); $( '.edit-slug' ).trigger( 'focus' ); }); // If more than 1/4th of 'full' is '%', make it empty. for ( i = 0; i < full.length; ++i ) { if ( '%' == full.charAt(i) ) c++; } slug_value = ( c > full.length / 4 ) ? '' : full; slug_label = __( 'URL Slug' ); $el.html( '' + '' ).children( 'input' ).on( 'keydown', function( e ) { var key = e.which; // On [Enter], just save the new slug, don't save the post. if ( 13 === key ) { e.preventDefault(); buttons.children( '.save' ).trigger( 'click' ); } // On [Esc] cancel the editing. if ( 27 === key ) { buttons.children( '.cancel' ).trigger( 'click' ); } } ).on( 'keyup', function() { real_slug.val( this.value ); }).trigger( 'focus' ); } $( '#titlediv' ).on( 'click', '.edit-slug', function() { editPermalink(); }); /** * Adds screen reader text to the title label when needed. * * Use the 'screen-reader-text' class to emulate a placeholder attribute * and hide the label when entering a value. * * @param {string} id Optional. HTML ID to add the screen reader helper text to. * * @global * * @return {void} */ window.wptitlehint = function( id ) { id = id || 'title'; var title = $( '#' + id ), titleprompt = $( '#' + id + '-prompt-text' ); if ( '' === title.val() ) { titleprompt.removeClass( 'screen-reader-text' ); } title.on( 'input', function() { if ( '' === this.value ) { titleprompt.removeClass( 'screen-reader-text' ); return; } titleprompt.addClass( 'screen-reader-text' ); } ); }; wptitlehint(); // Resize the WYSIWYG and plain text editors. ( function() { var editor, offset, mce, $handle = $('#post-status-info'), $postdivrich = $('#postdivrich'); // If there are no textareas or we are on a touch device, we can't do anything. if ( ! $textarea.length || 'ontouchstart' in window ) { // Hide the resize handle. $('#content-resize-handle').hide(); return; } /** * Handle drag event. * * @param {Object} event Event containing details about the drag. */ function dragging( event ) { if ( $postdivrich.hasClass( 'wp-editor-expand' ) ) { return; } if ( mce ) { editor.theme.resizeTo( null, offset + event.pageY ); } else { $textarea.height( Math.max( 50, offset + event.pageY ) ); } event.preventDefault(); } /** * When the dragging stopped make sure we return focus and do a confidence check on the height. */ function endDrag() { var height, toolbarHeight; if ( $postdivrich.hasClass( 'wp-editor-expand' ) ) { return; } if ( mce ) { editor.focus(); toolbarHeight = parseInt( $( '#wp-content-editor-container .mce-toolbar-grp' ).height(), 10 ); if ( toolbarHeight < 10 || toolbarHeight > 200 ) { toolbarHeight = 30; } height = parseInt( $('#content_ifr').css('height'), 10 ) + toolbarHeight - 28; } else { $textarea.trigger( 'focus' ); height = parseInt( $textarea.css('height'), 10 ); } $document.off( '.wp-editor-resize' ); // Confidence check: normalize height to stay within acceptable ranges. if ( height && height > 50 && height < 5000 ) { setUserSetting( 'ed_size', height ); } } $handle.on( 'mousedown.wp-editor-resize', function( event ) { if ( typeof tinymce !== 'undefined' ) { editor = tinymce.get('content'); } if ( editor && ! editor.isHidden() ) { mce = true; offset = $('#content_ifr').height() - event.pageY; } else { mce = false; offset = $textarea.height() - event.pageY; $textarea.trigger( 'blur' ); } $document.on( 'mousemove.wp-editor-resize', dragging ) .on( 'mouseup.wp-editor-resize mouseleave.wp-editor-resize', endDrag ); event.preventDefault(); }).on( 'mouseup.wp-editor-resize', endDrag ); })(); // TinyMCE specific handling of Post Format changes to reflect in the editor. if ( typeof tinymce !== 'undefined' ) { // When changing post formats, change the editor body class. $( '#post-formats-select input.post-format' ).on( 'change.set-editor-class', function() { var editor, body, format = this.id; if ( format && $( this ).prop( 'checked' ) && ( editor = tinymce.get( 'content' ) ) ) { body = editor.getBody(); body.className = body.className.replace( /\bpost-format-[^ ]+/, '' ); editor.dom.addClass( body, format == 'post-format-0' ? 'post-format-standard' : format ); $( document ).trigger( 'editor-classchange' ); } }); // When changing page template, change the editor body class. $( '#page_template' ).on( 'change.set-editor-class', function() { var editor, body, pageTemplate = $( this ).val() || ''; pageTemplate = pageTemplate.substr( pageTemplate.lastIndexOf( '/' ) + 1, pageTemplate.length ) .replace( /\.php$/, '' ) .replace( /\./g, '-' ); if ( pageTemplate && ( editor = tinymce.get( 'content' ) ) ) { body = editor.getBody(); body.className = body.className.replace( /\bpage-template-[^ ]+/, '' ); editor.dom.addClass( body, 'page-template-' + pageTemplate ); $( document ).trigger( 'editor-classchange' ); } }); } // Save on pressing [Ctrl]/[Command] + [S] in the Text editor. $textarea.on( 'keydown.wp-autosave', function( event ) { // Key [S] has code 83. if ( event.which === 83 ) { if ( event.shiftKey || event.altKey || ( isMac && ( ! event.metaKey || event.ctrlKey ) ) || ( ! isMac && ! event.ctrlKey ) ) { return; } wp.autosave && wp.autosave.server.triggerSave(); event.preventDefault(); } }); // If the last status was auto-draft and the save is triggered, edit the current URL. if ( $( '#original_post_status' ).val() === 'auto-draft' && window.history.replaceState ) { var location; $( '#publish' ).on( 'click', function() { location = window.location.href; location += ( location.indexOf( '?' ) !== -1 ) ? '&' : '?'; location += 'wp-post-new-reload=true'; window.history.replaceState( null, null, location ); }); } /** * Copies the attachment URL in the Edit Media page to the clipboard. * * @since 5.5.0 * * @param {MouseEvent} event A click event. * * @return {void} */ copyAttachmentURLClipboard.on( 'success', function( event ) { var triggerElement = $( event.trigger ), successElement = $( '.success', triggerElement.closest( '.copy-to-clipboard-container' ) ); // Clear the selection and move focus back to the trigger. event.clearSelection(); // Show success visual feedback. clearTimeout( copyAttachmentURLSuccessTimeout ); successElement.removeClass( 'hidden' ); // Hide success visual feedback after 3 seconds since last success. copyAttachmentURLSuccessTimeout = setTimeout( function() { successElement.addClass( 'hidden' ); }, 3000 ); // Handle success audible feedback. wp.a11y.speak( __( 'The file URL has been copied to your clipboard' ) ); } ); } ); /** * TinyMCE word count display */ ( function( $, counter ) { $( function() { var $content = $( '#content' ), $count = $( '#wp-word-count' ).find( '.word-count' ), prevCount = 0, contentEditor; /** * Get the word count from TinyMCE and display it */ function update() { var text, count; if ( ! contentEditor || contentEditor.isHidden() ) { text = $content.val(); } else { text = contentEditor.getContent( { format: 'raw' } ); } count = counter.count( text ); if ( count !== prevCount ) { $count.text( count ); } prevCount = count; } /** * Bind the word count update triggers. * * When a node change in the main TinyMCE editor has been triggered. * When a key has been released in the plain text content editor. */ $( document ).on( 'tinymce-editor-init', function( event, editor ) { if ( editor.id !== 'content' ) { return; } contentEditor = editor; editor.on( 'nodechange keyup', _.debounce( update, 1000 ) ); } ); $content.on( 'input keyup', _.debounce( update, 1000 ) ); update(); } ); } )( jQuery, new wp.utils.WordCounter() ); About – ISUHEN

WHO WE ARE

We are a diversified Mongolian company operating across import, restaurant, and agriculture sectors.By integrating these industries, we deliver consistent quality—from sourcing products to serving food.

our story

What started as a trading business has grown into a connected ecosystem.
We expanded into Japanese ramen restaurants to bring authentic dining experiences to Mongolia, and developed our own greenhouse to ensure fresh, reliable ingredients.

Today, our businesses work together—creating efficiency, quality, and sustainability at every step.

Global sourcing

Importing carefully selected products from China with reliable logistics.

 

Food & Hospitality

Operating multiple ramen restaurant locations focused on quality and consistency.

Sustainable Farming

Growing fresh vegetables in our greenhouse to support our kitchens year-round.