Commit 0d21dd9d authored by Andrea Giammarchi's avatar Andrea Giammarchi Committed by Winsley

Issue 6875 - IOFilterTable Component

parent 64651b9f
......@@ -17,6 +17,7 @@
@import "./io-checkbox.scss";
@import "./io-toggle.scss";
@import "./io-scrollbar.scss";
/*
The component depends on its style and it will look for the
......@@ -24,6 +25,7 @@
The property is also named like the component on purpose,
to be sure its an own property, not something inherited.
*/
io-filter-list
{
--io-filter-list: ready;
......@@ -92,9 +94,16 @@ io-filter-list td
io-filter-list th
{
display: flex;
padding: 8px;
cursor: pointer;
transition: background 0.2s ease-in;
align-items: center;
}
io-filter-list th:not([data-column="rule"])
{
justify-content: center;
}
io-filter-list th:hover
......@@ -169,18 +178,13 @@ io-filter-list td[data-column="warning"] img
io-filter-list thead th:not([data-column="selected"])::after
{
display: inline-block;
position: absolute;
width: 24px;
padding: 4px;
opacity: 0.3;
font-size: 0.7em;
line-height: 1rem;
}
io-filter-list thead th[data-column="status"]::after
{
position: initial;
}
io-filter-list thead th:not([data-column="selected"])::after
{
content: "▲";
......
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
* Copyright (C) 2006-present eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
@import "./io-filter-search.scss";
@import "./io-filter-list.scss";
io-filter-table
{
display: flex;
flex-direction: column;
min-width: 700px;
}
io-filter-table > io-filter-search
{
z-index: 1;
}
io-filter-table > io-filter-search > input
{
padding-right: 24px;
padding-left: 24px;
}
io-filter-table > io-filter-list > table
{
border-top: 0;
}
io-filter-table .footer
{
display: flex;
visibility: hidden;
flex-direction: row;
margin-top: 16px;
align-items: center;
}
io-filter-table .footer.visible
{
visibility: visible;
}
io-filter-table .footer button
{
display: inline-flex;
padding: 4px;
font-weight: 600;
justify-content: center;
align-content: center;
}
io-filter-table .footer button:not(:last-child)
{
text-transform: uppercase;
}
io-filter-table .footer button:not(:first-child)
{
margin: auto 16px;
}
io-filter-table .footer .delete
{
border: 2px solid #db394b;
color: #db394b;
}
io-filter-table .footer .copy
{
border: 2px solid #337ba2;
color: #337ba2;
}
io-filter-table .footer button:not(.error)::before
{
display: inline-block;
width: 1em;
height: 1em;
background-repeat: no-repeat;
background-size: contain;
content: "";
transform: translateY(0.1em);
}
io-filter-table .footer .delete::before
{
background-image: url(icons/trash.svg?error#error);
}
io-filter-table .footer .copy::before
{
background-image: url(icons/copy.svg);
}
io-filter-table .footer .error
{
border: 0;
color: #db394b;
text-transform: none;
flex-grow: 1;
}
io-filter-table .footer .error:not([data-filter])
{
outline: none;
cursor: default;
}
html:not([dir="rtl"]) io-filter-table .footer .error
{
text-align: left;
}
html:not([dir="rtl"]) io-filter-table .footer .delete::before,
html:not([dir="rtl"]) io-filter-table .footer .copy::before
{
margin-right: 4px;
}
html[dir="rtl"] io-filter-table .footer .error
{
text-align: right;
}
html[dir="rtl"] io-filter-table .footer .delete::before,
html[dir="rtl"] io-filter-table .footer .copy::before
{
margin-left: 4px;
}
......@@ -146,7 +146,7 @@ class IOFilterList extends IOElement
const {position, range} = this.scrollbar;
const {scrollHeight} = this.state;
this.setState({
scrollTop: scrollHeight * position / range
scrollTop: getScrollTop(scrollHeight * position / range)
});
});
this.addEventListener(
......@@ -163,14 +163,10 @@ class IOFilterList extends IOElement
}
const {scrollHeight, scrollTop} = this.state;
this.setState({
scrollTop: Math.max(
0,
Math.min(scrollHeight, scrollTop + event.deltaY)
)
scrollTop: getScrollTop(scrollTop + event.deltaY, scrollHeight)
});
// update the scrollbar position accordingly
const {range} = this.scrollbar;
this.scrollbar.position = this.state.scrollTop * range / scrollHeight;
updateScrollbarPosition.call(this);
},
{passive: false}
);
......@@ -186,12 +182,12 @@ class IOFilterList extends IOElement
if (index < 0)
console.error("invalid filter", row);
else
{
this.setState({
scrollTop: Math.min(
scrollHeight,
index * rowHeight
)
scrollTop: getScrollTop(index * rowHeight, scrollHeight)
});
updateScrollbarPosition.call(this);
}
}
onload()
......@@ -436,9 +432,12 @@ class IOFilterList extends IOElement
list[count++] = i < length ? this.state.filters[i++] : null;
}
}
const {length} = this.filters;
this.html`<table cellpadding="0" cellspacing="0">
<thead onclick="${this}" data-call="onheaderclick">
<th data-column="selected"><io-checkbox /></th>
<th data-column="selected">
<io-checkbox checked=${!!length && this.selected.size === length} />
</th>
<th data-column="status"></th>
<th data-column="rule">${{i18n: "options_filter_list_rule"}}</th>
<th data-column="warning">${
......@@ -575,7 +574,7 @@ function focusTheNextFilterIfAny(tr)
if (next.offsetTop > viewHeight)
{
this.setState({
scrollTop: scrollTop + rowHeight
scrollTop: getScrollTop(scrollTop + rowHeight)
});
}
// focus its content field
......@@ -599,6 +598,17 @@ function getFilter(event)
return div.data;
}
// ensure the number is always between 0 and a positive number
// specially handy when filters are erased and the viewHeight
// is higher than scrollHeight and other cases too
function getScrollTop(value, scrollHeight)
{
return Math.max(
0,
Math.min(scrollHeight || Infinity, value)
);
}
function getWarning(filter)
{
if (filter.reason)
......@@ -671,3 +681,10 @@ function setupPort()
}
});
}
function updateScrollbarPosition()
{
const {scrollbar, state} = this;
const {scrollHeight, scrollTop} = state;
scrollbar.position = scrollTop * scrollbar.range / scrollHeight;
}
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
* Copyright (C) 2006-present eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const IOElement = require("./io-element");
const IOFilterList = require("./io-filter-list");
const IOFilterSearch = require("./io-filter-search");
const {$, clipboard} = require("./dom");
const {bind, wire} = IOElement;
// io-filter-table is a basic controller
// used to relate the search and the list
class IOFilterTable extends IOElement
{
static get booleanAttributes()
{
return ["disabled"];
}
static get observedAttributes()
{
return ["match"];
}
get defaultState()
{
return {filters: [], match: -1, ready: false};
}
created()
{
this._showing = null;
this.search = this.appendChild(new IOFilterSearch());
this.search.addEventListener(
"filter:add",
event => this.onFilterAdd(event)
);
this.search.addEventListener(
"filter:match",
event => this.onFilterMatch(event)
);
this.list = this.appendChild(new IOFilterList());
this.footer = this.appendChild(wire()`<div class="footer" />`);
this.addEventListener("click", this);
this.addEventListener("error", this);
this.setState({ready: true});
}
attributeChangedCallback(name, prev, value)
{
if (name === "match")
this.setState({match: value}, false);
this.render();
}
get filters()
{
return this.state.filters;
}
set filters(value)
{
this.setState({filters: value});
}
get match()
{
return this.state.match;
}
set match(value)
{
this.setState({match: value});
}
onclick(event)
{
if (event.target.closest("io-checkbox"))
{
this.footer.classList.toggle("visible", !!this.list.selected.size);
}
}
onerror(event)
{
const {filter, errors} = event.detail;
const node = this.querySelector(".footer .error");
if (filter)
node.dataset.filter = filter;
else
delete node.dataset.filter;
bind(node)`${
errors ?
errors.join("\n") :
{i18n: "filter_action_failed"}
}}`;
}
onerrorclick(event)
{
const {filter} = event.currentTarget.dataset;
if (filter)
{
this.list.scrollTo(filter);
}
}
onfooterclick(event)
{
const {classList} = event.currentTarget;
switch (true)
{
case classList.contains("delete"):
const resolve = [];
for (const filter of this.list.selected)
{
this.list.selected.delete(filter);
this.filters.splice(this.filters.indexOf(filter), 1);
resolve.push(browser.runtime.sendMessage({
type: "filters.remove",
text: filter.text
}));
}
Promise.all(resolve).then(
() => updateList(this.list),
(errors) => this.onerror({detail: {errors}})
);
break;
case classList.contains("copy"):
const filters = [];
for (const filter of this.list.selected)
{
filters.push(filter.text);
}
clipboard.copy(filters.join("\n"));
break;
}
}
onFilterAdd(event)
{
const unknown = new WeakSet();
const filters = event.detail
.split(/(?:\r\n|\n)/)
.map(text =>
{
let value = this.filters.find(
filter => filter.text === text
);
if (!value)
{
value = {text};
unknown.add(value);
}
return value;
});
browser.runtime.sendMessage({
type: "filters.importRaw",
text: filters.map(filter => filter.text).join("\n")
})
.then(errors =>
{
if (!errors.length)
{
for (const filter of filters)
{
if (!unknown.has(filter))
this.filters.splice(this.filters.indexOf(filter), 1);
this.filters.unshift(filter);
}
updateList(this.list);
this.list.scrollTo(this.filters[0]);
this.search.value = "";
}
else
{
this.onerror({detail: {errors}});
}
});
}
onFilterMatch(event)
{
this.list.scrollTo(event.detail.filter);
}
render()
{
const {disabled} = this;
const {filters, match, ready} = this.state;
if (!ready)
return;
// simply update inner components
// no need to render any html in here
this.search.disabled = disabled;
this.search.filters = filters;
this.search.match = match;
this.list.disabled = disabled;
this.list.filters = filters;
this.renderFooter();
}
renderFooter()
{
bind(this.footer)`
<button
class="delete"
onclick="${this}"
data-call="onfooterclick"
>${{i18n: "delete"}}</button>
<button
class="copy"
onclick="${this}"
data-call="onfooterclick"
>${{i18n: "copy_selected"}}</button>
<button
class="error"
onclick="${this}"
data-call="onerrorclick"
></button>
`;
}
}
IOFilterTable.define("io-filter-table");
function updateList(list)
{
list.render();
list.updateScrollbar();
}
......@@ -7,6 +7,14 @@
"description": "Cancel button label",
"message": "Cancel"
},
"copy_selected": {
"description": "Copy selected items button label",
"message": "Copy selected"
},
"delete": {
"description": "Delete button label",
"message": "Delete"
},
"common_feature_anti_adblock_title": {
"description": "Feature title for anti-adblock blocking filter list",
"message": "Hide Adblock Warning Messages"
......@@ -26,5 +34,9 @@
"common_notification_hide": {
"description": "Hidden text attached to the close button for screen readers of Notification",
"message": "Close notification"
},
"filter_action_failed": {
"description": "One or more filters could not be saved or updated.",
"message": "Something went wrong. Please try again."
}
}
......@@ -95,6 +95,7 @@
},
"io-filter-search": "$ create.test.component io-filter-search",
"io-filter-list": "$ create.test.component io-filter-list",
"io-filter-table": "$ create.test.component io-filter-table",
"io-highlighter": {
"css": [
"$ create.test.css io-highlighter",
......
<?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="17px" viewBox="0 0 14 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="old-Settings" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Old-Advanced-tab" transform="translate(-396.000000, -1323.000000)" fill="#077CA6" fill-rule="nonzero">
<g id="Copy-btn" transform="translate(380.000000, 1314.000000)">
<g id="copy-content" transform="translate(16.000000, 9.000000)">
<path d="M11.0035088,0 L1.57192982,0 C0.707368421,0 0,0.736363636 0,1.63636364 L0,13.0909091 L0.975671029,13.0909091 L0.975671029,1.01554355 L11.0035088,1.01554355 L11.0035088,0 Z M12.4280702,2.27272727 L3.78245614,2.27272727 C2.91789474,2.27272727 2.21052632,3.00909091 2.21052632,3.90909091 L2.21052632,15.3636364 C2.21052632,16.2636364 2.91789474,17 3.78245614,17 L12.4280702,17 C13.2926316,17 14,16.2636364 14,15.3636364 L14,3.90909091 C14,3.00909091 13.2926316,2.27272727 12.4280702,2.27272727 Z M12.148538,16.3181818 L4.0619883,16.3181818 C3.50970355,16.3181818 3.0619883,15.8704666 3.0619883,15.3181818 L3.0619883,3.95454545 C3.0619883,3.4022607 3.50970355,2.95454545 4.0619883,2.95454545 L12.148538,2.95454545 C12.7008228,2.95454545 13.148538,3.4022607 13.148538,3.95454545 L13.148538,15.3181818 C13.148538,15.8704666 12.7008228,16.3181818 12.148538,16.3181818 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
......@@ -12,4 +12,12 @@
<rect x="15.96" y="16" width="2" height="22" fill="#5CBCE1"/>
<path d="M44,7H33V5a5,5,0,0,0-5-5H20a5,5,0,0,0-5,5V7H4V9H6.68V39a9,9,0,0,0,9,9H32.23a9,9,0,0,0,9-9V9H44ZM17,5a3,3,0,0,1,3-3h8a3,3,0,0,1,3,3V7H17ZM39.23,39a7,7,0,0,1-7,7H15.68a7,7,0,0,1-7-7V9H39.23Z" fill="#5CBCE1"/>
</g>
<view id="error" viewBox="0 96 48 48"/>
<g transform="translate(0, 96)">
<rect x="22.96" y="16" width="2" height="22" fill="#DB394B"/>
<rect x="29.96" y="16" width="2" height="22" fill="#DB394B"/>
<rect x="15.96" y="16" width="2" height="22" fill="#DB394B"/>
<path d="M44,7H33V5a5,5,0,0,0-5-5H20a5,5,0,0,0-5,5V7H4V9H6.68V39a9,9,0,0,0,9,9H32.23a9,9,0,0,0,9-9V9H44ZM17,5a3,3,0,0,1,3-3h8a3,3,0,0,1,3,3V7H17ZM39.23,39a7,7,0,0,1-7,7H15.68a7,7,0,0,1-7-7V9H39.23Z" fill="#DB394B"/>
</g>
</svg>
\ No newline at end of file
<!DOCTYPE html>
<!--
- This file is part of Adblock Plus <https://adblockplus.org/>,
- Copyright (C) 2006-present eyeo GmbH
-
- Adblock Plus is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License version 3 as
- published by the Free Software Foundation.
-
- Adblock Plus is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
-->
<html>
<head>
<meta charset="utf-8">
<title>Test io-filter-table.js</title>
<link rel="stylesheet" href="../skin/common.css">
<link rel="stylesheet" href="../skin/fonts.css">
<link rel="stylesheet" href="../skin/desktop-options.css">
<link rel="stylesheet" href="../smoke/css/io-filter-table.css">
<script defer src="../polyfill.js"></script>
<script defer src="../ext/common.js"></script>
<script defer src="../ext/content.js"></script>
<script defer src="../common.js"></script>
<script defer src="../i18n.js"></script>
<script defer src="./io-filter-table.js"></script>
</head>
<body>
<div style="width:500px;">
<io-filter-table></io-filter-table>
</div>
</body>
</html>
"use strict";
require("../js/io-filter-table");
const length = (Math.random() * 50) >>> 0;
const ioFilterTable = document.querySelector("io-filter-table");
ioFilterTable.filters = require("./random-filter-list")(length);
{}
\ No newline at end of file
{
"copy_selected": {
"description": "Copy selected items button label",
"message": "Copy selected"
},
"delete": {
"description": "Delete button label",
"message": "Delete"
},
"filter_action_failed": {
"description": "One or more filters could not be saved or updated.",
"message": "Something went wrong. Please try again."
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment