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",
"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"

View File

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

View File

@ -22,6 +22,14 @@
--><a class="tabButton" href="#about.html" data-i18n="aboutPageName"></a>
</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>

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

View File

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

View File

@ -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 = '';
});
/******************************************************************************/
})();

View File

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

View File

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