1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-10-04 08:37:11 +02:00

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.
This commit is contained in:
Raymond Hill 2019-05-19 15:35:00 -04:00
parent 6f9216585b
commit f677443878
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
8 changed files with 262 additions and 157 deletions

View File

@ -11,6 +11,18 @@
"message":"uBlock₀ — Dashboard", "message":"uBlock₀ — Dashboard",
"description":"English: 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":{ "settingsPageName":{
"message":"Settings", "message":"Settings",
"description":"appears as tab name in dashboard" "description":"appears as tab name in dashboard"

View File

@ -66,12 +66,32 @@ html, body {
border-bottom: 1px solid white; border-bottom: 1px solid white;
} }
iframe { iframe {
margin: 0;
border: 0;
padding: 0;
background-color: transparent; background-color: transparent;
border: 0;
margin: 0;
padding: 0;
width: 100%; 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"] { body:not(.canUpdateShortcuts) .tabButton[href="#shortcuts.html"] {
display: none; display: none;

View File

@ -22,6 +22,14 @@
--><a class="tabButton" href="#about.html" data-i18n="aboutPageName"></a> --><a class="tabButton" href="#about.html" data-i18n="aboutPageName"></a>
</div> </div>
</div> </div>
<div id="unsavedWarning">
<div>
<span data-i18n="dashboardUnsavedWarning"></span>&emsp;
<button class="custom" data-i18n="dashboardUnsavedWarningStay"></button>&emsp;
<button class="custom" data-i18n="dashboardUnsavedWarningIgnore"></button>
</div>
<div></div>
</div>
<iframe id="iframe" src=""></iframe> <iframe id="iframe" src=""></iframe>

View File

@ -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. // This is to give a visual hint that the content of user blacklist has changed.
const userFiltersChanged = function(changed) { const userFiltersChanged = function(changed) {
if ( typeof changed !== 'boolean' ) { if ( typeof changed !== 'boolean' ) {
changed = cmEditor.getValue().trim() !== cachedUserFilters; changed = self.hasUnsavedData();
} }
uDom.nodeFromId('userFiltersApply').disabled = !changed; uDom.nodeFromId('userFiltersApply').disabled = !changed;
uDom.nodeFromId('userFiltersRevert').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 // Handle user interaction
uDom('#importUserFiltersFromFile').on('click', startImportFilePicker); uDom('#importUserFiltersFromFile').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker);

View File

@ -25,19 +25,20 @@
/******************************************************************************/ /******************************************************************************/
(function() { (( ) => {
/******************************************************************************/ /******************************************************************************/
var listDetails = {}, const lastUpdateTemplateString = vAPI.i18n('3pLastUpdate');
filteringSettingsHash = '', const reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/;
lastUpdateTemplateString = vAPI.i18n('3pLastUpdate'),
reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/, let listDetails = {};
hideUnusedSet = new Set(); let filteringSettingsHash = '';
let hideUnusedSet = new Set();
/******************************************************************************/ /******************************************************************************/
var onMessage = function(msg) { const onMessage = function(msg) {
switch ( msg.what ) { switch ( msg.what ) {
case 'assetUpdated': case 'assetUpdated':
updateAssetStatus(msg); updateAssetStatus(msg);
@ -54,39 +55,39 @@ var onMessage = function(msg) {
} }
}; };
var messaging = vAPI.messaging; const messaging = vAPI.messaging;
messaging.addChannelListener('dashboard', onMessage); messaging.addChannelListener('dashboard', onMessage);
/******************************************************************************/ /******************************************************************************/
var renderNumber = function(value) { const renderNumber = function(value) {
return value.toLocaleString(); return value.toLocaleString();
}; };
/******************************************************************************/ /******************************************************************************/
var renderFilterLists = function(soft) { const renderFilterLists = function(soft) {
var listGroupTemplate = uDom('#templates .groupEntry'), const listGroupTemplate = uDom('#templates .groupEntry');
listEntryTemplate = uDom('#templates .listEntry'), const listEntryTemplate = uDom('#templates .listEntry');
listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats'), const listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats');
renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString, const renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString;
groupNames = new Map([ [ 'user', '' ] ]); const groupNames = new Map([ [ 'user', '' ] ]);
// Assemble a pretty list name if possible // Assemble a pretty list name if possible
var listNameFromListKey = function(listKey) { const listNameFromListKey = function(listKey) {
var list = listDetails.current[listKey] || listDetails.available[listKey]; const list = listDetails.current[listKey] || listDetails.available[listKey];
var listTitle = list ? list.title : ''; const listTitle = list ? list.title : '';
if ( listTitle === '' ) { return listKey; } if ( listTitle === '' ) { return listKey; }
return listTitle; return listTitle;
}; };
var liFromListEntry = function(listKey, li, hideUnused) { const liFromListEntry = function(listKey, li, hideUnused) {
var entry = listDetails.available[listKey], const entry = listDetails.available[listKey];
elem;
if ( !li ) { if ( !li ) {
li = listEntryTemplate.clone().nodeAt(0); li = listEntryTemplate.clone().nodeAt(0);
} }
var on = entry.off !== true; const on = entry.off !== true;
let elem;
if ( li.getAttribute('data-listkey') !== listKey ) { if ( li.getAttribute('data-listkey') !== listKey ) {
li.setAttribute('data-listkey', listKey); li.setAttribute('data-listkey', listKey);
elem = li.querySelector('input[type="checkbox"]'); elem = li.querySelector('input[type="checkbox"]');
@ -123,7 +124,7 @@ var renderFilterLists = function(soft) {
li.querySelector('input[type="checkbox"]').checked = on; li.querySelector('input[type="checkbox"]').checked = on;
} }
elem = li.querySelector('span.counts'); elem = li.querySelector('span.counts');
var text = ''; let text = '';
if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) { if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) {
text = listStatsTemplate text = listStatsTemplate
.replace('{{used}}', renderNumber(on ? entry.entryUsedCount : 0)) .replace('{{used}}', renderNumber(on ? entry.entryUsedCount : 0))
@ -131,8 +132,8 @@ var renderFilterLists = function(soft) {
} }
elem.textContent = text; elem.textContent = text;
// https://github.com/chrisaljoudi/uBlock/issues/104 // https://github.com/chrisaljoudi/uBlock/issues/104
var asset = listDetails.cache[listKey] || {}; const asset = listDetails.cache[listKey] || {};
var remoteURL = asset.remoteURL; const remoteURL = asset.remoteURL;
li.classList.toggle( li.classList.toggle(
'unsecure', 'unsecure',
typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0 typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0
@ -155,24 +156,23 @@ var renderFilterLists = function(soft) {
return li; return li;
}; };
var listEntryCountFromGroup = function(listKeys) { const listEntryCountFromGroup = function(listKeys) {
if ( Array.isArray(listKeys) === false ) { return ''; } if ( Array.isArray(listKeys) === false ) { return ''; }
var count = 0, let count = 0,
total = 0; total = 0;
var i = listKeys.length; for ( const listKey of listKeys ) {
while ( i-- ) { if ( listDetails.available[listKey].off !== true ) {
if ( listDetails.available[listKeys[i]].off !== true ) {
count += 1; count += 1;
} }
total += 1; total += 1;
} }
return total !== 0 ? return total !== 0 ?
'(' + count.toLocaleString() + '/' + total.toLocaleString() + ')' : `(${count.toLocaleString()}/${total.toLocaleString()})` :
''; '';
}; };
var liFromListGroup = function(groupKey, listKeys) { const liFromListGroup = function(groupKey, listKeys) {
let liGroup = document.querySelector('#lists > .groupEntry[data-groupkey="' + groupKey + '"]'); let liGroup = document.querySelector(`#lists > .groupEntry[data-groupkey="${groupKey}"]`);
if ( liGroup === null ) { if ( liGroup === null ) {
liGroup = listGroupTemplate.clone().nodeAt(0); liGroup = listGroupTemplate.clone().nodeAt(0);
let groupName = groupNames.get(groupKey); let groupName = groupNames.get(groupKey);
@ -207,7 +207,7 @@ var renderFilterLists = function(soft) {
return liGroup; return liGroup;
}; };
var groupsFromLists = function(lists) { const groupsFromLists = function(lists) {
let groups = new Map(); let groups = new Map();
let listKeys = Object.keys(lists); let listKeys = Object.keys(lists);
for ( let listKey of listKeys ) { for ( let listKey of listKeys ) {
@ -225,7 +225,7 @@ var renderFilterLists = function(soft) {
return groups; return groups;
}; };
var onListsReceived = function(details) { const onListsReceived = function(details) {
// Before all, set context vars // Before all, set context vars
listDetails = details; listDetails = details;
@ -238,22 +238,22 @@ var renderFilterLists = function(soft) {
uDom('#lists .listEntries .listEntry[data-listkey]').addClass('discard'); uDom('#lists .listEntries .listEntry[data-listkey]').addClass('discard');
// Remove import widget while we recreate list of lists. // 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 // Visually split the filter lists in purpose-based groups
var ulLists = document.querySelector('#lists'), const ulLists = document.querySelector('#lists');
groups = groupsFromLists(details.available), const groups = groupsFromLists(details.available);
groupKeys = [ const groupKeys = [
'user', 'user',
'default', 'default',
'ads', 'ads',
'privacy', 'privacy',
'malware', 'malware',
'annoyances', 'annoyances',
'multipurpose', 'multipurpose',
'regions', 'regions',
'custom' 'custom'
]; ];
document.body.classList.toggle('hideUnused', mustHideUnusedLists('*')); document.body.classList.toggle('hideUnused', mustHideUnusedLists('*'));
for ( let i = 0; i < groupKeys.length; i++ ) { for ( let i = 0; i < groupKeys.length; i++ ) {
let groupKey = groupKeys[i]; let groupKey = groupKeys[i];
@ -269,8 +269,7 @@ var renderFilterLists = function(soft) {
groups.delete(groupKey); groups.delete(groupKey);
} }
// For all groups not covered above (if any left) // For all groups not covered above (if any left)
groupKeys = Object.keys(groups); for ( const groupKey of Object.keys(groups) ) {
for ( let groupKey of groupKeys.keys() ) {
ulLists.appendChild(liFromListGroup(groupKey, groupKey)); ulLists.appendChild(liFromListGroup(groupKey, groupKey));
} }
@ -308,7 +307,7 @@ var renderFilterLists = function(soft) {
/******************************************************************************/ /******************************************************************************/
var renderWidgets = function() { const renderWidgets = function() {
uDom('#buttonApply').toggleClass( uDom('#buttonApply').toggleClass(
'disabled', 'disabled',
filteringSettingsHash === hashFromCurrentFromSettings() filteringSettingsHash === hashFromCurrentFromSettings()
@ -325,8 +324,8 @@ var renderWidgets = function() {
/******************************************************************************/ /******************************************************************************/
var updateAssetStatus = function(details) { const updateAssetStatus = function(details) {
let li = document.querySelector( const li = document.querySelector(
'#lists .listEntry[data-listkey="' + details.key + '"]' '#lists .listEntry[data-listkey="' + details.key + '"]'
); );
if ( li === null ) { return; } if ( li === null ) { return; }
@ -352,17 +351,14 @@ var updateAssetStatus = function(details) {
**/ **/
var hashFromCurrentFromSettings = function() { const hashFromCurrentFromSettings = function() {
var hash = [ const hash = [
uDom.nodeFromId('parseCosmeticFilters').checked, uDom.nodeFromId('parseCosmeticFilters').checked,
uDom.nodeFromId('ignoreGenericCosmeticFilters').checked uDom.nodeFromId('ignoreGenericCosmeticFilters').checked
]; ];
var listHash = [], const listHash = [];
listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'), const listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)');
liEntry, for ( const liEntry of listEntries ) {
i = listEntries.length;
while ( i-- ) {
liEntry = listEntries[i];
if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) { if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
listHash.push(liEntry.getAttribute('data-listkey')); listHash.push(liEntry.getAttribute('data-listkey'));
} }
@ -378,15 +374,15 @@ var hashFromCurrentFromSettings = function() {
/******************************************************************************/ /******************************************************************************/
var onFilteringSettingsChanged = function() { const onFilteringSettingsChanged = function() {
renderWidgets(); renderWidgets();
}; };
/******************************************************************************/ /******************************************************************************/
var onRemoveExternalList = function(ev) { const onRemoveExternalList = function(ev) {
var liEntry = uDom(this).ancestors('[data-listkey]'), const liEntry = uDom(this).ancestors('[data-listkey]');
listKey = liEntry.attr('data-listkey'); const listKey = liEntry.attr('data-listkey');
if ( listKey ) { if ( listKey ) {
liEntry.toggleClass('toRemove'); liEntry.toggleClass('toRemove');
renderWidgets(); renderWidgets();
@ -396,10 +392,10 @@ var onRemoveExternalList = function(ev) {
/******************************************************************************/ /******************************************************************************/
var onPurgeClicked = function() { const onPurgeClicked = function(ev) {
var button = uDom(this), const button = uDom(ev.target);
liEntry = button.ancestors('[data-listkey]'), const liEntry = button.ancestors('[data-listkey]');
listKey = liEntry.attr('data-listkey'); const listKey = liEntry.attr('data-listkey');
if ( !listKey ) { return; } if ( !listKey ) { return; }
messaging.send('dashboard', { what: 'purgeCache', assetKey: listKey }); 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 // Cosmetic filtering switch
messaging.send('dashboard', { messaging.send('dashboard', {
what: 'userSettings', what: 'userSettings',
@ -433,28 +429,24 @@ var selectFilterLists = function(callback) {
}); });
// Filter lists to select // Filter lists to select
var toSelect = [], const toSelect = [];
liEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'), let liEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)');
i = liEntries.length, for ( const liEntry of liEntries ) {
liEntry;
while ( i-- ) {
liEntry = liEntries[i];
if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) { if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
toSelect.push(liEntry.getAttribute('data-listkey')); toSelect.push(liEntry.getAttribute('data-listkey'));
} }
} }
// External filter lists to remove // External filter lists to remove
var toRemove = []; const toRemove = [];
liEntries = document.querySelectorAll('#lists .listEntry.toRemove[data-listkey]'); liEntries = document.querySelectorAll('#lists .listEntry.toRemove[data-listkey]');
i = liEntries.length; for ( const liEntry of liEntries ) {
while ( i-- ) { toRemove.push(liEntry.getAttribute('data-listkey'));
toRemove.push(liEntries[i].getAttribute('data-listkey'));
} }
// External filter lists to import // External filter lists to import
var externalListsElem = document.getElementById('externalLists'), const externalListsElem = document.getElementById('externalLists');
toImport = externalListsElem.value.trim(); const toImport = externalListsElem.value.trim();
externalListsElem.value = ''; externalListsElem.value = '';
uDom.nodeFromId('importLists').checked = false; uDom.nodeFromId('importLists').checked = false;
@ -473,30 +465,28 @@ var selectFilterLists = function(callback) {
/******************************************************************************/ /******************************************************************************/
var buttonApplyHandler = function() { const buttonApplyHandler = function() {
uDom('#buttonApply').removeClass('enabled'); uDom('#buttonApply').removeClass('enabled');
var onSelectionDone = function() { selectFilterLists(( ) => {
messaging.send('dashboard', { what: 'reloadAllFilters' }); messaging.send('dashboard', { what: 'reloadAllFilters' });
}; });
selectFilterLists(onSelectionDone);
renderWidgets(); renderWidgets();
}; };
/******************************************************************************/ /******************************************************************************/
var buttonUpdateHandler = function() { const buttonUpdateHandler = function() {
var onSelectionDone = function() { selectFilterLists(( ) => {
document.body.classList.add('updating'); document.body.classList.add('updating');
messaging.send('dashboard', { what: 'forceUpdateAssets' }); messaging.send('dashboard', { what: 'forceUpdateAssets' });
renderWidgets(); renderWidgets();
}; });
selectFilterLists(onSelectionDone);
renderWidgets(); renderWidgets();
}; };
/******************************************************************************/ /******************************************************************************/
var buttonPurgeAllHandler = function(ev) { const buttonPurgeAllHandler = function(ev) {
uDom('#buttonPurgeAll').removeClass('enabled'); uDom('#buttonPurgeAll').removeClass('enabled');
messaging.send( messaging.send(
'dashboard', 'dashboard',
@ -504,13 +494,15 @@ var buttonPurgeAllHandler = function(ev) {
what: 'purgeAllCaches', what: 'purgeAllCaches',
hard: ev.ctrlKey && ev.shiftKey hard: ev.ctrlKey && ev.shiftKey
}, },
function() { renderFilterLists(true); } ( ) => {
renderFilterLists(true);
}
); );
}; };
/******************************************************************************/ /******************************************************************************/
var autoUpdateCheckboxChanged = function() { const autoUpdateCheckboxChanged = function() {
messaging.send( messaging.send(
'dashboard', 'dashboard',
{ {
@ -525,16 +517,16 @@ var autoUpdateCheckboxChanged = function() {
// Collapsing of unused lists. // Collapsing of unused lists.
var mustHideUnusedLists = function(which) { const mustHideUnusedLists = function(which) {
var hideAll = hideUnusedSet.has('*'); const hideAll = hideUnusedSet.has('*');
if ( which === '*' ) { return hideAll; } if ( which === '*' ) { return hideAll; }
return hideUnusedSet.has(which) !== hideAll; return hideUnusedSet.has(which) !== hideAll;
}; };
var toggleHideUnusedLists = function(which) { const toggleHideUnusedLists = function(which) {
var groupSelector, const doesHideAll = hideUnusedSet.has('*');
doesHideAll = hideUnusedSet.has('*'), let groupSelector;
mustHide; let mustHide;
if ( which === '*' ) { if ( which === '*' ) {
mustHide = doesHideAll === false; mustHide = doesHideAll === false;
groupSelector = ''; groupSelector = '';
@ -545,7 +537,7 @@ var toggleHideUnusedLists = function(which) {
document.body.classList.toggle('hideUnused', mustHide); document.body.classList.toggle('hideUnused', mustHide);
uDom('.groupEntry[data-groupkey]').toggleClass('hideUnused', mustHide); uDom('.groupEntry[data-groupkey]').toggleClass('hideUnused', mustHide);
} else { } else {
var doesHide = hideUnusedSet.has(which); const doesHide = hideUnusedSet.has(which);
if ( doesHide ) { if ( doesHide ) {
hideUnusedSet.delete(which); hideUnusedSet.delete(which);
} else { } else {
@ -564,7 +556,7 @@ var toggleHideUnusedLists = function(which) {
); );
}; };
var revealHiddenUsedLists = function() { const revealHiddenUsedLists = function() {
uDom('#lists .listEntry.unused > input[type="checkbox"]:checked') uDom('#lists .listEntry.unused > input[type="checkbox"]:checked')
.ancestors('.listEntry[data-listkey]') .ancestors('.listEntry[data-listkey]')
.removeClass('unused'); .removeClass('unused');
@ -582,10 +574,11 @@ uDom('#lists').on('click', '.groupEntry[data-groupkey] > .geDetails', function(e
); );
}); });
(function() { // Initialize from saved state.
var aa; {
let aa;
try { try {
var json = vAPI.localStorage.getItem('hideUnusedFilterLists'); const json = vAPI.localStorage.getItem('hideUnusedFilterLists');
if ( json !== null ) { if ( json !== null ) {
aa = JSON.parse(json); aa = JSON.parse(json);
} }
@ -595,35 +588,33 @@ uDom('#lists').on('click', '.groupEntry[data-groupkey] > .geDetails', function(e
aa = [ '*' ]; aa = [ '*' ];
} }
hideUnusedSet = new Set(aa); hideUnusedSet = new Set(aa);
})(); }
/******************************************************************************/ /******************************************************************************/
// Cloud-related. // Cloud-related.
var toCloudData = function() { const toCloudData = function() {
var bin = { const bin = {
parseCosmeticFilters: uDom.nodeFromId('parseCosmeticFilters').checked, parseCosmeticFilters: uDom.nodeFromId('parseCosmeticFilters').checked,
ignoreGenericCosmeticFilters: uDom.nodeFromId('ignoreGenericCosmeticFilters').checked, ignoreGenericCosmeticFilters: uDom.nodeFromId('ignoreGenericCosmeticFilters').checked,
selectedLists: [] selectedLists: []
}; };
var liEntries = uDom('#lists .listEntry'), liEntry; const liEntries = document.querySelectorAll('#lists .listEntry');
var i = liEntries.length; for ( const liEntry of liEntries ) {
while ( i-- ) { if ( liEntry.querySelector('input').checked ) {
liEntry = liEntries.at(i); bin.selectedLists.push(liEntry.getAttribute('data-listkey'));
if ( liEntry.descendants('input').prop('checked') ) {
bin.selectedLists.push(liEntry.attr('data-listkey'));
} }
} }
return bin; return bin;
}; };
var fromCloudData = function(data, append) { const fromCloudData = function(data, append) {
if ( typeof data !== 'object' || data === null ) { return; } if ( typeof data !== 'object' || data === null ) { return; }
var elem, checked; let elem, checked;
elem = uDom.nodeFromId('parseCosmeticFilters'); elem = uDom.nodeFromId('parseCosmeticFilters');
checked = data.parseCosmeticFilters === true || append && elem.checked; checked = data.parseCosmeticFilters === true || append && elem.checked;
@ -633,21 +624,20 @@ var fromCloudData = function(data, append) {
checked = data.ignoreGenericCosmeticFilters === true || append && elem.checked; checked = data.ignoreGenericCosmeticFilters === true || append && elem.checked;
elem.checked = listDetails.ignoreGenericCosmeticFilters = checked; elem.checked = listDetails.ignoreGenericCosmeticFilters = checked;
var selectedSet = new Set(data.selectedLists), const selectedSet = new Set(data.selectedLists);
listEntries = uDom('#lists .listEntry'), const listEntries = uDom('#lists .listEntry');
listEntry, listKey; for ( let i = 0, n = listEntries.length; i < n; i++ ) {
for ( var i = 0, n = listEntries.length; i < n; i++ ) { const listEntry = listEntries.at(i);
listEntry = listEntries.at(i); const listKey = listEntry.attr('data-listkey');
listKey = listEntry.attr('data-listkey'); const hasListKey = selectedSet.has(listKey);
var hasListKey = selectedSet.has(listKey);
selectedSet.delete(listKey); selectedSet.delete(listKey);
var input = listEntry.descendants('input').first(); const input = listEntry.descendants('input').first();
if ( append && input.prop('checked') ) { continue; } if ( append && input.prop('checked') ) { continue; }
input.prop('checked', hasListKey); input.prop('checked', hasListKey);
} }
// If there are URL-like list keys left in the selected set, import them. // 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 ) { if ( reValidExternalList.test(listKey) === false ) {
selectedSet.delete(listKey); selectedSet.delete(listKey);
} }
@ -672,6 +662,12 @@ self.cloud.onPull = fromCloudData;
/******************************************************************************/ /******************************************************************************/
self.hasUnsavedData = function() {
return hashFromCurrentFromSettings() !== filteringSettingsHash;
};
/******************************************************************************/
uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged); uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
uDom('#parseCosmeticFilters').on('change', onFilteringSettingsChanged); uDom('#parseCosmeticFilters').on('change', onFilteringSettingsChanged);
uDom('#ignoreGenericCosmeticFilters').on('change', onFilteringSettingsChanged); uDom('#ignoreGenericCosmeticFilters').on('change', onFilteringSettingsChanged);

View File

@ -25,48 +25,93 @@
/******************************************************************************/ /******************************************************************************/
(function() { (( ) => {
/******************************************************************************/ /******************************************************************************/
const resizeFrame = function() { const resizeFrame = function() {
let navRect = document.getElementById('dashboard-nav').getBoundingClientRect(); const navRect = document.getElementById('dashboard-nav')
let viewRect = document.documentElement.getBoundingClientRect(); .getBoundingClientRect();
const viewRect = document.documentElement.getBoundingClientRect();
document.getElementById('iframe').style.setProperty( document.getElementById('iframe').style.setProperty(
'height', 'height',
(viewRect.height - navRect.height) + 'px' (viewRect.height - navRect.height) + 'px'
); );
}; };
const loadDashboardPanel = function() { const discardUnsavedData = function(synchronous = false) {
let pane = window.location.hash.slice(1); 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 === '' ) { if ( pane === '' ) {
pane = vAPI.localStorage.getItem('dashboardLastVisitedPane'); pane = vAPI.localStorage.getItem('dashboardLastVisitedPane');
if ( pane === null ) { if ( pane === null ) {
pane = 'settings.html'; 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; } if ( !tabButton || tabButton.hasClass('selected') ) { return; }
uDom('.tabButton.selected').toggleClass('selected', false); const loadPane = ( ) => {
uDom('iframe').attr('src', pane); self.location.replace(`#${pane}`);
tabButton.toggleClass('selected', true); 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) { const onTabClickHandler = function(ev) {
let url = window.location.href, loadDashboardPanel(ev.target.hash.slice(1));
pos = url.indexOf('#'); ev.preventDefault();
if ( pos !== -1 ) {
url = url.slice(0, pos);
}
url += this.hash;
if ( url !== window.location.href ) {
window.location.replace(url);
loadDashboardPanel();
}
e.preventDefault();
}; };
// https://github.com/uBlockOrigin/uBlock-issues/issues/106 // https://github.com/uBlockOrigin/uBlock-issues/issues/106
@ -80,6 +125,13 @@ loadDashboardPanel();
window.addEventListener('resize', resizeFrame); window.addEventListener('resize', resizeFrame);
uDom('.tabButton').on('click', onTabClickHandler); 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 = '';
});
/******************************************************************************/ /******************************************************************************/
})(); })();

View File

@ -25,7 +25,7 @@
/******************************************************************************/ /******************************************************************************/
(function() { (( ) => {
/******************************************************************************/ /******************************************************************************/
@ -329,7 +329,7 @@ const onFilterChanged = (function() {
overlay = null, overlay = null,
last = ''; last = '';
let process = function() { const process = function() {
timer = undefined; timer = undefined;
if ( mergeView.editor().isClean(cleanEditToken) === false ) { return; } if ( mergeView.editor().isClean(cleanEditToken) === false ) { return; }
let filter = uDom.nodeFromSelector('#ruleFilter input').value; let filter = uDom.nodeFromSelector('#ruleFilter input').value;
@ -359,7 +359,7 @@ const onFilterChanged = (function() {
const onTextChanged = (function() { const onTextChanged = (function() {
let timer; let timer;
let process = function(now) { const process = function(now) {
timer = undefined; timer = undefined;
const diff = document.getElementById('diff'); const diff = document.getElementById('diff');
let isClean = mergeView.editor().isClean(cleanEditToken); 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); messaging.send('dashboard', { what: 'getRules' }, renderRules);
// Handle user interaction // Handle user interaction

View File

@ -238,6 +238,12 @@ self.cloud.onPull = setCloudData;
/******************************************************************************/ /******************************************************************************/
self.hasUnsavedData = function() {
return cmEditor.getValue().trim() !== cachedWhitelist;
};
/******************************************************************************/
uDom('#importWhitelistFromFile').on('click', startImportFilePicker); uDom('#importWhitelistFromFile').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile); uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile);