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);