mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
Add :remove-attr() and :remove-class() pseudo selector operators
These two new pseudo selectors are _action_ operators, and thus can only be used at the end of a selector. They both take as argument a string or regex literal. For `:remove-class()`, when the argument matches a class name, that class name is removed. For `:remove-attr()`, when the argument matches an attribute name, that attribute is removed. These operators are meant to replace `+js(remove-attr, ...)` and `+js(remove-class, ...)`, which from now on are candidate for deprecation in some future. Once the next stable release is widespread, filter authors must use these two new operators instead of their `+js()` counterparts.
This commit is contained in:
parent
e959bc2832
commit
992255e993
4 changed files with 104 additions and 73 deletions
|
|
@ -176,8 +176,8 @@ const µBlock = { // jshint ignore:line
|
||||||
|
|
||||||
// Read-only
|
// Read-only
|
||||||
systemSettings: {
|
systemSettings: {
|
||||||
compiledMagic: 48, // Increase when compiled format changes
|
compiledMagic: 49, // Increase when compiled format changes
|
||||||
selfieMagic: 48, // Increase when selfie format changes
|
selfieMagic: 49, // Increase when selfie format changes
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,6 @@ const nonVisualElements = {
|
||||||
|
|
||||||
const regexFromString = (s, exact = false) => {
|
const regexFromString = (s, exact = false) => {
|
||||||
if ( s === '' ) { return /^/; }
|
if ( s === '' ) { return /^/; }
|
||||||
if ( /^".+"$/.test(s) ) {
|
|
||||||
s = s.slice(1,-1).replace(/\\(\\|")/g, '$1');
|
|
||||||
}
|
|
||||||
const match = /^\/(.+)\/([i]?)$/.exec(s);
|
const match = /^\/(.+)\/([i]?)$/.exec(s);
|
||||||
if ( match !== null ) {
|
if ( match !== null ) {
|
||||||
return new RegExp(match[1], match[2] || undefined);
|
return new RegExp(match[1], match[2] || undefined);
|
||||||
|
|
@ -68,11 +65,7 @@ class PSelectorVoidTask extends PSelectorTask {
|
||||||
class PSelectorHasTextTask extends PSelectorTask {
|
class PSelectorHasTextTask extends PSelectorTask {
|
||||||
constructor(task) {
|
constructor(task) {
|
||||||
super();
|
super();
|
||||||
let arg0 = task[1], arg1;
|
this.needle = regexFromString(task[1]);
|
||||||
if ( Array.isArray(task[1]) ) {
|
|
||||||
arg1 = arg0[1]; arg0 = arg0[0];
|
|
||||||
}
|
|
||||||
this.needle = new RegExp(arg0, arg1);
|
|
||||||
}
|
}
|
||||||
transpose(node, output) {
|
transpose(node, output) {
|
||||||
if ( this.needle.test(node.textContent) ) {
|
if ( this.needle.test(node.textContent) ) {
|
||||||
|
|
@ -168,11 +161,7 @@ class PSelectorMatchesMediaTask extends PSelectorTask {
|
||||||
class PSelectorMatchesPathTask extends PSelectorTask {
|
class PSelectorMatchesPathTask extends PSelectorTask {
|
||||||
constructor(task) {
|
constructor(task) {
|
||||||
super();
|
super();
|
||||||
let arg0 = task[1], arg1;
|
this.needle = regexFromString(task[1]);
|
||||||
if ( Array.isArray(task[1]) ) {
|
|
||||||
arg1 = arg0[1]; arg0 = arg0[0];
|
|
||||||
}
|
|
||||||
this.needle = new RegExp(arg0, arg1);
|
|
||||||
}
|
}
|
||||||
transpose(node, output) {
|
transpose(node, output) {
|
||||||
if ( this.needle.test(self.location.pathname + self.location.search) ) {
|
if ( this.needle.test(self.location.pathname + self.location.search) ) {
|
||||||
|
|
@ -450,13 +439,13 @@ class PSelector {
|
||||||
PSelector.prototype.operatorToTaskMap = undefined;
|
PSelector.prototype.operatorToTaskMap = undefined;
|
||||||
|
|
||||||
class PSelectorRoot extends PSelector {
|
class PSelectorRoot extends PSelector {
|
||||||
constructor(o, styleToken) {
|
constructor(o) {
|
||||||
super(o);
|
super(o);
|
||||||
this.budget = 200; // I arbitrary picked a 1/5 second
|
this.budget = 200; // I arbitrary picked a 1/5 second
|
||||||
this.raw = o.raw;
|
this.raw = o.raw;
|
||||||
this.cost = 0;
|
this.cost = 0;
|
||||||
this.lastAllowanceTime = 0;
|
this.lastAllowanceTime = 0;
|
||||||
this.styleToken = styleToken;
|
this.action = o.action;
|
||||||
}
|
}
|
||||||
prime(input) {
|
prime(input) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -486,16 +475,8 @@ class ProceduralFilterer {
|
||||||
let mustCommit = false;
|
let mustCommit = false;
|
||||||
for ( const selector of selectors ) {
|
for ( const selector of selectors ) {
|
||||||
if ( this.selectors.has(selector.raw) ) { continue; }
|
if ( this.selectors.has(selector.raw) ) { continue; }
|
||||||
let style, styleToken;
|
const pselector = new PSelectorRoot(selector);
|
||||||
if ( selector.action === undefined ) {
|
this.primeProceduralSelector(pselector);
|
||||||
style = vAPI.hideStyle;
|
|
||||||
} else if ( selector.action[0] === 'style' ) {
|
|
||||||
style = selector.action[1];
|
|
||||||
}
|
|
||||||
if ( style !== undefined ) {
|
|
||||||
styleToken = this.styleTokenFromStyle(style);
|
|
||||||
}
|
|
||||||
const pselector = new PSelectorRoot(selector, styleToken);
|
|
||||||
this.selectors.set(selector.raw, pselector);
|
this.selectors.set(selector.raw, pselector);
|
||||||
addedSelectors.push(pselector);
|
addedSelectors.push(pselector);
|
||||||
mustCommit = true;
|
mustCommit = true;
|
||||||
|
|
@ -510,13 +491,22 @@ class ProceduralFilterer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This allows to perform potentially expensive initialization steps
|
||||||
|
// before the filters are ready to be applied.
|
||||||
|
primeProceduralSelector(pselector) {
|
||||||
|
if ( pselector.action === undefined ) {
|
||||||
|
this.styleTokenFromStyle(vAPI.hideStyle);
|
||||||
|
} else if ( pselector.action[0] === 'style' ) {
|
||||||
|
this.styleTokenFromStyle(pselector.action[1]);
|
||||||
|
}
|
||||||
|
return pselector;
|
||||||
|
}
|
||||||
|
|
||||||
commitNow() {
|
commitNow() {
|
||||||
if ( this.selectors.size === 0 ) { return; }
|
if ( this.selectors.size === 0 ) { return; }
|
||||||
|
|
||||||
this.mustApplySelectors = false;
|
this.mustApplySelectors = false;
|
||||||
|
|
||||||
//console.time('procedural selectors/dom layout changed');
|
|
||||||
|
|
||||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
|
||||||
// Be ready to unhide nodes which no longer matches any of
|
// Be ready to unhide nodes which no longer matches any of
|
||||||
// the procedural selectors.
|
// the procedural selectors.
|
||||||
|
|
@ -543,16 +533,15 @@ class ProceduralFilterer {
|
||||||
t0 = t1;
|
t0 = t1;
|
||||||
if ( nodes.length === 0 ) { continue; }
|
if ( nodes.length === 0 ) { continue; }
|
||||||
pselector.hit = true;
|
pselector.hit = true;
|
||||||
this.styleNodes(nodes, pselector.styleToken);
|
this.processNodes(nodes, pselector.action);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.unstyleNodes(toUnstyle);
|
this.unprocessNodes(toUnstyle);
|
||||||
//console.timeEnd('procedural selectors/dom layout changed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
styleTokenFromStyle(style) {
|
styleTokenFromStyle(style) {
|
||||||
if ( style === undefined ) { return; }
|
if ( style === undefined ) { return; }
|
||||||
let styleToken = this.styleTokenMap.get(style);
|
let styleToken = this.styleTokenMap.get(vAPI.hideStyle);
|
||||||
if ( styleToken !== undefined ) { return styleToken; }
|
if ( styleToken !== undefined ) { return styleToken; }
|
||||||
styleToken = vAPI.randomToken();
|
styleToken = vAPI.randomToken();
|
||||||
this.styleTokenMap.set(style, styleToken);
|
this.styleTokenMap.set(style, styleToken);
|
||||||
|
|
@ -563,25 +552,60 @@ class ProceduralFilterer {
|
||||||
return styleToken;
|
return styleToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
styleNodes(nodes, styleToken) {
|
processNodes(nodes, action) {
|
||||||
if ( styleToken === undefined ) {
|
const op = action && action[0] || '';
|
||||||
|
const arg = op !== '' ? action[1] : '';
|
||||||
|
switch ( op ) {
|
||||||
|
case '':
|
||||||
|
/* fall through */
|
||||||
|
case 'style': {
|
||||||
|
const styleToken = this.styleTokenFromStyle(
|
||||||
|
arg === '' ? vAPI.hideStyle : arg
|
||||||
|
);
|
||||||
for ( const node of nodes ) {
|
for ( const node of nodes ) {
|
||||||
node.textContent = '';
|
node.setAttribute(this.masterToken, '');
|
||||||
node.remove();
|
node.setAttribute(styleToken, '');
|
||||||
|
this.styledNodes.add(node);
|
||||||
}
|
}
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
for ( const node of nodes ) {
|
case 'remove': {
|
||||||
node.setAttribute(this.masterToken, '');
|
for ( const node of nodes ) {
|
||||||
node.setAttribute(styleToken, '');
|
node.remove();
|
||||||
this.styledNodes.add(node);
|
node.textContent = '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'remove-attr': {
|
||||||
|
const reAttr = regexFromString(arg, true);
|
||||||
|
for ( const node of nodes ) {
|
||||||
|
for ( const name of node.getAttributeNames() ) {
|
||||||
|
if ( reAttr.test(name) === false ) { continue; }
|
||||||
|
node.removeAttribute(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'remove-class': {
|
||||||
|
const reClass = regexFromString(arg, true);
|
||||||
|
for ( const node of nodes ) {
|
||||||
|
const cl = node.classList;
|
||||||
|
for ( const name of cl.values() ) {
|
||||||
|
if ( reClass.test(name) === false ) { continue; }
|
||||||
|
cl.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Current assumption is one style per hit element. Could be an
|
// TODO: Current assumption is one style per hit element. Could be an
|
||||||
// issue if an element has multiple styling and one styling is
|
// issue if an element has multiple styling and one styling is
|
||||||
// brought back. Possibly too rare to care about this for now.
|
// brought back. Possibly too rare to care about this for now.
|
||||||
unstyleNodes(nodes) {
|
unprocessNodes(nodes) {
|
||||||
for ( const node of nodes ) {
|
for ( const node of nodes ) {
|
||||||
if ( this.styledNodes.has(node) ) { continue; }
|
if ( this.styledNodes.has(node) ) { continue; }
|
||||||
node.removeAttribute(this.masterToken);
|
node.removeAttribute(this.masterToken);
|
||||||
|
|
@ -589,7 +613,9 @@ class ProceduralFilterer {
|
||||||
}
|
}
|
||||||
|
|
||||||
createProceduralFilter(o) {
|
createProceduralFilter(o) {
|
||||||
return new PSelectorRoot(typeof o === 'string' ? JSON.parse(o) : o);
|
return this.primeProceduralSelector(
|
||||||
|
new PSelectorRoot(typeof o === 'string' ? JSON.parse(o) : o)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDOMCreated() {
|
onDOMCreated() {
|
||||||
|
|
|
||||||
|
|
@ -756,9 +756,17 @@ const filterToDOMInterface = (( ) => {
|
||||||
try {
|
try {
|
||||||
const o = JSON.parse(raw);
|
const o = JSON.parse(raw);
|
||||||
elems = vAPI.domFilterer.createProceduralFilter(o).exec();
|
elems = vAPI.domFilterer.createProceduralFilter(o).exec();
|
||||||
style = o.action === undefined || o.action[0] !== 'style'
|
switch ( o.action && o.action[0] || '' ) {
|
||||||
? vAPI.hideStyle
|
case '':
|
||||||
: o.action[1];
|
case 'remove':
|
||||||
|
style = vAPI.hideStyle;
|
||||||
|
break;
|
||||||
|
case 'style':
|
||||||
|
style = o.action[1];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -809,6 +817,7 @@ const filterToDOMInterface = (( ) => {
|
||||||
const rootElem = document.documentElement;
|
const rootElem = document.documentElement;
|
||||||
for ( const { elem, style } of lastResultset ) {
|
for ( const { elem, style } of lastResultset ) {
|
||||||
if ( elem === pickerRoot ) { continue; }
|
if ( elem === pickerRoot ) { continue; }
|
||||||
|
if ( style === undefined ) { continue; }
|
||||||
if ( elem === rootElem && style === vAPI.hideStyle ) { continue; }
|
if ( elem === rootElem && style === vAPI.hideStyle ) { continue; }
|
||||||
let styleToken = vAPI.epickerStyleProxies.get(style);
|
let styleToken = vAPI.epickerStyleProxies.get(style);
|
||||||
if ( styleToken === undefined ) {
|
if ( styleToken === undefined ) {
|
||||||
|
|
|
||||||
|
|
@ -1343,7 +1343,6 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
|
|
||||||
this.reEatBackslashes = /\\([()])/g;
|
this.reEatBackslashes = /\\([()])/g;
|
||||||
this.reEscapeRegex = /[.*+?^${}()|[\]\\]/g;
|
this.reEscapeRegex = /[.*+?^${}()|[\]\\]/g;
|
||||||
this.regexToRawValue = new Map();
|
|
||||||
// https://github.com/gorhill/uBlock/issues/2793
|
// https://github.com/gorhill/uBlock/issues/2793
|
||||||
this.normalizedOperators = new Map([
|
this.normalizedOperators = new Map([
|
||||||
[ '-abp-has', 'has' ],
|
[ '-abp-has', 'has' ],
|
||||||
|
|
@ -1379,6 +1378,8 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
]);
|
]);
|
||||||
this.proceduralActionNames = new Set([
|
this.proceduralActionNames = new Set([
|
||||||
'remove',
|
'remove',
|
||||||
|
'remove-attr',
|
||||||
|
'remove-class',
|
||||||
'style',
|
'style',
|
||||||
]);
|
]);
|
||||||
this.normalizedExtendedSyntaxOperators = new Map([
|
this.normalizedExtendedSyntaxOperators = new Map([
|
||||||
|
|
@ -1563,6 +1564,10 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
if ( this.maybeProceduralOperatorNames.has(data.name) === false ) {
|
if ( this.maybeProceduralOperatorNames.has(data.name) === false ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ( this.astHasType(args, 'ActionSelector') ) {
|
||||||
|
data.type = 'Error';
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ( this.astHasType(args, 'ProceduralSelector') ) {
|
if ( this.astHasType(args, 'ProceduralSelector') ) {
|
||||||
data.type = 'ProceduralSelector';
|
data.type = 'ProceduralSelector';
|
||||||
return;
|
return;
|
||||||
|
|
@ -1719,7 +1724,7 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
return out.join('');
|
return out.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
astCompile(parts) {
|
astCompile(parts, details = {}) {
|
||||||
if ( Array.isArray(parts) === false ) { return; }
|
if ( Array.isArray(parts) === false ) { return; }
|
||||||
if ( parts.length === 0 ) { return; }
|
if ( parts.length === 0 ) { return; }
|
||||||
if ( parts[0].data.type !== 'SelectorList' ) { return; }
|
if ( parts[0].data.type !== 'SelectorList' ) { return; }
|
||||||
|
|
@ -1730,6 +1735,8 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
const { data } = part;
|
const { data } = part;
|
||||||
switch ( data.type ) {
|
switch ( data.type ) {
|
||||||
case 'ActionSelector': {
|
case 'ActionSelector': {
|
||||||
|
if ( details.noaction ) { return; }
|
||||||
|
if ( out.action !== undefined ) { return; }
|
||||||
if ( prelude.length !== 0 ) {
|
if ( prelude.length !== 0 ) {
|
||||||
if ( tasks.length === 0 ) {
|
if ( tasks.length === 0 ) {
|
||||||
out.selector = prelude.join('');
|
out.selector = prelude.join('');
|
||||||
|
|
@ -1881,23 +1888,20 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
compileArgumentAst(operator, parts) {
|
compileArgumentAst(operator, parts) {
|
||||||
switch ( operator ) {
|
switch ( operator ) {
|
||||||
case 'has': {
|
case 'has': {
|
||||||
let r = this.astCompile(parts);
|
let r = this.astCompile(parts, { noaction: true });
|
||||||
if ( typeof r === 'string' ) {
|
if ( typeof r === 'string' ) {
|
||||||
r = { selector: r.replace(/^\s*:scope\s*/, ' ') };
|
r = { selector: r.replace(/^\s*:scope\s*/, ' ') };
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
case 'not': {
|
case 'not': {
|
||||||
return this.astCompile(parts);
|
return this.astCompile(parts, { noaction: true });
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if ( Array.isArray(parts) === false || parts.length === 0 ) { return; }
|
||||||
let arg;
|
const arg = this.astSerialize(parts, false);
|
||||||
if ( Array.isArray(parts) && parts.length !== 0 ) {
|
|
||||||
arg = this.astSerialize(parts, false);
|
|
||||||
}
|
|
||||||
if ( arg === undefined ) { return; }
|
if ( arg === undefined ) { return; }
|
||||||
switch ( operator ) {
|
switch ( operator ) {
|
||||||
case 'has-text':
|
case 'has-text':
|
||||||
|
|
@ -1924,6 +1928,10 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
return this.compileNoArgument(arg);
|
return this.compileNoArgument(arg);
|
||||||
case 'remove':
|
case 'remove':
|
||||||
return this.compileNoArgument(arg);
|
return this.compileNoArgument(arg);
|
||||||
|
case 'remove-attr':
|
||||||
|
return this.compileText(arg);
|
||||||
|
case 'remove-class':
|
||||||
|
return this.compileText(arg);
|
||||||
case 'style':
|
case 'style':
|
||||||
return this.compileStyleProperties(arg);
|
return this.compileStyleProperties(arg);
|
||||||
case 'upward':
|
case 'upward':
|
||||||
|
|
@ -1999,6 +2007,7 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
if ( attr === '' ) { return; }
|
if ( attr === '' ) { return; }
|
||||||
if ( value.length !== 0 ) {
|
if ( value.length !== 0 ) {
|
||||||
r = this.unquoteString(value);
|
r = this.unquoteString(value);
|
||||||
|
if ( r.i !== value.length ) { return; }
|
||||||
value = r.s;
|
value = r.s;
|
||||||
}
|
}
|
||||||
return { attr, value };
|
return { attr, value };
|
||||||
|
|
@ -2011,22 +2020,7 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
if ( s === '' ) { return; }
|
if ( s === '' ) { return; }
|
||||||
const r = this.unquoteString(s);
|
const r = this.unquoteString(s);
|
||||||
if ( r.i !== s.length ) { return; }
|
if ( r.i !== s.length ) { return; }
|
||||||
const match = this.reParseRegexLiteral.exec(r.s);
|
return r.s;
|
||||||
let regexDetails;
|
|
||||||
if ( match !== null ) {
|
|
||||||
regexDetails = match[1];
|
|
||||||
if ( this.isBadRegex(regexDetails) ) { return; }
|
|
||||||
if ( match[2] ) {
|
|
||||||
regexDetails = [ regexDetails, match[2] ];
|
|
||||||
}
|
|
||||||
} else if ( r.s === '' ) {
|
|
||||||
regexDetails = '^$';
|
|
||||||
} else {
|
|
||||||
regexDetails = r.s.replace(this.reEatBackslashes, '$1')
|
|
||||||
.replace(this.reEscapeRegex, '\\$&');
|
|
||||||
this.regexToRawValue.set(regexDetails, r.s);
|
|
||||||
}
|
|
||||||
return regexDetails;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileCSSDeclaration(s) {
|
compileCSSDeclaration(s) {
|
||||||
|
|
@ -2051,7 +2045,6 @@ Parser.prototype.SelectorCompiler = class {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
regexDetails = '^' + value.replace(this.reEscapeRegex, '\\$&') + '$';
|
regexDetails = '^' + value.replace(this.reEscapeRegex, '\\$&') + '$';
|
||||||
this.regexToRawValue.set(regexDetails, value);
|
|
||||||
}
|
}
|
||||||
return { name, pseudo, value: regexDetails };
|
return { name, pseudo, value: regexDetails };
|
||||||
}
|
}
|
||||||
|
|
@ -2138,6 +2131,7 @@ Parser.prototype.proceduralOperatorTokens = new Map([
|
||||||
[ 'has-text', 0b01 ],
|
[ 'has-text', 0b01 ],
|
||||||
[ 'if', 0b00 ],
|
[ 'if', 0b00 ],
|
||||||
[ 'if-not', 0b00 ],
|
[ 'if-not', 0b00 ],
|
||||||
|
[ 'matches-attr', 0b01 ],
|
||||||
[ 'matches-css', 0b11 ],
|
[ 'matches-css', 0b11 ],
|
||||||
[ 'matches-media', 0b11 ],
|
[ 'matches-media', 0b11 ],
|
||||||
[ 'matches-path', 0b11 ],
|
[ 'matches-path', 0b11 ],
|
||||||
|
|
@ -2146,6 +2140,8 @@ Parser.prototype.proceduralOperatorTokens = new Map([
|
||||||
[ 'nth-ancestor', 0b00 ],
|
[ 'nth-ancestor', 0b00 ],
|
||||||
[ 'others', 0b01 ],
|
[ 'others', 0b01 ],
|
||||||
[ 'remove', 0b11 ],
|
[ 'remove', 0b11 ],
|
||||||
|
[ 'remove-attr', 0b01 ],
|
||||||
|
[ 'remove-class', 0b01 ],
|
||||||
[ 'style', 0b11 ],
|
[ 'style', 0b11 ],
|
||||||
[ 'upward', 0b01 ],
|
[ 'upward', 0b01 ],
|
||||||
[ 'watch-attr', 0b11 ],
|
[ 'watch-attr', 0b11 ],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue