[mv3] Code review re. re-worked dashboard

Related commit:
ae4754415c

Fine-tuned visuals; fixed sublist quirks related to admin-selected
lists.
This commit is contained in:
Raymond Hill 2024-11-18 10:16:01 -05:00
parent ae4754415c
commit d7c6b41992
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
6 changed files with 64 additions and 46 deletions

View file

@ -230,5 +230,9 @@
"showBlockedCountLabel": {
"message": "Show the number of blocked requests on the toolbar icon",
"description": "Label for a checkbox in the options page"
},
"findListsPlaceholder": {
"message": "Find lists",
"description": "Placeholder for the input field used to find lists"
}
}

View file

@ -3,6 +3,7 @@ body {
box-sizing: border-box;
display: flex;
flex-direction: column;
max-height: 100vh;
padding: 0 var(--default-gap-xxsmall);
}
body > * {

View file

@ -7,9 +7,6 @@
flex-wrap: wrap;
overflow-x: hidden;
padding: 0;
position: sticky;
top: 0;
z-index: 100;
}
.tabButton {
background-color: transparent;
@ -43,6 +40,8 @@ body[data-pane="about"] #dashboard-nav .tabButton[data-pane="about"] {
body > section {
display: none;
overflow: auto;
padding-bottom: 8rem;
}
body[data-pane="settings"] > section[data-pane="settings"],
body[data-pane="rulesets"] > section[data-pane="rulesets"],

View file

@ -1,12 +1,3 @@
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
legend {
color: var(--ink-3);
font-size: var(--font-size-smaller);
padding: var(--default-gap-xxsmall);
}
body .firstRun {
display: none;
}
@ -78,8 +69,19 @@ h3[data-i18n="filteringMode0Name"]::first-letter {
width: 100%;
}
#lists {
padding-block-end: 8rem;
section[data-pane="rulesets"] > div:first-of-type {
background-color: var(--surface-1);
flex-shrink: 0;
padding: 1em 0;
position: sticky;
top: 0;
z-index: 10;
}
section[data-pane="rulesets"] > div:first-of-type > p:first-of-type {
margin-top: 0;
}
section[data-pane="rulesets"] > div:first-of-type > p:last-of-type {
margin-bottom: 0;
}
.listEntry {
display: flex;
@ -112,7 +114,7 @@ h3[data-i18n="filteringMode0Name"]::first-letter {
.listEntry.hideUnused > .listEntries > .listEntry:not(.isDefault):has(> .detailbar input:not(:checked)) {
display: none;
}
.listEntry.fromAdmin:has(input[disabled]:not(:checked)) {
.listEntry.fromAdmin:has(> .detailbar input[disabled]:not(:checked)) {
display: none;
}
.listEntry > * {

View file

@ -101,8 +101,8 @@
<section data-pane="rulesets">
<div>
<p id="listsOfBlockedHostsPrompt"></p>
<p><input id="findInLists" type="search" spellcheck="false" placeholder="findListsPlaceholder" /></p>
</div>
<div class="searchfield"><input type="search" spellcheck="false" placeholder="" /><span class="fa-icon">search</span></div>
<div id="lists"></div>
</section>
<!-- -------- -->

View file

@ -36,7 +36,9 @@ function renderNumber(value) {
return value.toLocaleString();
}
function renderRuleCounts() {
/******************************************************************************/
function renderTotalRuleCounts() {
let rulesetCount = 0;
let filterCount = 0;
let ruleCount = 0;
@ -49,8 +51,8 @@ function renderRuleCounts() {
filterCount += stats.filterCount;
}
dom.text('#listsOfBlockedHostsPrompt', i18n$('perRulesetStats')
.replace('{{ruleCount}}', ruleCount.toLocaleString())
.replace('{{filterCount}}', filterCount.toLocaleString())
.replace('{{ruleCount}}', renderNumber(ruleCount))
.replace('{{filterCount}}', renderNumber(filterCount))
);
dom.cl.toggle(dom.body, 'noMoreRuleset',
@ -62,16 +64,26 @@ function renderRuleCounts() {
function updateNodes(listEntries) {
listEntries = listEntries || qs$('#lists');
const sublistSelector = '.listEntry[data-rulesetid] > .detailbar input';
const checkedSublistSelector = `${sublistSelector}:checked`;
const adminSublistSelector = '.listEntry.fromAdmin[data-rulesetid] > .detailbar input';
for ( const listEntry of qsa$(listEntries, '.listEntry[data-nodeid]') ) {
const totalCount = qsa$(listEntry, '.listEntry[data-rulesetid] input').length;
const checkedCount = qsa$(listEntry, '.listEntry[data-rulesetid] input:checked').length;
dom.text(qs$(listEntry, '.detailbar .count'), `${checkedCount}/${totalCount}`);
const checkbox = qs$(listEntry, ':scope > .detailbar .checkbox');
if ( checkbox === null ) { continue; }
dom.prop(qs$(checkbox, 'input'), 'checked', checkedCount !== 0);
dom.cl.toggle(checkbox, 'partial',
const countElem = qs$(listEntry, ':scope > .detailbar .count');
if ( countElem === null ) { continue; }
const totalCount = qsa$(listEntry, sublistSelector).length;
const checkedCount = qsa$(listEntry, checkedSublistSelector).length;
dom.text(countElem, `${checkedCount}/${totalCount}`);
const checkboxElem = qs$(listEntry, ':scope > .detailbar .checkbox');
if ( checkboxElem === null ) { continue; }
const checkboxInput = qs$(checkboxElem, 'input');
dom.prop(checkboxInput, 'checked', checkedCount !== 0);
dom.cl.toggle(checkboxElem, 'partial',
checkedCount !== 0 && checkedCount !== totalCount
);
const adminCount = qsa$(listEntry, adminSublistSelector).length;
const fromAdmin = adminCount === totalCount;
dom.cl.toggle(listEntry, 'fromAdmin', fromAdmin);
dom.attr(checkboxInput, 'disabled', fromAdmin ? '' : null);
}
}
@ -124,6 +136,7 @@ export function renderFilterLists(rulesetData) {
}
dom.cl.toggle(listEntry, 'isDefault', ruleset.id === 'default');
const stats = rulesetStats(ruleset.id);
if ( stats === undefined ) { return; }
listEntry.title = listStatsTemplate
.replace('{{ruleCount}}', renderNumber(stats.ruleCount))
.replace('{{filterCount}}', renderNumber(stats.filterCount));
@ -135,7 +148,6 @@ export function renderFilterLists(rulesetData) {
'disabled',
disabled ? '' : null
);
return listEntry;
};
// Update already rendered DOM lists
@ -146,7 +158,7 @@ export function renderFilterLists(rulesetData) {
initializeListEntry(ruleset, listEntry);
}
updateNodes();
renderRuleCounts();
renderTotalRuleCounts();
return;
}
@ -229,20 +241,17 @@ export function renderFilterLists(rulesetData) {
// Build list tree
const listTree = {};
const groupNames = new Map();
for ( const [ groupKey, groupRulesets ] of groups ) {
let groupName = groupNames.get(groupKey);
if ( groupName === undefined ) {
groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
groupNames.set(groupKey, groupName);
for ( const [ nodeid, rulesets ] of groups ) {
let name = groupNames.get(nodeid);
if ( name === undefined ) {
name = i18n$(`3pGroup${nodeid.charAt(0).toUpperCase()}${nodeid.slice(1)}`);
groupNames.set(nodeid, name);
}
const groupDetails = {
name: groupName,
lists: {},
};
listTree[groupKey] = groupDetails;
for ( const ruleset of groupRulesets ) {
const details = { name, lists: {} };
listTree[nodeid] = details;
for ( const ruleset of rulesets ) {
if ( ruleset.parent !== undefined ) {
let lists = groupDetails.lists;
let lists = details.lists;
for ( const parent of ruleset.parent.split('|') ) {
if ( lists[parent] === undefined ) {
lists[parent] = { name: parent, lists: {} };
@ -251,11 +260,11 @@ export function renderFilterLists(rulesetData) {
}
lists[ruleset.id] = ruleset;
} else {
groupDetails.lists[ruleset.id] = ruleset;
details.lists[ruleset.id] = ruleset;
}
}
}
// Move lonely sublist to list level
// Replace composite list with only one sublist with sublist itself
const promoteLonelySublist = (parent, depth = 0) => {
if ( Boolean(parent.lists) === false ) { return parent; }
const childKeys = Object.keys(parent.lists);
@ -279,7 +288,7 @@ export function renderFilterLists(rulesetData) {
dom.clear('#lists');
qs$('#lists').append(listEntries);
renderRuleCounts();
renderTotalRuleCounts();
}
/******************************************************************************/
@ -337,11 +346,12 @@ localRead('hideUnusedFilterLists').then(value => {
/******************************************************************************/
const searchFilterLists = ( ) => {
const pattern = dom.prop('.searchfield input', 'value') || '';
const pattern = dom.prop('#findInLists', 'value') || '';
dom.cl.toggle('#lists', 'searchMode', pattern !== '');
if ( pattern === '' ) { return; }
const re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
for ( const listEntry of qsa$('#lists [data-role="leaf"]') ) {
if ( dom.cl.has(listEntry, 'fromAdmin') ) { continue; }
const rulesetid = listEntry.dataset.rulesetid;
const rulesetDetails = rulesetMap.get(rulesetid);
if ( rulesetDetails === undefined ) { continue; }
@ -365,7 +375,7 @@ const searchFilterLists = ( ) => {
const perListHaystack = new WeakMap();
dom.on('.searchfield input', 'input', searchFilterLists);
dom.on('#findInLists', 'input', searchFilterLists);
/******************************************************************************/
@ -379,6 +389,8 @@ async function applyEnabledRulesets() {
enabledRulesets.push(rulesetid);
}
if ( enabledRulesets.length === 0 ) { return; }
await sendMessage({
what: 'applyRulesets',
enabledRulesets,
@ -396,7 +408,7 @@ dom.on('#lists', 'change', '.listEntry input[type="checkbox"]', ev => {
input.checked = checkAll;
}
}
renderRuleCounts();
updateNodes();
renderTotalRuleCounts();
applyEnabledRulesets();
});