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