mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-10 17:17:57 +01:00
new content script code: code review, fine tuning perf
This commit is contained in:
parent
2d68c8ee6c
commit
b65699aef2
1 changed files with 189 additions and 116 deletions
|
@ -47,31 +47,34 @@ vAPI.contentscriptInjected = true;
|
||||||
vAPI.domFilterer = {
|
vAPI.domFilterer = {
|
||||||
allExceptions: Object.create(null),
|
allExceptions: Object.create(null),
|
||||||
allSelectors: Object.create(null),
|
allSelectors: Object.create(null),
|
||||||
|
cosmeticFiltersActivatedTimer: null,
|
||||||
cssNotHiddenId: '',
|
cssNotHiddenId: '',
|
||||||
disabledId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36),
|
disabledId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
hiddenId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36),
|
hiddenId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36),
|
||||||
hiddenNodeCount: 0,
|
hiddenNodeCount: 0,
|
||||||
matchesProp: 'matches',
|
matchesProp: 'matches',
|
||||||
newSelectors: [],
|
newDeclarativeSelectors: [],
|
||||||
shadowId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36),
|
shadowId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36),
|
||||||
styleTags: [],
|
styleTags: [],
|
||||||
xpathNotHiddenId: '',
|
xpathNotHiddenId: '',
|
||||||
|
|
||||||
complexGroupSelector: null,
|
|
||||||
complexSelectors: [],
|
|
||||||
complexSelectorsMissCount: 0,
|
|
||||||
simpleGroupSelector: null,
|
simpleGroupSelector: null,
|
||||||
simpleSelectors: [],
|
simpleSelectors: [],
|
||||||
|
|
||||||
|
complexGroupSelector: null,
|
||||||
|
complexSelectors: [],
|
||||||
|
complexSelectorsCost: 0,
|
||||||
|
complexSelectorsNodeSet: null,
|
||||||
|
|
||||||
complexHasSelectors: [],
|
complexHasSelectors: [],
|
||||||
complexHasSelectorsMissCount: 0,
|
complexHasSelectorsCost: 0,
|
||||||
simpleHasSelectors: [],
|
simpleHasSelectors: [],
|
||||||
|
|
||||||
xpathExpression: null,
|
xpathExpression: null,
|
||||||
xpathResult: null,
|
xpathResult: null,
|
||||||
xpathSelectors: [],
|
xpathSelectors: [],
|
||||||
xpathSelectorsMissCount: 0,
|
xpathSelectorsCost: 0,
|
||||||
|
|
||||||
addExceptions: function(aa) {
|
addExceptions: function(aa) {
|
||||||
for ( var i = 0, n = aa.length; i < n; i++ ) {
|
for ( var i = 0, n = aa.length; i < n; i++ ) {
|
||||||
|
@ -85,7 +88,7 @@ vAPI.domFilterer = {
|
||||||
this.simpleHasSelectors.push(entry);
|
this.simpleHasSelectors.push(entry);
|
||||||
} else {
|
} else {
|
||||||
this.complexHasSelectors.push(entry);
|
this.complexHasSelectors.push(entry);
|
||||||
this.complexHasSelectorsMissCount = 0;
|
this.complexHasSelectorsCost = 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -112,9 +115,9 @@ vAPI.domFilterer = {
|
||||||
} else {
|
} else {
|
||||||
this.complexSelectors.push(s);
|
this.complexSelectors.push(s);
|
||||||
this.complexGroupSelector = null;
|
this.complexGroupSelector = null;
|
||||||
this.complexSelectorsMissCount = 0;
|
this.complexSelectorsCost = 0;
|
||||||
}
|
}
|
||||||
this.newSelectors.push(s);
|
this.newDeclarativeSelectors.push(s);
|
||||||
},
|
},
|
||||||
|
|
||||||
addSelectors: function(aa) {
|
addSelectors: function(aa) {
|
||||||
|
@ -126,6 +129,7 @@ vAPI.domFilterer = {
|
||||||
addXpathSelector: function(s1, s2) {
|
addXpathSelector: function(s1, s2) {
|
||||||
this.xpathSelectors.push(s2.slice(7, -1));
|
this.xpathSelectors.push(s2.slice(7, -1));
|
||||||
this.xpathExpression = null;
|
this.xpathExpression = null;
|
||||||
|
this.xpathSelectorsCost = 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
checkStyleTags: function(commitIfNeeded) {
|
checkStyleTags: function(commitIfNeeded) {
|
||||||
|
@ -164,35 +168,115 @@ vAPI.domFilterer = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
commit: function(newNodes) {
|
commit: function(stagedNodes) {
|
||||||
var beforeHiddenNodeCount = this.hiddenNodeCount;
|
var beforeHiddenNodeCount = this.hiddenNodeCount;
|
||||||
|
|
||||||
if ( newNodes === undefined ) {
|
if ( stagedNodes === undefined ) {
|
||||||
newNodes = [ document.documentElement ];
|
stagedNodes = [ document.documentElement ];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject new selectors as CSS rules in a style tag.
|
// Inject new declarative selectors.
|
||||||
if ( this.newSelectors.length ) {
|
if ( this.newDeclarativeSelectors.length ) {
|
||||||
var styleTag = document.createElement('style');
|
var styleTag = document.createElement('style');
|
||||||
styleTag.setAttribute('type', 'text/css');
|
styleTag.setAttribute('type', 'text/css');
|
||||||
styleTag.textContent =
|
styleTag.textContent =
|
||||||
':root ' +
|
':root ' +
|
||||||
this.newSelectors.join(',\n:root ') +
|
this.newDeclarativeSelectors.join(',\n:root ') +
|
||||||
'\n{ display: none !important; }';
|
'\n{ display: none !important; }';
|
||||||
document.head.appendChild(styleTag);
|
document.head.appendChild(styleTag);
|
||||||
this.styleTags.push(styleTag);
|
this.styleTags.push(styleTag);
|
||||||
|
this.newDeclarativeSelectors.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodes, node, parents, parent, i, j, k, entry;
|
|
||||||
|
|
||||||
// Simple `:has()` selectors.
|
// Simple `:has()` selectors.
|
||||||
i = this.simpleHasSelectors.length;
|
if ( this.simpleHasSelectors.length ) {
|
||||||
|
this.commitSimpleHasSelectors(stagedNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex `:has()` selectors.
|
||||||
|
if ( this.complexHasSelectorsCost < 10 && this.complexHasSelectors.length ) {
|
||||||
|
this.commitComplexHasSelectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// `:xpath()` selectors.
|
||||||
|
if ( this.xpathSelectorsCost < 10 && this.xpathSelectors.length ) {
|
||||||
|
this.commitXpathSelectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Committing declarative selectors is entirely optional, but it helps
|
||||||
|
// harden uBO against sites which try to bypass uBO's injected styles.
|
||||||
|
|
||||||
|
// Simple selectors.
|
||||||
|
if ( this.simpleSelectors.length ) {
|
||||||
|
this.commitSimpleSelectors(stagedNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex selectors.
|
||||||
|
if ( this.complexSelectorsCost < 10 && this.complexSelectors.length ) {
|
||||||
|
this.commitComplexSelectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If DOM nodes have been affected, lazily notify core process.
|
||||||
|
if (
|
||||||
|
this.hiddenNodeCount !== beforeHiddenNodeCount &&
|
||||||
|
this.cosmeticFiltersActivatedTimer === null
|
||||||
|
) {
|
||||||
|
this.cosmeticFiltersActivatedTimer = vAPI.setTimeout(
|
||||||
|
this.cosmeticFiltersActivated,
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
commitComplexHasSelectors: function() {
|
||||||
|
var tstart = window.performance.now(),
|
||||||
|
entry, nodes, j, node,
|
||||||
|
i = this.complexHasSelectors.length;
|
||||||
|
while ( i-- ) {
|
||||||
|
entry = this.complexHasSelectors[i];
|
||||||
|
nodes = document.querySelectorAll(entry.a + this.cssNotHiddenId);
|
||||||
|
j = nodes.length;
|
||||||
|
while ( j-- ) {
|
||||||
|
node = nodes[j];
|
||||||
|
if ( node.querySelector(entry.b) !== null ) {
|
||||||
|
this.hideNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.complexHasSelectorsCost = window.performance.now() - tstart;
|
||||||
|
},
|
||||||
|
|
||||||
|
commitComplexSelectors: function() {
|
||||||
|
var tstart = window.performance.now(),
|
||||||
|
newNodeSet = new Set();
|
||||||
|
if ( this.complexGroupSelector === null ) {
|
||||||
|
this.complexGroupSelector = this.complexSelectors.join(',');
|
||||||
|
}
|
||||||
|
var nodes = document.querySelectorAll(this.complexGroupSelector),
|
||||||
|
i = nodes.length, node;
|
||||||
|
while ( i-- ) {
|
||||||
|
node = nodes[i];
|
||||||
|
newNodeSet.add(node);
|
||||||
|
if ( !this.complexSelectorsNodeSet.delete(node) ) {
|
||||||
|
this.hideNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var iter = this.complexSelectorsNodeSet.values();
|
||||||
|
while ( (node = iter.next().value) ) {
|
||||||
|
this.unhideNode(node);
|
||||||
|
}
|
||||||
|
this.complexSelectorsNodeSet = newNodeSet;
|
||||||
|
this.complexSelectorsCost = window.performance.now() - tstart;
|
||||||
|
},
|
||||||
|
|
||||||
|
commitSimpleHasSelectors: function(stagedNodes) {
|
||||||
|
var i = this.simpleHasSelectors.length,
|
||||||
|
entry, j, parent, nodes, k, node;
|
||||||
while ( i-- ) {
|
while ( i-- ) {
|
||||||
entry = this.simpleHasSelectors[i];
|
entry = this.simpleHasSelectors[i];
|
||||||
parents = newNodes;
|
j = stagedNodes.length;
|
||||||
j = parents.length;
|
|
||||||
while ( j-- ) {
|
while ( j-- ) {
|
||||||
parent = parents[j];
|
parent = stagedNodes[j];
|
||||||
if ( parent[this.matchesProp](entry.a) && parent.querySelector(entry.b) !== null ) {
|
if ( parent[this.matchesProp](entry.a) && parent.querySelector(entry.b) !== null ) {
|
||||||
this.hideNode(parent);
|
this.hideNode(parent);
|
||||||
}
|
}
|
||||||
|
@ -206,97 +290,57 @@ vAPI.domFilterer = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Complex `:has()` selectors.
|
commitSimpleSelectors: function(stagedNodes) {
|
||||||
if ( this.complexHasSelectorsMissCount < 5 && this.complexHasSelectors.length ) {
|
if ( this.simpleGroupSelector === null ) {
|
||||||
this.complexHasSelectorsMissCount += 1;
|
this.simpleGroupSelector =
|
||||||
i = this.complexHasSelectors.length;
|
this.simpleSelectors.join(this.cssNotHiddenId + ',') +
|
||||||
while ( i-- ) {
|
this.cssNotHiddenId;
|
||||||
entry = this.complexHasSelectors[i];
|
}
|
||||||
nodes = document.querySelectorAll(entry.a + this.cssNotHiddenId);
|
var i = stagedNodes.length, stagedNode, nodes, j;
|
||||||
j = nodes.length;
|
while ( i-- ) {
|
||||||
while ( j-- ) {
|
stagedNode = stagedNodes[i];
|
||||||
node = nodes[j];
|
if ( stagedNode[this.matchesProp](this.simpleGroupSelector) ) {
|
||||||
if ( node.querySelector(entry.b) !== null ) {
|
this.hideNode(stagedNode);
|
||||||
this.hideNode(node);
|
}
|
||||||
this.complexHasSelectorsMissCount = 0;
|
nodes = stagedNode.querySelectorAll(this.simpleGroupSelector);
|
||||||
}
|
j = nodes.length;
|
||||||
}
|
while ( j-- ) {
|
||||||
|
this.hideNode(nodes[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// `:xpath()` selectors.
|
commitXpathSelectors: function() {
|
||||||
if ( this.xpathSelectorsMissCount < 5 && this.xpathSelectors.length ) {
|
var tstart = window.performance.now();
|
||||||
this.xpathSelectorsMissCount += 1;
|
if ( this.xpathExpression === null ) {
|
||||||
if ( this.xpathExpression === null ) {
|
this.xpathExpression = document.createExpression(
|
||||||
this.xpathExpression = document.createExpression(
|
this.xpathSelectors.join(this.xpathNotHiddenId + '|') + this.xpathNotHiddenId,
|
||||||
this.xpathSelectors.join(this.xpathNotHiddenId + '|') + this.xpathNotHiddenId,
|
null
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.xpathResult = this.xpathExpression.evaluate(
|
|
||||||
document,
|
|
||||||
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
|
|
||||||
this.xpathResult
|
|
||||||
);
|
|
||||||
i = this.xpathResult.snapshotLength;
|
|
||||||
while ( i-- ) {
|
|
||||||
node = this.xpathResult.snapshotItem(i);
|
|
||||||
if ( node.nodeType === 1 ) {
|
|
||||||
this.hideNode(node);
|
|
||||||
this.xpathSelectorsMissCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple selectors.
|
|
||||||
if ( this.simpleSelectors.length ) {
|
|
||||||
if ( this.simpleGroupSelector === null ) {
|
|
||||||
this.simpleGroupSelector =
|
|
||||||
this.simpleSelectors.join(this.cssNotHiddenId + ',') +
|
|
||||||
this.cssNotHiddenId;
|
|
||||||
}
|
|
||||||
parents = newNodes;
|
|
||||||
i = parents.length;
|
|
||||||
while ( i-- ) {
|
|
||||||
parent = parents[i];
|
|
||||||
if ( parent[this.matchesProp](this.simpleGroupSelector) ) {
|
|
||||||
this.hideNode(parent);
|
|
||||||
}
|
|
||||||
nodes = parent.querySelectorAll(this.simpleGroupSelector);
|
|
||||||
j = nodes.length;
|
|
||||||
while ( j-- ) {
|
|
||||||
this.hideNode(nodes[j]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complex selectors.
|
|
||||||
if ( this.complexSelectorsMissCount < 5 && this.complexSelectors.length ) {
|
|
||||||
this.complexSelectorsMissCount += 1;
|
|
||||||
if ( this.complexGroupSelector === null ) {
|
|
||||||
this.complexGroupSelector =
|
|
||||||
this.complexSelectors.join(this.cssNotHiddenId + ',') +
|
|
||||||
this.cssNotHiddenId;
|
|
||||||
}
|
|
||||||
nodes = document.querySelectorAll(this.complexGroupSelector);
|
|
||||||
i = nodes.length;
|
|
||||||
while ( i-- ) {
|
|
||||||
this.hideNode(nodes[i]);
|
|
||||||
this.complexSelectorsMissCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset transient state.
|
|
||||||
this.newSelectors.length = 0;
|
|
||||||
|
|
||||||
// If DOM nodes have been affected, notify core process.
|
|
||||||
if ( this.hiddenNodeCount !== beforeHiddenNodeCount ) {
|
|
||||||
vAPI.messaging.send(
|
|
||||||
'contentscript',
|
|
||||||
{ what: 'cosmeticFiltersActivated' }
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.xpathResult = this.xpathExpression.evaluate(
|
||||||
|
document,
|
||||||
|
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
|
||||||
|
this.xpathResult
|
||||||
|
);
|
||||||
|
var i = this.xpathResult.snapshotLength, node;
|
||||||
|
while ( i-- ) {
|
||||||
|
node = this.xpathResult.snapshotItem(i);
|
||||||
|
if ( node.nodeType === 1 ) {
|
||||||
|
this.hideNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.xpathSelectorsCost = window.performance.now() - tstart;
|
||||||
|
},
|
||||||
|
|
||||||
|
cosmeticFiltersActivated: function() {
|
||||||
|
this.cosmeticFiltersActivatedTimer = null;
|
||||||
|
vAPI.messaging.send(
|
||||||
|
'contentscript',
|
||||||
|
{ what: 'cosmeticFiltersActivated' }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
hideNode: (function() {
|
hideNode: (function() {
|
||||||
|
@ -343,6 +387,20 @@ vAPI.domFilterer = {
|
||||||
|
|
||||||
toggleOn: function() {
|
toggleOn: function() {
|
||||||
this.enabled = true;
|
this.enabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
unhideNode: function(node) {
|
||||||
|
this.hiddenNodeCount--;
|
||||||
|
node.removeAttribute(this.hiddenId);
|
||||||
|
var shadow = node.shadowRoot;
|
||||||
|
if ( shadow && shadow.className === this.shadowId ) {
|
||||||
|
if ( shadow.firstElementChild !== null ) {
|
||||||
|
shadow.removeChild(shadow.firstElementChild);
|
||||||
|
}
|
||||||
|
shadow.appendChild(document.createElement('content'));
|
||||||
|
} else {
|
||||||
|
node.style.removeProperty('display');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -360,6 +418,21 @@ vAPI.domFilterer = {
|
||||||
df.matchesProp = 'webkitMatchesSelector';
|
df.matchesProp = 'webkitMatchesSelector';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Complex selectors, due to their nature may need to be "de-committed". A
|
||||||
|
// Set() is used to implement this functionality. For browser with no
|
||||||
|
// support of Set(), uBO will skip committing complex selectors.
|
||||||
|
if ( typeof window.Set === 'function' ) {
|
||||||
|
df.complexSelectorsNodeSet = new Set();
|
||||||
|
} else {
|
||||||
|
df.complexSelectorsCost = Number.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theoretically, `:has`- and `:xpath`-based selectors may also need to
|
||||||
|
// be de-committed. But for performance purpose, this is not implemented,
|
||||||
|
// and anyways, the point of these selectors is to be very accurate, so
|
||||||
|
// I do not expect de-committing scenarios to occur with proper use of
|
||||||
|
// these selectors.
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
@ -696,9 +769,15 @@ var domCollapser = (function() {
|
||||||
var iframesFromNode = function(node) {
|
var iframesFromNode = function(node) {
|
||||||
if ( node.localName === 'iframe' ) {
|
if ( node.localName === 'iframe' ) {
|
||||||
addIFrame(node);
|
addIFrame(node);
|
||||||
|
process();
|
||||||
|
}
|
||||||
|
if ( node.children.length !== 0 ) {
|
||||||
|
var iframes = node.getElementsByTagName('iframe');
|
||||||
|
if ( iframes.length !== 0 ) {
|
||||||
|
addIFrames(iframes);
|
||||||
|
process();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
addIFrames(node.getElementsByTagName('iframe'));
|
|
||||||
process();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -736,7 +815,7 @@ if ( !vAPI.contentscriptInjected ) {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// Cosmetic filters
|
// Cosmetic filtering.
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
if ( vAPI.skipCosmeticFiltering ) {
|
if ( vAPI.skipCosmeticFiltering ) {
|
||||||
|
@ -770,7 +849,6 @@ if ( !vAPI.contentscriptInjected ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//var tStart = timer.now();
|
|
||||||
var result = response && response.result;
|
var result = response && response.result;
|
||||||
|
|
||||||
if ( result ) {
|
if ( result ) {
|
||||||
|
@ -794,7 +872,6 @@ if ( !vAPI.contentscriptInjected ) {
|
||||||
}
|
}
|
||||||
domFilterer.commit(contextNodes);
|
domFilterer.commit(contextNodes);
|
||||||
contextNodes = [];
|
contextNodes = [];
|
||||||
//console.debug('%f: uBlock: CSS injection time', timer.now() - tStart);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var retrieveGenericSelectors = function() {
|
var retrieveGenericSelectors = function() {
|
||||||
|
@ -815,10 +892,6 @@ if ( !vAPI.contentscriptInjected ) {
|
||||||
lowGenericSelectors = [];
|
lowGenericSelectors = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure elements matching a set of selectors are visually removed
|
|
||||||
// from the page, by:
|
|
||||||
// - Modifying the style property on the elements themselves
|
|
||||||
// - Injecting a style tag
|
|
||||||
// Extract and return the staged nodes which (may) match the selectors.
|
// Extract and return the staged nodes which (may) match the selectors.
|
||||||
|
|
||||||
var selectNodes = function(selector) {
|
var selectNodes = function(selector) {
|
||||||
|
@ -1069,7 +1142,7 @@ if ( !vAPI.contentscriptInjected ) {
|
||||||
var addedNodesHandler = function() {
|
var addedNodesHandler = function() {
|
||||||
addedNodeListsTimer = null;
|
addedNodeListsTimer = null;
|
||||||
if ( addedNodeListsTimerDelay < 100 ) {
|
if ( addedNodeListsTimerDelay < 100 ) {
|
||||||
addedNodeListsTimerDelay *= 2;
|
addedNodeListsTimerDelay += 25;
|
||||||
}
|
}
|
||||||
var iNodeList = addedNodeLists.length,
|
var iNodeList = addedNodeLists.length,
|
||||||
nodeList, iNode, node;
|
nodeList, iNode, node;
|
||||||
|
|
Loading…
Reference in a new issue