/** * MetaSync HTML Visual Editor * * Initializes GrapesJS editor with custom configuration * for editing raw HTML pages with full visual capabilities. * * @global grapesjs - Loaded from GrapesJS library script * @global metasyncEditor - Injected via wp_localize_script */ /* eslint-env browser */ /* global grapesjs, metasyncEditor */ (function($) { 'use strict'; let editor; let hasUnsavedChanges = false; $(document).ready(function() { // Bind the header controls first so Back and Preview keep working even // if the canvas never starts. initializeEventHandlers(); if (!startEditor()) { return; } preventAccidentalExit(); }); /** * Start the editor, reporting any dependency or start-up failure to the * user instead of leaving an unexplained empty canvas behind. * * @return {boolean} True when the canvas initialized. */ function startEditor() { // Dependencies absent from disk (reported by PHP) plus any whose // request failed in the browser (blocked by CSP, an extension, etc). const missing = (metasyncEditor.missing || []).slice(); const failed = (window.metasyncEditorAssets && window.metasyncEditorAssets.failed) || []; failed.forEach(function(name) { if (missing.indexOf(name) === -1) { missing.push(name); } }); // The editor script is ordered after GrapesJS but WordPress cannot // guarantee the file actually arrived, so verify the global exists. if (typeof grapesjs === 'undefined') { if (missing.indexOf('grapesjs') === -1) { missing.push('grapesjs'); } showLoadFailure( metasyncEditor.i18n.load_failed_core, missing, { fatal: true } ); return false; } try { initializeEditor(); } catch (err) { console.error('MetaSync HTML Editor failed to initialize:', err); showLoadFailure( metasyncEditor.i18n.load_failed_init, missing, { fatal: true } ); return false; } // The blocks library is optional: without it the canvas still loads and // the page stays editable, only the extra block palette is missing. if (!resolveBlocksPlugin()) { if (missing.indexOf('grapesjs-blocks-basic') === -1) { missing.push('grapesjs-blocks-basic'); } showLoadFailure( metasyncEditor.i18n.load_failed_blocks, missing, { fatal: false } ); } return true; } /** * Resolve the grapesjs-blocks-basic plugin. * * Releases before 1.0.0 registered themselves under the name * 'gjs-blocks-basic'; 1.0.x only exposes a UMD export, so the plugin is * looked up by reference and the legacy registry name is only a fallback. * * @return {Function|undefined} The plugin function when available. */ function resolveBlocksPlugin() { const exported = window['gjs-blocks-basic']; const plugin = exported && exported.default ? exported.default : exported; if (typeof plugin === 'function') { return plugin; } if (grapesjs.plugins && typeof grapesjs.plugins.get === 'function') { return grapesjs.plugins.get('gjs-blocks-basic'); } return undefined; } /** * Report an asset-loading or start-up failure in the page. * * Only dependency names and the localized message are shown; no paths, * versions or server detail are exposed. * * @param {string} message Localized explanation. * @param {Array} missing Names of the dependencies that did not load. * @param {Object} options Set fatal to true when the canvas is unusable. */ function showLoadFailure(message, missing, options) { const settings = options || {}; const $panel = $('#metasync-editor-load-failure'); console.error('MetaSync HTML Editor: ' + message + (missing.length ? ' (' + missing.join(', ') + ')' : '')); if (!$panel.length) { return; } $panel.find('.metasync-load-failure-title').text(metasyncEditor.i18n.load_failed_title); $panel.find('.metasync-load-failure-message').text(message); const $detail = $panel.find('.metasync-load-failure-detail'); if (missing.length) { $detail.text( (metasyncEditor.i18n.load_failed_detail || '%s').replace('%s', missing.join(', ')) ).show(); } else { $detail.hide(); } $panel.find('.metasync-load-failure-reload') .text(metasyncEditor.i18n.reload) .off('click') .on('click', function(e) { e.preventDefault(); window.location.reload(); }); $panel.toggleClass('metasync-load-failure-fatal', !!settings.fatal); // Clear `hidden` rather than calling .show(), which would set an inline // display and break the panel's flex centering. $panel.removeAttr('hidden'); if (settings.fatal) { $('.metasync-save-button') .prop('disabled', true) .attr('title', metasyncEditor.i18n.save_disabled); } else { // Non-fatal: let the notice be dismissed and keep editing. $panel.find('.metasync-load-failure-dismiss') .text(metasyncEditor.i18n.dismiss) .off('click') .on('click', function(e) { e.preventDefault(); $panel.hide(); }) .show(); } updateStatus('error'); } /** * Sidebar panels, in switcher order. * * Each entry maps a switcher button to the sidebar panel it reveals, the * command that activates it, and the localized-label key for its title. * * @type {Array} */ const SIDEBAR_PANELS = [ { id: 'styles', command: 'show-styles', icon: 'dashicons-art', labelKey: 'panel_styles', fallback: 'Styles' }, { id: 'traits', command: 'show-traits', icon: 'dashicons-admin-generic', labelKey: 'panel_settings', fallback: 'Settings' }, { id: 'layers', command: 'show-layers', icon: 'dashicons-menu', labelKey: 'panel_layers', fallback: 'Layers' }, { id: 'blocks', command: 'show-blocks', icon: 'dashicons-grid-view', labelKey: 'panel_blocks', fallback: 'Blocks' } ]; /** * Resolve a localized string with an English fallback. * * @param {string} key Property under metasyncEditor.i18n. * @param {string} fallback Used when the payload predates the key. * @return {string} */ function t(key, fallback) { return (metasyncEditor.i18n && metasyncEditor.i18n[key]) || fallback; } /** * Show a transient action notice in the page's live region. * * alert() blocks the UI thread, cannot be styled or dismissed by * keyboard, and is unreachable to screen-reader users mid-flow; the * notice element is announced automatically instead. * * @param {string} message Localized message to display. */ let noticeTimer = null; function showNotice(message) { const $notice = $('#metasync-editor-notice'); if (!$notice.length) { window.alert(message); return; } $notice.text(message).removeAttr('hidden'); if (noticeTimer) { clearTimeout(noticeTimer); } noticeTimer = setTimeout(function() { $notice.attr('hidden', true); }, 6000); } /** * Render the sidebar's panel switcher buttons. * * The buttons are created in the sidebar itself rather than as GrapesJS * panel buttons, so the editor re-rendering its panels cannot detach them. */ function buildPanelSwitcher() { const $switcher = $('#metasync-editor-panel-switcher'); if (!$switcher.length) { return; } $switcher.empty(); SIDEBAR_PANELS.forEach(function(panel) { const label = t(panel.labelKey, panel.fallback); const $button = $('