/**
* 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 }
);
}
// Pages the canvas cannot round-trip are announced after it starts, so
// the layout stays visible for reference, but saving is disabled before
// the user can edit: the document was already reduced when it was
// parsed into the canvas, and saving that reduction is the data loss.
showBlockedNotice();
return true;
}
/**
* Disable saving for a page the canvas cannot represent faithfully.
*
* GrapesJS builds body fragments. A complete document, or a page carrying
* scripts, loses whatever the canvas cannot hold the moment it is parsed —
* so the warning belongs at load, not at save, and the only honest action
* is to send the user to the editor that can save the page intact.
*/
function showBlockedNotice() {
const reason = metasyncEditor.blocked_reason;
if (!reason) {
return;
}
const $panel = $('#metasync-editor-load-failure');
const message = reason === 'lps'
? t('blocked_lps', 'This page was imported from Website Studio, which owns its content.')
: t('blocked_full_document', 'This page is a complete HTML document, so saving here would discard its doctype, head and scripts.');
$('.metasync-save-button')
.prop('disabled', true)
.attr('title', t('blocked_save_disabled', 'Saving is disabled to protect this page from being rewritten'));
if (!$panel.length) {
return;
}
$panel.find('.metasync-load-failure-title')
.text(t('blocked_title', 'This page cannot be saved from the visual editor'));
$panel.find('.metasync-load-failure-message').text(message);
$panel.find('.metasync-load-failure-detail').hide();
// Non-fatal styling: the canvas stays visible and usable for reading,
// so the notice sits above it rather than covering it.
$panel.removeClass('metasync-load-failure-fatal');
// Reload cannot help here — the block is a property of the stored
// page — so that button becomes the route to the lossless editor.
const $primary = $panel.find('.metasync-load-failure-reload').off('click');
if (metasyncEditor.direct_edit_url) {
$primary
.text(t('open_direct_editor', 'Edit HTML Directly'))
.on('click', function(e) {
e.preventDefault();
window.location.href = metasyncEditor.direct_edit_url;
})
.show();
} else {
$primary.hide();
}
$panel.find('.metasync-load-failure-dismiss')
.text(t('dismiss', 'Dismiss'))
.off('click')
.on('click', function(e) {
e.preventDefault();
$panel.attr('hidden', 'hidden');
})
.show();
$panel.removeAttr('hidden');
updateStatus('error');
}
/**
* 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