From f67744387882b646a50911973ad9cfe32b7f50f4 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 19 May 2019 15:35:00 -0400 Subject: [PATCH] Warn when navigating away from pane with unsaved changes Related issue: - https://github.com/gorhill/uBlock/issues/3271 When navigating away by clicking another pane tab button, there will be an embedded warning, which can be ignore in order to proceed to the new pane, or dismissed by either clicking on the "Stay" button or anywhere else in the dashboard. When navigating away by trying to close the tab, there will be a built-in browser warning asking for confirmation. --- src/_locales/en/messages.json | 12 ++ src/css/dashboard.css | 26 +++- src/dashboard.html | 8 ++ src/js/1p-filters.js | 11 +- src/js/3p-filters.js | 246 +++++++++++++++++----------------- src/js/dashboard.js | 98 ++++++++++---- src/js/dyna-rules.js | 12 +- src/js/whitelist.js | 6 + 8 files changed, 262 insertions(+), 157 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 83febf56d..b65f6320a 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -11,6 +11,18 @@ "message":"uBlock₀ — Dashboard", "description":"English: uBlock₀ — Dashboard" }, + "dashboardUnsavedWarning":{ + "message":"Warning! You have unsaved changes", + "description":"A warning in the dashboard when navigating away from unsaved changes" + }, + "dashboardUnsavedWarningStay":{ + "message":"Stay", + "description":"Label for button to prevent navigating away from unsaved changes" + }, + "dashboardUnsavedWarningIgnore":{ + "message":"Ignore", + "description":"Label for button to ignore unsaved changes" + }, "settingsPageName":{ "message":"Settings", "description":"appears as tab name in dashboard" diff --git a/src/css/dashboard.css b/src/css/dashboard.css index b5e78a6bc..bd25eced4 100644 --- a/src/css/dashboard.css +++ b/src/css/dashboard.css @@ -66,12 +66,32 @@ html, body { border-bottom: 1px solid white; } iframe { - margin: 0; - border: 0; - padding: 0; background-color: transparent; + border: 0; + margin: 0; + padding: 0; width: 100%; } +#unsavedWarning { + box-shadow: rgba(128,128,128,0.4) 0 4px 4px; + display: none; + left: 0; + position: absolute; + width: 100%; + z-index: 20; + } +#unsavedWarning.on { + display: initial; + } +#unsavedWarning > div:first-of-type { + background-color: #ffffcc; + padding: 0.5em; + } +#unsavedWarning > div:last-of-type { + height: 100vh; + position: absolute; + width: 100vw; + } body:not(.canUpdateShortcuts) .tabButton[href="#shortcuts.html"] { display: none; diff --git a/src/dashboard.html b/src/dashboard.html index 3e3106ad4..92689608c 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -22,6 +22,14 @@ --> +
+
+   +   + +
+
+
diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index 4e7b19138..045542970 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -25,7 +25,7 @@ /******************************************************************************/ -(function() { +(( ) => { /******************************************************************************/ @@ -58,14 +58,13 @@ window.addEventListener('beforeunload', ( ) => { ); }); - /******************************************************************************/ // This is to give a visual hint that the content of user blacklist has changed. const userFiltersChanged = function(changed) { if ( typeof changed !== 'boolean' ) { - changed = cmEditor.getValue().trim() !== cachedUserFilters; + changed = self.hasUnsavedData(); } uDom.nodeFromId('userFiltersApply').disabled = !changed; uDom.nodeFromId('userFiltersRevert').disabled = !changed; @@ -214,6 +213,12 @@ self.cloud.onPull = setCloudData; /******************************************************************************/ +self.hasUnsavedData = function() { + return cmEditor.getValue().trim() !== cachedUserFilters; +}; + +/******************************************************************************/ + // Handle user interaction uDom('#importUserFiltersFromFile').on('click', startImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker); diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js index 5963c1e21..94c35d35c 100644 --- a/src/js/3p-filters.js +++ b/src/js/3p-filters.js @@ -25,19 +25,20 @@ /******************************************************************************/ -(function() { +(( ) => { /******************************************************************************/ -var listDetails = {}, - filteringSettingsHash = '', - lastUpdateTemplateString = vAPI.i18n('3pLastUpdate'), - reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/, - hideUnusedSet = new Set(); +const lastUpdateTemplateString = vAPI.i18n('3pLastUpdate'); +const reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/; + +let listDetails = {}; +let filteringSettingsHash = ''; +let hideUnusedSet = new Set(); /******************************************************************************/ -var onMessage = function(msg) { +const onMessage = function(msg) { switch ( msg.what ) { case 'assetUpdated': updateAssetStatus(msg); @@ -54,39 +55,39 @@ var onMessage = function(msg) { } }; -var messaging = vAPI.messaging; +const messaging = vAPI.messaging; messaging.addChannelListener('dashboard', onMessage); /******************************************************************************/ -var renderNumber = function(value) { +const renderNumber = function(value) { return value.toLocaleString(); }; /******************************************************************************/ -var renderFilterLists = function(soft) { - var listGroupTemplate = uDom('#templates .groupEntry'), - listEntryTemplate = uDom('#templates .listEntry'), - listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats'), - renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString, - groupNames = new Map([ [ 'user', '' ] ]); +const renderFilterLists = function(soft) { + const listGroupTemplate = uDom('#templates .groupEntry'); + const listEntryTemplate = uDom('#templates .listEntry'); + const listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats'); + const renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString; + const groupNames = new Map([ [ 'user', '' ] ]); // Assemble a pretty list name if possible - var listNameFromListKey = function(listKey) { - var list = listDetails.current[listKey] || listDetails.available[listKey]; - var listTitle = list ? list.title : ''; + const listNameFromListKey = function(listKey) { + const list = listDetails.current[listKey] || listDetails.available[listKey]; + const listTitle = list ? list.title : ''; if ( listTitle === '' ) { return listKey; } return listTitle; }; - var liFromListEntry = function(listKey, li, hideUnused) { - var entry = listDetails.available[listKey], - elem; + const liFromListEntry = function(listKey, li, hideUnused) { + const entry = listDetails.available[listKey]; if ( !li ) { li = listEntryTemplate.clone().nodeAt(0); } - var on = entry.off !== true; + const on = entry.off !== true; + let elem; if ( li.getAttribute('data-listkey') !== listKey ) { li.setAttribute('data-listkey', listKey); elem = li.querySelector('input[type="checkbox"]'); @@ -123,7 +124,7 @@ var renderFilterLists = function(soft) { li.querySelector('input[type="checkbox"]').checked = on; } elem = li.querySelector('span.counts'); - var text = ''; + let text = ''; if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) { text = listStatsTemplate .replace('{{used}}', renderNumber(on ? entry.entryUsedCount : 0)) @@ -131,8 +132,8 @@ var renderFilterLists = function(soft) { } elem.textContent = text; // https://github.com/chrisaljoudi/uBlock/issues/104 - var asset = listDetails.cache[listKey] || {}; - var remoteURL = asset.remoteURL; + const asset = listDetails.cache[listKey] || {}; + const remoteURL = asset.remoteURL; li.classList.toggle( 'unsecure', typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0 @@ -155,24 +156,23 @@ var renderFilterLists = function(soft) { return li; }; - var listEntryCountFromGroup = function(listKeys) { + const listEntryCountFromGroup = function(listKeys) { if ( Array.isArray(listKeys) === false ) { return ''; } - var count = 0, + let count = 0, total = 0; - var i = listKeys.length; - while ( i-- ) { - if ( listDetails.available[listKeys[i]].off !== true ) { + for ( const listKey of listKeys ) { + if ( listDetails.available[listKey].off !== true ) { count += 1; } total += 1; } return total !== 0 ? - '(' + count.toLocaleString() + '/' + total.toLocaleString() + ')' : + `(${count.toLocaleString()}/${total.toLocaleString()})` : ''; }; - var liFromListGroup = function(groupKey, listKeys) { - let liGroup = document.querySelector('#lists > .groupEntry[data-groupkey="' + groupKey + '"]'); + const liFromListGroup = function(groupKey, listKeys) { + let liGroup = document.querySelector(`#lists > .groupEntry[data-groupkey="${groupKey}"]`); if ( liGroup === null ) { liGroup = listGroupTemplate.clone().nodeAt(0); let groupName = groupNames.get(groupKey); @@ -207,7 +207,7 @@ var renderFilterLists = function(soft) { return liGroup; }; - var groupsFromLists = function(lists) { + const groupsFromLists = function(lists) { let groups = new Map(); let listKeys = Object.keys(lists); for ( let listKey of listKeys ) { @@ -225,7 +225,7 @@ var renderFilterLists = function(soft) { return groups; }; - var onListsReceived = function(details) { + const onListsReceived = function(details) { // Before all, set context vars listDetails = details; @@ -238,22 +238,22 @@ var renderFilterLists = function(soft) { uDom('#lists .listEntries .listEntry[data-listkey]').addClass('discard'); // Remove import widget while we recreate list of lists. - var importWidget = uDom('.listEntry.toImport').detach(); + const importWidget = uDom('.listEntry.toImport').detach(); // Visually split the filter lists in purpose-based groups - var ulLists = document.querySelector('#lists'), - groups = groupsFromLists(details.available), - groupKeys = [ - 'user', - 'default', - 'ads', - 'privacy', - 'malware', - 'annoyances', - 'multipurpose', - 'regions', - 'custom' - ]; + const ulLists = document.querySelector('#lists'); + const groups = groupsFromLists(details.available); + const groupKeys = [ + 'user', + 'default', + 'ads', + 'privacy', + 'malware', + 'annoyances', + 'multipurpose', + 'regions', + 'custom' + ]; document.body.classList.toggle('hideUnused', mustHideUnusedLists('*')); for ( let i = 0; i < groupKeys.length; i++ ) { let groupKey = groupKeys[i]; @@ -269,8 +269,7 @@ var renderFilterLists = function(soft) { groups.delete(groupKey); } // For all groups not covered above (if any left) - groupKeys = Object.keys(groups); - for ( let groupKey of groupKeys.keys() ) { + for ( const groupKey of Object.keys(groups) ) { ulLists.appendChild(liFromListGroup(groupKey, groupKey)); } @@ -308,7 +307,7 @@ var renderFilterLists = function(soft) { /******************************************************************************/ -var renderWidgets = function() { +const renderWidgets = function() { uDom('#buttonApply').toggleClass( 'disabled', filteringSettingsHash === hashFromCurrentFromSettings() @@ -325,8 +324,8 @@ var renderWidgets = function() { /******************************************************************************/ -var updateAssetStatus = function(details) { - let li = document.querySelector( +const updateAssetStatus = function(details) { + const li = document.querySelector( '#lists .listEntry[data-listkey="' + details.key + '"]' ); if ( li === null ) { return; } @@ -352,17 +351,14 @@ var updateAssetStatus = function(details) { **/ -var hashFromCurrentFromSettings = function() { - var hash = [ +const hashFromCurrentFromSettings = function() { + const hash = [ uDom.nodeFromId('parseCosmeticFilters').checked, uDom.nodeFromId('ignoreGenericCosmeticFilters').checked ]; - var listHash = [], - listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'), - liEntry, - i = listEntries.length; - while ( i-- ) { - liEntry = listEntries[i]; + const listHash = []; + const listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'); + for ( const liEntry of listEntries ) { if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) { listHash.push(liEntry.getAttribute('data-listkey')); } @@ -378,15 +374,15 @@ var hashFromCurrentFromSettings = function() { /******************************************************************************/ -var onFilteringSettingsChanged = function() { +const onFilteringSettingsChanged = function() { renderWidgets(); }; /******************************************************************************/ -var onRemoveExternalList = function(ev) { - var liEntry = uDom(this).ancestors('[data-listkey]'), - listKey = liEntry.attr('data-listkey'); +const onRemoveExternalList = function(ev) { + const liEntry = uDom(this).ancestors('[data-listkey]'); + const listKey = liEntry.attr('data-listkey'); if ( listKey ) { liEntry.toggleClass('toRemove'); renderWidgets(); @@ -396,10 +392,10 @@ var onRemoveExternalList = function(ev) { /******************************************************************************/ -var onPurgeClicked = function() { - var button = uDom(this), - liEntry = button.ancestors('[data-listkey]'), - listKey = liEntry.attr('data-listkey'); +const onPurgeClicked = function(ev) { + const button = uDom(ev.target); + const liEntry = button.ancestors('[data-listkey]'); + const listKey = liEntry.attr('data-listkey'); if ( !listKey ) { return; } messaging.send('dashboard', { what: 'purgeCache', assetKey: listKey }); @@ -419,7 +415,7 @@ var onPurgeClicked = function() { /******************************************************************************/ -var selectFilterLists = function(callback) { +const selectFilterLists = function(callback) { // Cosmetic filtering switch messaging.send('dashboard', { what: 'userSettings', @@ -433,28 +429,24 @@ var selectFilterLists = function(callback) { }); // Filter lists to select - var toSelect = [], - liEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'), - i = liEntries.length, - liEntry; - while ( i-- ) { - liEntry = liEntries[i]; + const toSelect = []; + let liEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'); + for ( const liEntry of liEntries ) { if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) { toSelect.push(liEntry.getAttribute('data-listkey')); } } // External filter lists to remove - var toRemove = []; + const toRemove = []; liEntries = document.querySelectorAll('#lists .listEntry.toRemove[data-listkey]'); - i = liEntries.length; - while ( i-- ) { - toRemove.push(liEntries[i].getAttribute('data-listkey')); + for ( const liEntry of liEntries ) { + toRemove.push(liEntry.getAttribute('data-listkey')); } // External filter lists to import - var externalListsElem = document.getElementById('externalLists'), - toImport = externalListsElem.value.trim(); + const externalListsElem = document.getElementById('externalLists'); + const toImport = externalListsElem.value.trim(); externalListsElem.value = ''; uDom.nodeFromId('importLists').checked = false; @@ -473,30 +465,28 @@ var selectFilterLists = function(callback) { /******************************************************************************/ -var buttonApplyHandler = function() { +const buttonApplyHandler = function() { uDom('#buttonApply').removeClass('enabled'); - var onSelectionDone = function() { + selectFilterLists(( ) => { messaging.send('dashboard', { what: 'reloadAllFilters' }); - }; - selectFilterLists(onSelectionDone); + }); renderWidgets(); }; /******************************************************************************/ -var buttonUpdateHandler = function() { - var onSelectionDone = function() { +const buttonUpdateHandler = function() { + selectFilterLists(( ) => { document.body.classList.add('updating'); messaging.send('dashboard', { what: 'forceUpdateAssets' }); renderWidgets(); - }; - selectFilterLists(onSelectionDone); + }); renderWidgets(); }; /******************************************************************************/ -var buttonPurgeAllHandler = function(ev) { +const buttonPurgeAllHandler = function(ev) { uDom('#buttonPurgeAll').removeClass('enabled'); messaging.send( 'dashboard', @@ -504,13 +494,15 @@ var buttonPurgeAllHandler = function(ev) { what: 'purgeAllCaches', hard: ev.ctrlKey && ev.shiftKey }, - function() { renderFilterLists(true); } + ( ) => { + renderFilterLists(true); + } ); }; /******************************************************************************/ -var autoUpdateCheckboxChanged = function() { +const autoUpdateCheckboxChanged = function() { messaging.send( 'dashboard', { @@ -525,16 +517,16 @@ var autoUpdateCheckboxChanged = function() { // Collapsing of unused lists. -var mustHideUnusedLists = function(which) { - var hideAll = hideUnusedSet.has('*'); +const mustHideUnusedLists = function(which) { + const hideAll = hideUnusedSet.has('*'); if ( which === '*' ) { return hideAll; } return hideUnusedSet.has(which) !== hideAll; }; -var toggleHideUnusedLists = function(which) { - var groupSelector, - doesHideAll = hideUnusedSet.has('*'), - mustHide; +const toggleHideUnusedLists = function(which) { + const doesHideAll = hideUnusedSet.has('*'); + let groupSelector; + let mustHide; if ( which === '*' ) { mustHide = doesHideAll === false; groupSelector = ''; @@ -545,7 +537,7 @@ var toggleHideUnusedLists = function(which) { document.body.classList.toggle('hideUnused', mustHide); uDom('.groupEntry[data-groupkey]').toggleClass('hideUnused', mustHide); } else { - var doesHide = hideUnusedSet.has(which); + const doesHide = hideUnusedSet.has(which); if ( doesHide ) { hideUnusedSet.delete(which); } else { @@ -564,7 +556,7 @@ var toggleHideUnusedLists = function(which) { ); }; -var revealHiddenUsedLists = function() { +const revealHiddenUsedLists = function() { uDom('#lists .listEntry.unused > input[type="checkbox"]:checked') .ancestors('.listEntry[data-listkey]') .removeClass('unused'); @@ -582,10 +574,11 @@ uDom('#lists').on('click', '.groupEntry[data-groupkey] > .geDetails', function(e ); }); -(function() { - var aa; +// Initialize from saved state. +{ + let aa; try { - var json = vAPI.localStorage.getItem('hideUnusedFilterLists'); + const json = vAPI.localStorage.getItem('hideUnusedFilterLists'); if ( json !== null ) { aa = JSON.parse(json); } @@ -595,35 +588,33 @@ uDom('#lists').on('click', '.groupEntry[data-groupkey] > .geDetails', function(e aa = [ '*' ]; } hideUnusedSet = new Set(aa); -})(); +} /******************************************************************************/ // Cloud-related. -var toCloudData = function() { - var bin = { +const toCloudData = function() { + const bin = { parseCosmeticFilters: uDom.nodeFromId('parseCosmeticFilters').checked, ignoreGenericCosmeticFilters: uDom.nodeFromId('ignoreGenericCosmeticFilters').checked, selectedLists: [] }; - var liEntries = uDom('#lists .listEntry'), liEntry; - var i = liEntries.length; - while ( i-- ) { - liEntry = liEntries.at(i); - if ( liEntry.descendants('input').prop('checked') ) { - bin.selectedLists.push(liEntry.attr('data-listkey')); + const liEntries = document.querySelectorAll('#lists .listEntry'); + for ( const liEntry of liEntries ) { + if ( liEntry.querySelector('input').checked ) { + bin.selectedLists.push(liEntry.getAttribute('data-listkey')); } } return bin; }; -var fromCloudData = function(data, append) { +const fromCloudData = function(data, append) { if ( typeof data !== 'object' || data === null ) { return; } - var elem, checked; + let elem, checked; elem = uDom.nodeFromId('parseCosmeticFilters'); checked = data.parseCosmeticFilters === true || append && elem.checked; @@ -633,21 +624,20 @@ var fromCloudData = function(data, append) { checked = data.ignoreGenericCosmeticFilters === true || append && elem.checked; elem.checked = listDetails.ignoreGenericCosmeticFilters = checked; - var selectedSet = new Set(data.selectedLists), - listEntries = uDom('#lists .listEntry'), - listEntry, listKey; - for ( var i = 0, n = listEntries.length; i < n; i++ ) { - listEntry = listEntries.at(i); - listKey = listEntry.attr('data-listkey'); - var hasListKey = selectedSet.has(listKey); + const selectedSet = new Set(data.selectedLists); + const listEntries = uDom('#lists .listEntry'); + for ( let i = 0, n = listEntries.length; i < n; i++ ) { + const listEntry = listEntries.at(i); + const listKey = listEntry.attr('data-listkey'); + const hasListKey = selectedSet.has(listKey); selectedSet.delete(listKey); - var input = listEntry.descendants('input').first(); + const input = listEntry.descendants('input').first(); if ( append && input.prop('checked') ) { continue; } input.prop('checked', hasListKey); } // If there are URL-like list keys left in the selected set, import them. - for ( listKey of selectedSet ) { + for ( const listKey of selectedSet ) { if ( reValidExternalList.test(listKey) === false ) { selectedSet.delete(listKey); } @@ -672,6 +662,12 @@ self.cloud.onPull = fromCloudData; /******************************************************************************/ +self.hasUnsavedData = function() { + return hashFromCurrentFromSettings() !== filteringSettingsHash; +}; + +/******************************************************************************/ + uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged); uDom('#parseCosmeticFilters').on('change', onFilteringSettingsChanged); uDom('#ignoreGenericCosmeticFilters').on('change', onFilteringSettingsChanged); diff --git a/src/js/dashboard.js b/src/js/dashboard.js index efd744306..6bea58e9c 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -25,48 +25,93 @@ /******************************************************************************/ -(function() { +(( ) => { /******************************************************************************/ const resizeFrame = function() { - let navRect = document.getElementById('dashboard-nav').getBoundingClientRect(); - let viewRect = document.documentElement.getBoundingClientRect(); + const navRect = document.getElementById('dashboard-nav') + .getBoundingClientRect(); + const viewRect = document.documentElement.getBoundingClientRect(); document.getElementById('iframe').style.setProperty( 'height', (viewRect.height - navRect.height) + 'px' ); }; -const loadDashboardPanel = function() { - let pane = window.location.hash.slice(1); +const discardUnsavedData = function(synchronous = false) { + const paneFrame = document.getElementById('iframe'); + const paneWindow = paneFrame.contentWindow; + if ( + typeof paneWindow.hasUnsavedData !== 'function' || + paneWindow.hasUnsavedData() === false + ) { + return true; + } + + if ( synchronous ) { + return false; + } + + return new Promise(resolve => { + const modal = uDom.nodeFromId('unsavedWarning'); + modal.classList.add('on'); + modal.focus(); + + const onDone = status => { + modal.classList.remove('on'); + document.removeEventListener('click', onClick, true); + resolve(status); + }; + + const onClick = ev => { + const target = ev.target; + if ( target.matches('[data-i18n="dashboardUnsavedWarningStay"]') ) { + return onDone(false); + } + if ( target.matches('[data-i18n="dashboardUnsavedWarningIgnore"]') ) { + return onDone(true); + } + if ( modal.querySelector('[data-i18n="dashboardUnsavedWarning"]').contains(target) ) { + return; + } + onDone(false); + }; + + document.addEventListener('click', onClick, true); + }); +}; + +const loadDashboardPanel = function(pane = '') { if ( pane === '' ) { pane = vAPI.localStorage.getItem('dashboardLastVisitedPane'); if ( pane === null ) { pane = 'settings.html'; } - } else { - vAPI.localStorage.setItem('dashboardLastVisitedPane', pane); } - let tabButton = uDom('[href="#' + pane + '"]'); + const tabButton = uDom(`[href="#${pane}"]`); if ( !tabButton || tabButton.hasClass('selected') ) { return; } - uDom('.tabButton.selected').toggleClass('selected', false); - uDom('iframe').attr('src', pane); - tabButton.toggleClass('selected', true); + const loadPane = ( ) => { + self.location.replace(`#${pane}`); + uDom('.tabButton.selected').toggleClass('selected', false); + tabButton.toggleClass('selected', true); + uDom.nodeFromId('iframe').setAttribute('src', pane); + vAPI.localStorage.setItem('dashboardLastVisitedPane', pane); + }; + const r = discardUnsavedData(); + if ( r === false ) { return; } + if ( r === true ) { + return loadPane(); + } + r.then(status => { + if ( status === false ) { return; } + loadPane(); + }); }; -const onTabClickHandler = function(e) { - let url = window.location.href, - pos = url.indexOf('#'); - if ( pos !== -1 ) { - url = url.slice(0, pos); - } - url += this.hash; - if ( url !== window.location.href ) { - window.location.replace(url); - loadDashboardPanel(); - } - e.preventDefault(); +const onTabClickHandler = function(ev) { + loadDashboardPanel(ev.target.hash.slice(1)); + ev.preventDefault(); }; // https://github.com/uBlockOrigin/uBlock-issues/issues/106 @@ -80,6 +125,13 @@ loadDashboardPanel(); window.addEventListener('resize', resizeFrame); uDom('.tabButton').on('click', onTabClickHandler); +// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event +window.addEventListener('beforeunload', ( ) => { + if ( discardUnsavedData(true) ) { return; } + event.preventDefault(); + event.returnValue = ''; +}); + /******************************************************************************/ })(); diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js index 5e36ce912..77565c58a 100644 --- a/src/js/dyna-rules.js +++ b/src/js/dyna-rules.js @@ -25,7 +25,7 @@ /******************************************************************************/ -(function() { +(( ) => { /******************************************************************************/ @@ -329,7 +329,7 @@ const onFilterChanged = (function() { overlay = null, last = ''; - let process = function() { + const process = function() { timer = undefined; if ( mergeView.editor().isClean(cleanEditToken) === false ) { return; } let filter = uDom.nodeFromSelector('#ruleFilter input').value; @@ -359,7 +359,7 @@ const onFilterChanged = (function() { const onTextChanged = (function() { let timer; - let process = function(now) { + const process = function(now) { timer = undefined; const diff = document.getElementById('diff'); let isClean = mergeView.editor().isClean(cleanEditToken); @@ -474,6 +474,12 @@ self.cloud.onPull = function(data, append) { /******************************************************************************/ +self.hasUnsavedData = function() { + return mergeView.editor().isClean(cleanEditToken) === false; +}; + +/******************************************************************************/ + messaging.send('dashboard', { what: 'getRules' }, renderRules); // Handle user interaction diff --git a/src/js/whitelist.js b/src/js/whitelist.js index fac008542..9ba00fea7 100644 --- a/src/js/whitelist.js +++ b/src/js/whitelist.js @@ -238,6 +238,12 @@ self.cloud.onPull = setCloudData; /******************************************************************************/ +self.hasUnsavedData = function() { + return cmEditor.getValue().trim() !== cachedWhitelist; +}; + +/******************************************************************************/ + uDom('#importWhitelistFromFile').on('click', startImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile);