diff --git a/edit/edit.js b/edit/edit.js index eb41963b..f19225dd 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -25,7 +25,8 @@ const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'do let editor; -window.onbeforeunload = beforeUnload; + +document.addEventListener('visibilitychange', beforeUnload); chrome.runtime.onMessage.addListener(onRuntimeMessage); preinit(); @@ -176,7 +177,8 @@ function onRuntimeMessage(request) { break; case 'styleDeleted': if (styleId === request.id || editor && editor.getStyle().id === request.id) { - window.onbeforeunload = () => {}; + document.removeEventListener('visibilitychange', beforeUnload); + window.onbeforeunload = null; closeCurrentTab(); break; } @@ -192,24 +194,27 @@ function onRuntimeMessage(request) { } } +/** + * Invoked for 'visibilitychange' event by default. + * Invoked for 'beforeunload' event when the style is modified and unsaved. + * See https://developers.google.com/web/updates/2018/07/page-lifecycle-api#legacy-lifecycle-apis-to-avoid + * > Never add a beforeunload listener unconditionally or use it as an end-of-session signal. + * > Only add it when a user has unsaved work, and remove it as soon as that work has been saved. + */ function beforeUnload() { - if (saveSizeOnClose) { - rememberWindowSize(); + if (saveSizeOnClose) rememberWindowSize(); + const activeElement = document.activeElement; + if (activeElement) { + // blurring triggers 'change' or 'input' event if needed + activeElement.blur(); + // refocus if unloading was canceled + setTimeout(() => activeElement.focus()); } - document.activeElement.blur(); - if (isClean()) { - return; - } - updateLintReportIfEnabled(null, 0); - // neither confirm() nor custom messages work in modern browsers but just in case - return t('styleChangesNotSaved'); - - function isClean() { - if (editor) { - return !editor.isDirty(); - } else { - return isCleanGlobal(); - } + const isDirty = editor ? editor.isDirty() : !isCleanGlobal(); + if (isDirty) { + updateLintReportIfEnabled(null, 0); + // neither confirm() nor custom messages work in modern browsers but just in case + return t('styleChangesNotSaved'); } } @@ -406,6 +411,7 @@ function updateTitle() { const clean = isCleanGlobal(); const title = styleId === null ? t('addStyleTitle') : t('editStyleTitle', [name]); document.title = clean ? title : DIRTY_TITLE.replace('$', title); + window.onbeforeunload = clean ? null : beforeUnload; $('#save-button').disabled = clean; } diff --git a/edit/source-editor.js b/edit/source-editor.js index 32c3e972..53f8d510 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -4,6 +4,7 @@ global CodeMirror dirtyReporter global updateLintReportIfEnabled initLint linterConfig updateLinter global createAppliesToLineWidget messageBox global sectionsToMozFormat +global beforeUnload */ 'use strict'; @@ -18,8 +19,10 @@ function createSourceEditor(style) { const dirty = dirtyReporter(); dirty.onChange(() => { - document.body.classList.toggle('dirty', dirty.isDirty()); - $('#save-button').disabled = !dirty.isDirty(); + const isDirty = dirty.isDirty(); + window.onbeforeunload = isDirty ? beforeUnload : null; + document.body.classList.toggle('dirty', isDirty); + $('#save-button').disabled = !isDirty; updateTitle(); }); diff --git a/manage/manage.js b/manage/manage.js index 25204021..6db745d6 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -73,8 +73,7 @@ function initGlobalEvents() { installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle); installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle); - // remember scroll position on normal history navigation - window.onbeforeunload = rememberScrollPosition; + document.addEventListener('visibilitychange', rememberScrollPosition); $$('[data-toggle-on-click]').forEach(el => { // dataset on SVG doesn't work in Chrome 49-??, works in 57+