| @@ -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 | } |