PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 All 139 releases
← All changes | admin/js/metasync-html-editor.js +495 -339 2.6.212.7.0 View file →
@@ -12,27 +12,361 @@
12 12
13 13 (function($) {
14 14 'use strict';
15 15
16 - console.log('MetaSync HTML Editor script loaded');
17 - console.log('GrapesJS available:', typeof grapesjs !== 'undefined');
18 - console.log('jQuery available:', typeof $ !== 'undefined');
19 -
20 16 let editor;
21 17 let hasUnsavedChanges = false;
22 18
23 19 $(document).ready(function() {
24 - console.log('Document ready, initializing editor...');
25 - initializeEditor();
20 + // Bind the header controls first so Back and Preview keep working even
21 + // if the canvas never starts.
26 22 initializeEventHandlers();
23 +
24 + if (!startEditor()) {
25 + return;
26 + }
27 +
27 28 preventAccidentalExit();
28 29 });
29 30
30 31 /**
32 + * Start the editor, reporting any dependency or start-up failure to the
33 + * user instead of leaving an unexplained empty canvas behind.
34 + *
35 + * @return {boolean} True when the canvas initialized.
36 + */
37 + function startEditor() {
38 + // Dependencies absent from disk (reported by PHP) plus any whose
39 + // request failed in the browser (blocked by CSP, an extension, etc).
40 + const missing = (metasyncEditor.missing || []).slice();
41 + const failed = (window.metasyncEditorAssets && window.metasyncEditorAssets.failed) || [];
42 +
43 + failed.forEach(function(name) {
44 + if (missing.indexOf(name) === -1) {
45 + missing.push(name);
46 + }
47 + });
48 + // The editor script is ordered after GrapesJS but WordPress cannot
49 + // guarantee the file actually arrived, so verify the global exists.
50 + if (typeof grapesjs === 'undefined') {
51 + if (missing.indexOf('grapesjs') === -1) {
52 + missing.push('grapesjs');
53 + }
54 + showLoadFailure(
55 + metasyncEditor.i18n.load_failed_core,
56 + missing,
57 + { fatal: true }
58 + );
59 + return false;
60 + }
61 +
62 + try {
63 + initializeEditor();
64 + } catch (err) {
65 + console.error('MetaSync HTML Editor failed to initialize:', err);
66 + showLoadFailure(
67 + metasyncEditor.i18n.load_failed_init,
68 + missing,
69 + { fatal: true }
70 + );
71 + return false;
72 + }
73 +
74 + // The blocks library is optional: without it the canvas still loads and
75 + // the page stays editable, only the extra block palette is missing.
76 + if (!resolveBlocksPlugin()) {
77 + if (missing.indexOf('grapesjs-blocks-basic') === -1) {
78 + missing.push('grapesjs-blocks-basic');
79 + }
80 + showLoadFailure(
81 + metasyncEditor.i18n.load_failed_blocks,
82 + missing,
83 + { fatal: false }
84 + );
85 + }
86 +
87 + // Pages the canvas cannot round-trip are announced after it starts, so
88 + // the layout stays visible for reference, but saving is disabled before
89 + // the user can edit: the document was already reduced when it was
90 + // parsed into the canvas, and saving that reduction is the data loss.
91 + showBlockedNotice();
92 +
93 + return true;
94 + }
95 +
96 + /**
97 + * Disable saving for a page the canvas cannot represent faithfully.
98 + *
99 + * GrapesJS builds body fragments. A complete document, or a page carrying
100 + * scripts, loses whatever the canvas cannot hold the moment it is parsed —
101 + * so the warning belongs at load, not at save, and the only honest action
102 + * is to send the user to the editor that can save the page intact.
103 + */
104 + function showBlockedNotice() {
105 + const reason = metasyncEditor.blocked_reason;
106 +
107 + if (!reason) {
108 + return;
109 + }
110 +
111 + const $panel = $('#metasync-editor-load-failure');
112 +
113 + const message = reason === 'lps'
114 + ? t('blocked_lps', 'This page was imported from Website Studio, which owns its content.')
115 + : t('blocked_full_document', 'This page is a complete HTML document, so saving here would discard its doctype, head and scripts.');
116 +
117 + $('.metasync-save-button')
118 + .prop('disabled', true)
119 + .attr('title', t('blocked_save_disabled', 'Saving is disabled to protect this page from being rewritten'));
120 +
121 + if (!$panel.length) {
122 + return;
123 + }
124 +
125 + $panel.find('.metasync-load-failure-title')
126 + .text(t('blocked_title', 'This page cannot be saved from the visual editor'));
127 + $panel.find('.metasync-load-failure-message').text(message);
128 + $panel.find('.metasync-load-failure-detail').hide();
129 +
130 + // Non-fatal styling: the canvas stays visible and usable for reading,
131 + // so the notice sits above it rather than covering it.
132 + $panel.removeClass('metasync-load-failure-fatal');
133 +
134 + // Reload cannot help here — the block is a property of the stored
135 + // page — so that button becomes the route to the lossless editor.
136 + const $primary = $panel.find('.metasync-load-failure-reload').off('click');
137 +
138 + if (metasyncEditor.direct_edit_url) {
139 + $primary
140 + .text(t('open_direct_editor', 'Edit HTML Directly'))
141 + .on('click', function(e) {
142 + e.preventDefault();
143 + window.location.href = metasyncEditor.direct_edit_url;
144 + })
145 + .show();
146 + } else {
147 + $primary.hide();
148 + }
149 +
150 + $panel.find('.metasync-load-failure-dismiss')
151 + .text(t('dismiss', 'Dismiss'))
152 + .off('click')
153 + .on('click', function(e) {
154 + e.preventDefault();
155 + $panel.attr('hidden', 'hidden');
156 + })
157 + .show();
158 +
159 + $panel.removeAttr('hidden');
160 + updateStatus('error');
161 + }
162 +
163 + /**
164 + * Resolve the grapesjs-blocks-basic plugin.
165 + *
166 + * Releases before 1.0.0 registered themselves under the name
167 + * 'gjs-blocks-basic'; 1.0.x only exposes a UMD export, so the plugin is
168 + * looked up by reference and the legacy registry name is only a fallback.
169 + *
170 + * @return {Function|undefined} The plugin function when available.
171 + */
172 + function resolveBlocksPlugin() {
173 + const exported = window['gjs-blocks-basic'];
174 + const plugin = exported && exported.default ? exported.default : exported;
175 +
176 + if (typeof plugin === 'function') {
177 + return plugin;
178 + }
179 +
180 + if (grapesjs.plugins && typeof grapesjs.plugins.get === 'function') {
181 + return grapesjs.plugins.get('gjs-blocks-basic');
182 + }
183 +
184 + return undefined;
185 + }
186 +
187 + /**
188 + * Report an asset-loading or start-up failure in the page.
189 + *
190 + * Only dependency names and the localized message are shown; no paths,
191 + * versions or server detail are exposed.
192 + *
193 + * @param {string} message Localized explanation.
194 + * @param {Array} missing Names of the dependencies that did not load.
195 + * @param {Object} options Set fatal to true when the canvas is unusable.
196 + */
197 + function showLoadFailure(message, missing, options) {
198 + const settings = options || {};
199 + const $panel = $('#metasync-editor-load-failure');
200 +
201 + console.error('MetaSync HTML Editor: ' + message +
202 + (missing.length ? ' (' + missing.join(', ') + ')' : ''));
203 +
204 + if (!$panel.length) {
205 + return;
206 + }
207 +
208 + $panel.find('.metasync-load-failure-title').text(metasyncEditor.i18n.load_failed_title);
209 + $panel.find('.metasync-load-failure-message').text(message);
210 +
211 + const $detail = $panel.find('.metasync-load-failure-detail');
212 + if (missing.length) {
213 + $detail.text(
214 + (metasyncEditor.i18n.load_failed_detail || '%s').replace('%s', missing.join(', '))
215 + ).show();
216 + } else {
217 + $detail.hide();
218 + }
219 +
220 + $panel.find('.metasync-load-failure-reload')
221 + .text(metasyncEditor.i18n.reload)
222 + .off('click')
223 + .on('click', function(e) {
224 + e.preventDefault();
225 + window.location.reload();
226 + });
227 +
228 + $panel.toggleClass('metasync-load-failure-fatal', !!settings.fatal);
229 + // Clear `hidden` rather than calling .show(), which would set an inline
230 + // display and break the panel's flex centering.
231 + $panel.removeAttr('hidden');
232 +
233 + if (settings.fatal) {
234 + $('.metasync-save-button')
235 + .prop('disabled', true)
236 + .attr('title', metasyncEditor.i18n.save_disabled);
237 + } else {
238 + // Non-fatal: let the notice be dismissed and keep editing.
239 + $panel.find('.metasync-load-failure-dismiss')
240 + .text(metasyncEditor.i18n.dismiss)
241 + .off('click')
242 + .on('click', function(e) {
243 + e.preventDefault();
244 + $panel.hide();
245 + })
246 + .show();
247 + }
248 +
249 + updateStatus('error');
250 + }
251 +
252 + /**
253 + * Sidebar panels, in switcher order.
254 + *
255 + * Each entry maps a switcher button to the sidebar panel it reveals, the
256 + * command that activates it, and the localized-label key for its title.
257 + *
258 + * @type {Array<Object>}
259 + */
260 + const SIDEBAR_PANELS = [
261 + { id: 'styles', command: 'show-styles', icon: 'dashicons-art', labelKey: 'panel_styles', fallback: 'Styles' },
262 + { id: 'traits', command: 'show-traits', icon: 'dashicons-admin-generic', labelKey: 'panel_settings', fallback: 'Settings' },
263 + { id: 'layers', command: 'show-layers', icon: 'dashicons-menu', labelKey: 'panel_layers', fallback: 'Layers' },
264 + { id: 'blocks', command: 'show-blocks', icon: 'dashicons-grid-view', labelKey: 'panel_blocks', fallback: 'Blocks' }
265 + ];
266 +
267 + /**
268 + * Resolve a localized string with an English fallback.
269 + *
270 + * @param {string} key Property under metasyncEditor.i18n.
271 + * @param {string} fallback Used when the payload predates the key.
272 + * @return {string}
273 + */
274 + function t(key, fallback) {
275 + return (metasyncEditor.i18n && metasyncEditor.i18n[key]) || fallback;
276 + }
277 +
278 + /**
279 + * Show a transient action notice in the page's live region.
280 + *
281 + * alert() blocks the UI thread, cannot be styled or dismissed by
282 + * keyboard, and is unreachable to screen-reader users mid-flow; the
283 + * notice element is announced automatically instead.
284 + *
285 + * @param {string} message Localized message to display.
286 + */
287 + let noticeTimer = null;
288 + function showNotice(message) {
289 + const $notice = $('#metasync-editor-notice');
290 +
291 + if (!$notice.length) {
292 + window.alert(message);
293 + return;
294 + }
295 +
296 + $notice.text(message).removeAttr('hidden');
297 +
298 + if (noticeTimer) {
299 + clearTimeout(noticeTimer);
300 + }
301 + noticeTimer = setTimeout(function() {
302 + $notice.attr('hidden', true);
303 + }, 6000);
304 + }
305 +
306 + /**
307 + * Render the sidebar's panel switcher buttons.
308 + *
309 + * The buttons are created in the sidebar itself rather than as GrapesJS
310 + * panel buttons, so the editor re-rendering its panels cannot detach them.
311 + */
312 + function buildPanelSwitcher() {
313 + const $switcher = $('#metasync-editor-panel-switcher');
314 +
315 + if (!$switcher.length) {
316 + return;
317 + }
318 +
319 + $switcher.empty();
320 +
321 + SIDEBAR_PANELS.forEach(function(panel) {
322 + const label = t(panel.labelKey, panel.fallback);
323 + const $button = $('<button/>', {
324 + type: 'button',
325 + 'class': 'metasync-panel-switch',
326 + 'data-metasync-panel-target': panel.id,
327 + 'aria-pressed': 'false',
328 + title: label
329 + });
330 +
331 + $button.append($('<span/>', { 'class': 'dashicons ' + panel.icon, 'aria-hidden': 'true' }));
332 + $button.append($('<span/>', { 'class': 'metasync-panel-switch-label', text: label }));
333 + $button.on('click', function() {
334 + editor.runCommand(panel.command);
335 + });
336 +
337 + $switcher.append($button);
338 + });
339 +
340 + showPanel('styles');
341 + }
342 +
343 + /**
344 + * Reveal one sidebar panel and mark its switcher button active.
345 + *
346 + * @param {string} panelId Identifier from SIDEBAR_PANELS.
347 + */
348 + function showPanel(panelId) {
349 + SIDEBAR_PANELS.forEach(function(panel) {
350 + const isActive = panel.id === panelId;
351 + const target = document.getElementById('metasync-panel-' + panel.id);
352 +
353 + if (target) {
354 + target.hidden = !isActive;
355 + }
356 +
357 + $('.metasync-panel-switch[data-metasync-panel-target="' + panel.id + '"]')
358 + .toggleClass('is-active', isActive)
359 + .attr('aria-pressed', isActive ? 'true' : 'false');
360 + });
361 + }
362 +
363 + /**
31 364 * Initialize GrapesJS editor
32 365 */
33 366 function initializeEditor() {
34 367 const htmlContent = $('#metasync-html-content').val();
368 + const blocksPlugin = resolveBlocksPlugin();
35 369
36 370 editor = grapesjs.init({
37 371 container: '#metasync-gjs-editor',
38 372 fromElement: false,
@@ -39,13 +373,12 @@
39 373 height: 'calc(100vh - 112px)',
40 374 width: 'auto',
41 375 storageManager: false, // Disable built-in storage
42 376
43 - // Plugins
44 - plugins: ['gjs-blocks-basic'],
45 - pluginsOpts: {
46 - 'gjs-blocks-basic': {}
47 - },
377 + // Plugins. Passed by reference so the editor does not depend on the
378 + // library registering itself under a legacy global name.
379 + plugins: blocksPlugin ? [blocksPlugin] : [],
380 + pluginsOpts: {},
48 381
49 382 // Enable double-click to edit text
50 383 allowScripts: 0,
51 384 showOffsets: 1,
@@ -55,8 +388,59 @@
55 388 richTextEditor: {
56 389 actions: ['bold', 'italic', 'underline', 'strikethrough', 'link']
57 390 },
58 391
392 + // Upload images through the plugin's AJAX endpoint. Without this
393 + // the asset manager has no upload URL and its upload button does
394 + // nothing at all — no request, no asset, no error.
395 + assetManager: {
396 + upload: metasyncEditor.ajax_url,
397 + uploadName: 'file',
398 + // Values must stay flat strings: the uploader FormData-appends
399 + // each param verbatim, so a nested object would serialize as
400 + // the literal "[object Object]".
401 + params: {
402 + action: 'metasync_upload_image',
403 + nonce: metasyncEditor.nonce,
404 + post_id: metasyncEditor.post_id
405 + },
406 + multiUpload: false,
407 + // The endpoint can fail two ways: a WordPress envelope that
408 + // reports the error with HTTP 200, and a non-200 response
409 + // (expired nonce answers "-1" with 403, a fatal answers HTML
410 + // with 500). Both must reject here, or the asset manager
411 + // would add the error payload as a broken, imageless asset.
412 + customFetch: function (url, options) {
413 + return fetch(url, options)
414 + .then(function (response) {
415 + if (!response.ok) {
416 + return response.text().then(function (text) {
417 + var body = null;
418 + try {
419 + body = JSON.parse(text);
420 + } catch (error) {
421 + body = null;
422 + }
423 + return Promise.reject(body || { data: { message: 'HTTP ' + response.status } });
424 + });
425 + }
426 + return response.text();
427 + })
428 + .then(function (text) {
429 + var body;
430 + try {
431 + body = JSON.parse(text);
432 + } catch (error) {
433 + return text;
434 + }
435 + if (body && body.success === false) {
436 + return Promise.reject(body);
437 + }
438 + return text;
439 + });
440 + }
441 + },
442 +
59 443 // Canvas settings
60 444 canvas: {
61 445 styles: [],
62 446 scripts: []
@@ -63,8 +447,9 @@
63 447 },
64 448
65 449 // Block Manager
66 450 blockManager: {
451 + appendTo: '#metasync-editor-blocks',
67 452 blocks: [
68 453 {
69 454 id: 'section',
70 455 label: '<div class="gjs-block-label">Section</div>',
@@ -101,8 +486,9 @@
101 486 },
102 487
103 488 // Style Manager
104 489 styleManager: {
490 + appendTo: '#metasync-editor-styles',
105 491 sectors: [
106 492 {
107 493 name: 'Colors',
108 494 open: true,
@@ -358,12 +744,16 @@
358 744 ]
359 745 },
360 746
361 747 // Layer Manager
362 - layerManager: {},
748 + layerManager: {
749 + appendTo: '#metasync-editor-layers'
750 + },
363 751
364 752 // Traits Manager - for editing element properties
365 - traitManager: {},
753 + traitManager: {
754 + appendTo: '#metasync-editor-traits'
755 + },
366 756
367 757 // Panels
368 758 panels: {
369 759 defaults: [
@@ -374,9 +764,9 @@
374 764 {
375 765 id: 'visibility',
376 766 active: true,
377 767 className: 'btn-toggle-borders',
378 - label: '<i class="fa fa-clone"></i>',
768 + label: '<span class="dashicons dashicons-editor-table"></span>',
379 769 command: 'sw-visibility'
380 770 }
381 771 ]
382 772 },
@@ -385,9 +775,9 @@
385 775 el: '.gjs-pn-devices',
386 776 buttons: [
387 777 {
388 778 id: 'device-desktop',
389 - label: '<i class="fa fa-television"></i>',
779 + label: '<span class="dashicons dashicons-desktop"></span>',
390 780 command: 'set-device-desktop',
391 781 active: true,
392 782 togglable: false
393 783 },
@@ -392,15 +782,15 @@
392 782 togglable: false
393 783 },
394 784 {
395 785 id: 'device-tablet',
396 - label: '<i class="fa fa-tablet"></i>',
786 + label: '<span class="dashicons dashicons-tablet"></span>',
397 787 command: 'set-device-tablet',
398 788 togglable: false
399 789 },
400 790 {
401 791 id: 'device-mobile',
402 - label: '<i class="fa fa-mobile"></i>',
792 + label: '<span class="dashicons dashicons-smartphone"></span>',
403 793 command: 'set-device-mobile',
404 794 togglable: false
405 795 }
406 796 ]
@@ -429,136 +819,17 @@
429 819 },
430 820
431 821 // Selector Manager
432 822 selectorManager: {
433 - appendTo: ''
823 + appendTo: '#metasync-editor-selectors'
434 824 }
435 825 });
436 826
437 - // Create and show the right sidebar panel container
438 - const editorEl = editor.getContainer();
439 - let viewsContainer = editorEl.querySelector('.gjs-pn-views-container');
827 + // Each manager was handed its own `appendTo` target above, so GrapesJS
828 + // renders the panels into the sidebar itself. Nothing is re-parented
829 + // here, and no sector is registered a second time: the container is
830 + // emptied on init, so anything appended to it by hand is discarded.
440 831
441 - if (!viewsContainer) {
442 - viewsContainer = document.createElement('div');
443 - viewsContainer.className = 'gjs-pn-views-container';
444 - editorEl.appendChild(viewsContainer);
445 -
446 - const viewsInner = document.createElement('div');
447 - viewsInner.className = 'gjs-pn-views';
448 - viewsContainer.appendChild(viewsInner);
449 - }
450 -
451 - // Add sectors to Style Manager first
452 - const sm = editor.StyleManager;
453 - sm.addSector('colors', {
454 - name: 'Colors',
455 - open: true,
456 - properties: [
457 - {
458 - name: 'Text Color',
459 - property: 'color',
460 - type: 'color'
461 - },
462 - {
463 - name: 'Background Color',
464 - property: 'background-color',
465 - type: 'color'
466 - },
467 - {
468 - name: 'Border Color',
469 - property: 'border-color',
470 - type: 'color'
471 - },
472 - {
473 - name: 'Opacity',
474 - property: 'opacity',
475 - type: 'slider',
476 - defaults: 1,
477 - step: 0.01,
478 - max: 1,
479 - min: 0
480 - }
481 - ]
482 - });
483 -
484 - sm.addSector('typography', {
485 - name: 'Typography',
486 - open: false,
487 - properties: [
488 - 'font-family',
489 - 'font-size',
490 - 'font-weight',
491 - 'letter-spacing',
492 - 'line-height',
493 - 'text-align'
494 - ]
495 - });
496 -
497 - sm.addSector('decorations', {
498 - name: 'Decorations',
499 - open: false,
500 - properties: [
501 - 'border-radius',
502 - 'border',
503 - 'box-shadow'
504 - ]
505 - });
506 -
507 - sm.addSector('dimensions', {
508 - name: 'Dimensions',
509 - open: false,
510 - properties: [
511 - 'width',
512 - 'height',
513 - 'max-width',
514 - 'min-width',
515 - 'padding',
516 - 'margin'
517 - ]
518 - });
519 -
520 - console.log('Style Manager sectors added:', sm.getSectors().length);
521 -
522 - // Render the Style Manager immediately after adding sectors
523 - const smEl = sm.render().el;
524 - console.log('Style Manager rendered, sectors in element:', $(smEl).find('.gjs-sm-sector').length);
525 -
526 - // Append panels to the container
527 - setTimeout(function() {
528 - const $views = $('.gjs-pn-views');
529 - if ($views.length) {
530 - console.log('Views container found, appending panels...');
531 -
532 - // Append already-rendered Style Manager
533 - $(smEl).show().css({'display': 'block', 'visibility': 'visible'});
534 - $views.append(smEl);
535 - console.log('Style Manager appended to sidebar');
536 -
537 - // Append Trait Manager (initially hidden)
538 - const tmEl = editor.TraitManager.render().el;
539 - $(tmEl).hide();
540 - $views.append(tmEl);
541 - console.log('Trait Manager appended');
542 -
543 - // Append Layer Manager (initially hidden)
544 - const lmEl = editor.LayerManager.render().el;
545 - $(lmEl).hide();
546 - $views.append(lmEl);
547 - console.log('Layer Manager appended');
548 -
549 - // Append Block Manager (initially hidden)
550 - const bmEl = editor.BlockManager.render().el;
551 - $(bmEl).hide();
552 - $views.append(bmEl);
553 - console.log('Block Manager appended');
554 -
555 - console.log('All panels appended to sidebar');
556 - } else {
557 - console.error('Views container not found!');
558 - }
559 - }, 300);
560 -
561 832 // Load HTML content
562 833 editor.setComponents(htmlContent);
563 834
564 835 // Extract and load styles
@@ -570,50 +841,8 @@
570 841 });
571 842 editor.setStyle(allStyles);
572 843 }
573 844
574 - // Add panel switcher buttons after editor initializes
575 - const panelManager = editor.Panels;
576 - const viewsPanel = panelManager.addPanel({
577 - id: 'panel-switcher'
578 - });
579 -
580 - viewsPanel.get('buttons').add([
581 - {
582 - id: 'show-style',
583 - active: true,
584 - label: '<i class="fa fa-paint-brush"></i><div class="gjs-pn-label">Styles</div>',
585 - command: 'show-styles',
586 - togglable: false
587 - },
588 - {
589 - id: 'show-traits',
590 - label: '<i class="fa fa-cog"></i><div class="gjs-pn-label">Settings</div>',
591 - command: 'show-traits',
592 - togglable: false
593 - },
594 - {
595 - id: 'show-layers',
596 - label: '<i class="fa fa-bars"></i><div class="gjs-pn-label">Layers</div>',
597 - command: 'show-layers',
598 - togglable: false
599 - },
600 - {
601 - id: 'show-blocks',
602 - label: '<i class="fa fa-th-large"></i><div class="gjs-pn-label">Blocks</div>',
603 - command: 'show-blocks',
604 - togglable: false
605 - }
606 - ]);
607 -
608 - // Move panel to the right sidebar
609 - setTimeout(function() {
610 - const $viewsContainer = $('.gjs-pn-views');
611 - if ($viewsContainer.length) {
612 - $('#panel-switcher').prependTo($viewsContainer);
613 - }
614 - }, 100);
615 -
616 845 // Enhance component types with better traits
617 846 editor.DomComponents.addType('text', {
618 847 model: {
619 848 defaults: {
@@ -704,116 +933,56 @@
704 933 editor.setDevice('Mobile');
705 934 }
706 935 });
707 936
708 - // Add custom commands for panel switching
709 - editor.Commands.add('show-styles', {
710 - run: function(editor) {
711 - const pnl = editor.Panels.getPanel('panel-switcher');
712 - if (pnl) {
713 - pnl.get('buttons').each(function(btn) {
714 - btn.set('active', btn.id === 'show-style');
715 - });
937 + // Panel-switching commands.
938 + //
939 + // Each manager renders into its own sidebar mount point, so switching
940 + // is purely a matter of revealing the right panel. These used to also
941 + // call render() on the manager and toggle GrapesJS panel classes,
942 + // which fought with the editor's own rendering.
943 + SIDEBAR_PANELS.forEach(function(panel) {
944 + editor.Commands.add(panel.command, {
945 + run: function() {
946 + showPanel(panel.id);
716 947 }
717 -
718 - const sm = editor.StyleManager;
719 - const tm = editor.TraitManager;
720 - const lm = editor.LayerManager;
721 - const bm = editor.BlockManager;
722 -
723 - sm.render();
724 - $('.gjs-pn-views .gjs-sm-sectors').show();
725 - $('.gjs-pn-views .gjs-trt-traits').hide();
726 - $('.gjs-pn-views .gjs-layers').hide();
727 - $('.gjs-pn-views .gjs-blocks-c').hide();
728 - }
948 + });
729 949 });
730 950
731 - editor.Commands.add('show-traits', {
732 - run: function(editor) {
733 - const pnl = editor.Panels.getPanel('panel-switcher');
734 - if (pnl) {
735 - pnl.get('buttons').each(function(btn) {
736 - btn.set('active', btn.id === 'show-traits');
737 - });
738 - }
951 + // Build the sidebar's panel switcher, now that the commands its
952 + // buttons run are registered.
953 + //
954 + // These were previously GrapesJS panel buttons that a timer tried to
955 + // re-parent into the sidebar. The buttons are plain markup in the
956 + // sidebar now, so they cannot be detached by the editor re-rendering
957 + // its own panels.
958 + buildPanelSwitcher();
739 959
740 - const tm = editor.TraitManager;
741 - tm.render();
742 - $('.gjs-pn-views .gjs-sm-sectors').hide();
743 - $('.gjs-pn-views .gjs-trt-traits').show();
744 - $('.gjs-pn-views .gjs-layers').hide();
745 - $('.gjs-pn-views .gjs-blocks-c').hide();
746 - }
747 - });
748 -
749 - editor.Commands.add('show-layers', {
750 - run: function(editor) {
751 - const pnl = editor.Panels.getPanel('panel-switcher');
752 - if (pnl) {
753 - pnl.get('buttons').each(function(btn) {
754 - btn.set('active', btn.id === 'show-layers');
755 - });
756 - }
757 -
758 - const lm = editor.LayerManager;
759 - lm.render();
760 - $('.gjs-pn-views .gjs-sm-sectors').hide();
761 - $('.gjs-pn-views .gjs-trt-traits').hide();
762 - $('.gjs-pn-views .gjs-layers').show();
763 - $('.gjs-pn-views .gjs-blocks-c').hide();
764 - }
765 - });
766 -
767 - editor.Commands.add('show-blocks', {
768 - run: function(editor) {
769 - const pnl = editor.Panels.getPanel('panel-switcher');
770 - if (pnl) {
771 - pnl.get('buttons').each(function(btn) {
772 - btn.set('active', btn.id === 'show-blocks');
773 - });
774 - }
775 -
776 - const bm = editor.BlockManager;
777 - bm.render();
778 - $('.gjs-pn-views .gjs-sm-sectors').hide();
779 - $('.gjs-pn-views .gjs-trt-traits').hide();
780 - $('.gjs-pn-views .gjs-layers').hide();
781 - $('.gjs-pn-views .gjs-blocks-c').show();
782 - }
783 - });
784 -
785 - // Add selected element indicator
786 - setTimeout(function() {
787 - const $viewsContainer = $('.gjs-pn-views');
788 - if ($viewsContainer.length) {
789 - $viewsContainer.prepend('<div id="metasync-selected-element" style="display: none; padding: 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; font-size: 13px; font-weight: 600; border-bottom: 1px solid #1a1e23;"><div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px;">Editing Element</div><div id="metasync-element-name"></div></div>');
790 - }
791 - }, 200);
792 -
793 960 // When an element is selected, automatically show the Styles panel
794 961 editor.on('component:selected', function(component) {
795 - // Show selected element indicator
962 + // Name the selected element in the sidebar. The indicator is part
963 + // of the page template, so it does not need to be injected here.
796 964 const elementType = component.get('type') || 'div';
797 965 const elementName = component.getName() || elementType;
798 - $('#metasync-selected-element').show();
966 + const indicator = document.getElementById('metasync-selected-element');
967 +
968 + if (indicator) {
969 + indicator.hidden = false;
970 + }
971 +
799 972 $('#metasync-element-name').text(elementName.charAt(0).toUpperCase() + elementName.slice(1));
800 973
801 974 // Automatically switch to Styles panel when selecting an element
802 975 editor.runCommand('show-styles');
803 -
804 - // Open the Colors section by default
805 - setTimeout(function() {
806 - const $colorsSector = $('.gjs-sm-sector').first();
807 - if ($colorsSector.length && !$colorsSector.hasClass('gjs-sm-open')) {
808 - $colorsSector.find('.gjs-sm-sector-title').click();
809 - }
810 - }, 100);
811 976 });
812 977
813 978 // Hide indicator when no element is selected
814 979 editor.on('component:deselected', function() {
815 - $('#metasync-selected-element').hide();
980 + const indicator = document.getElementById('metasync-selected-element');
981 +
982 + if (indicator) {
983 + indicator.hidden = true;
984 + }
816 985 });
817 986
818 987 // Track changes
819 988 editor.on('change:changesCount', function() {
@@ -820,31 +989,14 @@
820 989 hasUnsavedChanges = true;
821 990 updateStatus('unsaved');
822 991 });
823 992
824 - // Custom image upload
825 - editor.on('asset:upload:start', handleImageUpload);
826 -
827 - // Force render all managers to ensure panels appear
828 - setTimeout(function() {
829 - editor.StyleManager.render();
830 - editor.TraitManager.render();
831 - editor.LayerManager.render();
832 - editor.BlockManager.render();
833 -
834 - // Show the views container
835 - $('.gjs-pn-views-container, .gjs-pn-views').show().css({
836 - 'display': 'block',
837 - 'visibility': 'visible'
838 - });
839 -
840 - // Initialize with styles panel
841 - editor.runCommand('show-styles');
842 -
843 - console.log('Panels rendered and displayed');
844 - }, 500);
845 -
846 - console.log('MetaSync HTML Editor initialized');
993 + // Upload failures are rejected by the assetManager's customFetch and
994 + // surface here. Tell the user instead of failing silently.
995 + editor.on('asset:upload:error', function (error) {
996 + var message = error && error.data && error.data.message;
997 + showNotice(message || t('upload_failed', 'Image upload failed'));
998 + });
847 999 }
848 1000
849 1001 /**
850 1002 * Initialize event handlers
@@ -869,8 +1021,22 @@
869 1021 /**
870 1022 * Save HTML via AJAX
871 1023 */
872 1024 function saveHTML() {
1025 + // The save button is disabled after a fatal load, but the Ctrl/Cmd+S
1026 + // binding still fires — there is no editor to read from then.
1027 + if (!editor) {
1028 + return;
1029 + }
1030 +
1031 + // Same reasoning for a page the canvas cannot round-trip: the button is
1032 + // disabled, but the keyboard shortcut would otherwise still overwrite
1033 + // the stored document with the canvas's reduced copy of it.
1034 + if (metasyncEditor.blocked_reason) {
1035 + showBlockedNotice();
1036 + return;
1037 + }
1038 +
873 1039 const $button = $('.metasync-save-button');
874 1040 const originalText = $button.text();
875 1041
876 1042 // Get HTML and CSS from editor
@@ -883,9 +1049,9 @@
883 1049 fullHTML = `<style>${css}</style>\n${html}`;
884 1050 }
885 1051
886 1052 // Update button state
887 - $button.prop('disabled', true).text(metasyncEditor.i18n.saving);
1053 + $button.prop('disabled', true).text(t('saving', 'Saving...'));
888 1054 updateStatus('saving');
889 1055
890 1056 // Send AJAX request
891 1057 $.ajax({
@@ -900,9 +1066,9 @@
900 1066 success: function(response) {
901 1067 if (response.success) {
902 1068 hasUnsavedChanges = false;
903 1069 updateStatus('saved');
904 - $button.text(metasyncEditor.i18n.saved);
1070 + $button.text(t('saved', 'Saved!'));
905 1071
906 1072 setTimeout(function() {
907 1073 $button.text(originalText);
908 1074 updateStatus('ready');
@@ -907,14 +1073,29 @@
907 1073 $button.text(originalText);
908 1074 updateStatus('ready');
909 1075 }, 2000);
910 1076 } else {
911 - alert(response.data.message || metasyncEditor.i18n.error);
1077 + showNotice((response.data && response.data.message) || t('error', 'Error saving'));
912 1078 updateStatus('error');
913 1079 }
914 1080 },
915 - error: function() {
916 - alert(metasyncEditor.i18n.error);
1081 + error: function(xhr) {
1082 + // An expired nonce answers "-1" (or "0" logged-out) outside
1083 + // the JSON envelope; anything else is a real failure. The
1084 + // session-expired message warns that reloading will lose the
1085 + // canvas, which the generic error message does not.
1086 + var body = xhr && xhr.responseText;
1087 + if (body === '-1' || body === '0') {
1088 + showNotice(t('session_expired', 'Your session has expired. Copy your work before reloading the page.'));
1089 + } else {
1090 + var message = null;
1091 + try {
1092 + message = JSON.parse(body).data.message;
1093 + } catch (parseError) {
1094 + message = null;
1095 + }
1096 + showNotice(message || t('error', 'Error saving'));
1097 + }
917 1098 updateStatus('error');
918 1099 },
919 1100 complete: function() {
920 1101 $button.prop('disabled', false);
@@ -926,9 +1107,9 @@
926 1107 * Open preview in new tab
927 1108 */
928 1109 function openPreview() {
929 1110 if (hasUnsavedChanges) {
930 - if (!confirm('You have unsaved changes. Preview will show the last saved version. Continue?')) {
1111 + if (!window.confirm(t('confirm_preview', 'You have unsaved changes. Preview will show the last saved version. Continue?'))) {
931 1112 return;
932 1113 }
933 1114 }
934 1115 window.open(metasyncEditor.preview_url, '_blank');
@@ -934,36 +1115,8 @@
934 1115 window.open(metasyncEditor.preview_url, '_blank');
935 1116 }
936 1117
937 1118 /**
938 - * Handle image upload
939 - */
940 - function handleImageUpload(e) {
941 - const file = e.target.files[0];
942 - if (!file) return;
943 -
944 - const formData = new FormData();
945 - formData.append('action', 'metasync_upload_image');
946 - formData.append('nonce', metasyncEditor.nonce);
947 - formData.append('file', file);
948 -
949 - $.ajax({
950 - url: metasyncEditor.ajax_url,
951 - type: 'POST',
952 - data: formData,
953 - processData: false,
954 - contentType: false,
955 - success: function(response) {
956 - if (response.success) {
957 - editor.AssetManager.add({ src: response.data.url });
958 - } else {
959 - alert(response.data.message || 'Upload failed');
960 - }
961 - }
962 - });
963 - }
964 -
965 - /**
966 1119 * Update status indicator
967 1120 */
968 1121 function updateStatus(status) {
969 1122 const $indicator = $('.metasync-status-indicator');
@@ -972,16 +1125,16 @@
972 1125 $indicator.removeClass('unsaved saving saved error ready');
973 1126 $indicator.addClass(status);
974 1127
975 1128 const statusText = {
976 - ready: 'Ready',
977 - unsaved: 'Unsaved changes',
978 - saving: 'Saving...',
979 - saved: 'Saved!',
980 - error: 'Error'
1129 + ready: t('ready', 'Ready'),
1130 + unsaved: t('unsaved_changes', 'Unsaved changes'),
1131 + saving: t('saving', 'Saving...'),
1132 + saved: t('saved', 'Saved!'),
1133 + error: t('error', 'Error saving')
981 1134 };
982 1135
983 - $text.text(statusText[status] || 'Ready');
1136 + $text.text(statusText[status] || t('ready', 'Ready'));
984 1137 }
985 1138
986 1139 /**
987 1140 * Prevent accidental exit with unsaved changes
@@ -987,9 +1140,12 @@
987 1140 * Prevent accidental exit with unsaved changes
988 1141 */
989 1142 function preventAccidentalExit() {
990 1143 $(window).on('beforeunload', function(e) {
991 - if (hasUnsavedChanges) {
1144 + // Nothing on a blocked page can be saved, so warning about unsaved
1145 + // changes would only trap the user on a page they were told to
1146 + // leave for the lossless editor.
1147 + if (hasUnsavedChanges && !metasyncEditor.blocked_reason) {
992 1148 const message = metasyncEditor.i18n.confirm_exit;
993 1149 e.returnValue = message;
994 1150 return message;
995 1151 }