udom.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 Raymond Hill
  4. Copyright (C) 2019 Alessio Vanni
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see {http://www.gnu.org/licenses/}.
  15. Home: https://libregit.org/heckyel/ematrix
  16. uMatrix Home: https://github.com/gorhill/uBlock
  17. */
  18. /* global DOMTokenList */
  19. /* exported uDom */
  20. 'use strict';
  21. /******************************************************************************/
  22. // It's just a silly, minimalist DOM framework: this allows me to not rely
  23. // on jQuery. jQuery contains way too much stuff than I need, and as per
  24. // Opera rules, I am not allowed to use a cut-down version of jQuery. So
  25. // the code here does *only* what I need, and nothing more, and with a lot
  26. // of assumption on passed parameters, etc. I grow it on a per-need-basis only.
  27. var uDom = (function() {
  28. /******************************************************************************/
  29. var DOMList = function() {
  30. this.nodes = [];
  31. };
  32. /******************************************************************************/
  33. Object.defineProperty(
  34. DOMList.prototype,
  35. 'length',
  36. {
  37. get: function() {
  38. return this.nodes.length;
  39. }
  40. }
  41. );
  42. /******************************************************************************/
  43. var DOMListFactory = function(selector, context) {
  44. var r = new DOMList();
  45. if ( typeof selector === 'string' ) {
  46. selector = selector.trim();
  47. if ( selector !== '' ) {
  48. return addSelectorToList(r, selector, context);
  49. }
  50. }
  51. if ( selector instanceof Node ) {
  52. return addNodeToList(r, selector);
  53. }
  54. if ( selector instanceof NodeList ) {
  55. return addNodeListToList(r, selector);
  56. }
  57. if ( selector instanceof DOMList ) {
  58. return addListToList(r, selector);
  59. }
  60. return r;
  61. };
  62. /******************************************************************************/
  63. DOMListFactory.onLoad = function(callback) {
  64. window.addEventListener('load', callback);
  65. };
  66. /******************************************************************************/
  67. DOMListFactory.nodeFromId = function(id) {
  68. return document.getElementById(id);
  69. };
  70. DOMListFactory.nodeFromSelector = function(selector) {
  71. return document.querySelector(selector);
  72. };
  73. /******************************************************************************/
  74. var addNodeToList = function(list, node) {
  75. if ( node ) {
  76. list.nodes.push(node);
  77. }
  78. return list;
  79. };
  80. /******************************************************************************/
  81. var addNodeListToList = function(list, nodelist) {
  82. if ( nodelist ) {
  83. var n = nodelist.length;
  84. for ( var i = 0; i < n; i++ ) {
  85. list.nodes.push(nodelist[i]);
  86. }
  87. }
  88. return list;
  89. };
  90. /******************************************************************************/
  91. var addListToList = function(list, other) {
  92. list.nodes = list.nodes.concat(other.nodes);
  93. return list;
  94. };
  95. /******************************************************************************/
  96. var addSelectorToList = function(list, selector, context) {
  97. var p = context || document;
  98. var r = p.querySelectorAll(selector);
  99. var n = r.length;
  100. for ( var i = 0; i < n; i++ ) {
  101. list.nodes.push(r[i]);
  102. }
  103. return list;
  104. };
  105. /******************************************************************************/
  106. var nodeInNodeList = function(node, nodeList) {
  107. var i = nodeList.length;
  108. while ( i-- ) {
  109. if ( nodeList[i] === node ) {
  110. return true;
  111. }
  112. }
  113. return false;
  114. };
  115. /******************************************************************************/
  116. var doesMatchSelector = function(node, selector) {
  117. if ( !node ) {
  118. return false;
  119. }
  120. if ( node.nodeType !== 1 ) {
  121. return false;
  122. }
  123. if ( selector === undefined ) {
  124. return true;
  125. }
  126. var parentNode = node.parentNode;
  127. if ( !parentNode || !parentNode.setAttribute ) {
  128. return false;
  129. }
  130. var doesMatch = false;
  131. parentNode.setAttribute('uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO', '');
  132. var grandpaNode = parentNode.parentNode || document;
  133. var nl = grandpaNode.querySelectorAll('[uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO] > ' + selector);
  134. var i = nl.length;
  135. while ( doesMatch === false && i-- ) {
  136. doesMatch = nl[i] === node;
  137. }
  138. parentNode.removeAttribute('uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO');
  139. return doesMatch;
  140. };
  141. /******************************************************************************/
  142. DOMList.prototype.nodeAt = function(i) {
  143. return this.nodes[i] || null;
  144. };
  145. DOMList.prototype.at = function(i) {
  146. return addNodeToList(new DOMList(), this.nodes[i]);
  147. };
  148. /******************************************************************************/
  149. DOMList.prototype.toArray = function() {
  150. return this.nodes.slice();
  151. };
  152. /******************************************************************************/
  153. DOMList.prototype.pop = function() {
  154. return addNodeToList(new DOMList(), this.nodes.pop());
  155. };
  156. /******************************************************************************/
  157. DOMList.prototype.forEach = function(fn) {
  158. var n = this.nodes.length;
  159. for ( var i = 0; i < n; i++ ) {
  160. fn(this.at(i), i);
  161. }
  162. return this;
  163. };
  164. /******************************************************************************/
  165. DOMList.prototype.subset = function(i, l) {
  166. var r = new DOMList();
  167. var n = l !== undefined ? l : this.nodes.length;
  168. var j = Math.min(i + n, this.nodes.length);
  169. if ( i < j ) {
  170. r.nodes = this.nodes.slice(i, j);
  171. }
  172. return r;
  173. };
  174. /******************************************************************************/
  175. DOMList.prototype.first = function() {
  176. return this.subset(0, 1);
  177. };
  178. /******************************************************************************/
  179. DOMList.prototype.next = function(selector) {
  180. var r = new DOMList();
  181. var n = this.nodes.length;
  182. var node;
  183. for ( var i = 0; i < n; i++ ) {
  184. node = this.nodes[i];
  185. while ( node.nextSibling !== null ) {
  186. node = node.nextSibling;
  187. if ( node.nodeType !== 1 ) {
  188. continue;
  189. }
  190. if ( doesMatchSelector(node, selector) === false ) {
  191. continue;
  192. }
  193. addNodeToList(r, node);
  194. break;
  195. }
  196. }
  197. return r;
  198. };
  199. /******************************************************************************/
  200. DOMList.prototype.parent = function() {
  201. var r = new DOMList();
  202. if ( this.nodes.length ) {
  203. addNodeToList(r, this.nodes[0].parentNode);
  204. }
  205. return r;
  206. };
  207. /******************************************************************************/
  208. DOMList.prototype.filter = function(filter) {
  209. var r = new DOMList();
  210. var filterFunc;
  211. if ( typeof filter === 'string' ) {
  212. filterFunc = function() {
  213. return doesMatchSelector(this, filter);
  214. };
  215. } else if ( typeof filter === 'function' ) {
  216. filterFunc = filter;
  217. } else {
  218. filterFunc = function(){
  219. return true;
  220. };
  221. }
  222. var n = this.nodes.length;
  223. var node;
  224. for ( var i = 0; i < n; i++ ) {
  225. node = this.nodes[i];
  226. if ( filterFunc.apply(node) ) {
  227. addNodeToList(r, node);
  228. }
  229. }
  230. return r;
  231. };
  232. /******************************************************************************/
  233. // TODO: Avoid possible duplicates
  234. DOMList.prototype.ancestors = function(selector) {
  235. var r = new DOMList();
  236. var n = this.nodes.length;
  237. var node;
  238. for ( var i = 0; i < n; i++ ) {
  239. node = this.nodes[i].parentNode;
  240. while ( node ) {
  241. if ( doesMatchSelector(node, selector) ) {
  242. addNodeToList(r, node);
  243. }
  244. node = node.parentNode;
  245. }
  246. }
  247. return r;
  248. };
  249. /******************************************************************************/
  250. DOMList.prototype.descendants = function(selector) {
  251. var r = new DOMList();
  252. var n = this.nodes.length;
  253. var nl;
  254. for ( var i = 0; i < n; i++ ) {
  255. nl = this.nodes[i].querySelectorAll(selector);
  256. addNodeListToList(r, nl);
  257. }
  258. return r;
  259. };
  260. /******************************************************************************/
  261. DOMList.prototype.contents = function() {
  262. var r = new DOMList();
  263. var cnodes, cn, ci;
  264. var n = this.nodes.length;
  265. for ( var i = 0; i < n; i++ ) {
  266. cnodes = this.nodes[i].childNodes;
  267. cn = cnodes.length;
  268. for ( ci = 0; ci < cn; ci++ ) {
  269. addNodeToList(r, cnodes.item(ci));
  270. }
  271. }
  272. return r;
  273. };
  274. /******************************************************************************/
  275. DOMList.prototype.remove = function() {
  276. var cn, p;
  277. var i = this.nodes.length;
  278. while ( i-- ) {
  279. cn = this.nodes[i];
  280. if ( (p = cn.parentNode) ) {
  281. p.removeChild(cn);
  282. }
  283. }
  284. return this;
  285. };
  286. DOMList.prototype.detach = DOMList.prototype.remove;
  287. /******************************************************************************/
  288. DOMList.prototype.empty = function() {
  289. var node;
  290. var i = this.nodes.length;
  291. while ( i-- ) {
  292. node = this.nodes[i];
  293. while ( node.firstChild ) {
  294. node.removeChild(node.firstChild);
  295. }
  296. }
  297. return this;
  298. };
  299. /******************************************************************************/
  300. DOMList.prototype.append = function(selector, context) {
  301. var p = this.nodes[0];
  302. if ( p ) {
  303. var c = DOMListFactory(selector, context);
  304. var n = c.nodes.length;
  305. for ( var i = 0; i < n; i++ ) {
  306. p.appendChild(c.nodes[i]);
  307. }
  308. }
  309. return this;
  310. };
  311. /******************************************************************************/
  312. DOMList.prototype.prepend = function(selector, context) {
  313. var p = this.nodes[0];
  314. if ( p ) {
  315. var c = DOMListFactory(selector, context);
  316. var i = c.nodes.length;
  317. while ( i-- ) {
  318. p.insertBefore(c.nodes[i], p.firstChild);
  319. }
  320. }
  321. return this;
  322. };
  323. /******************************************************************************/
  324. DOMList.prototype.appendTo = function(selector, context) {
  325. var p = selector instanceof DOMListFactory ? selector : DOMListFactory(selector, context);
  326. var n = p.length;
  327. for ( var i = 0; i < n; i++ ) {
  328. p.nodes[0].appendChild(this.nodes[i]);
  329. }
  330. return this;
  331. };
  332. /******************************************************************************/
  333. DOMList.prototype.insertAfter = function(selector, context) {
  334. if ( this.nodes.length === 0 ) {
  335. return this;
  336. }
  337. var p = this.nodes[0].parentNode;
  338. if ( !p ) {
  339. return this;
  340. }
  341. var c = DOMListFactory(selector, context);
  342. var n = c.nodes.length;
  343. for ( var i = 0; i < n; i++ ) {
  344. p.appendChild(c.nodes[i]);
  345. }
  346. return this;
  347. };
  348. /******************************************************************************/
  349. DOMList.prototype.insertBefore = function(selector, context) {
  350. if ( this.nodes.length === 0 ) {
  351. return this;
  352. }
  353. var referenceNodes = DOMListFactory(selector, context);
  354. if ( referenceNodes.nodes.length === 0 ) {
  355. return this;
  356. }
  357. var referenceNode = referenceNodes.nodes[0];
  358. var parentNode = referenceNode.parentNode;
  359. if ( !parentNode ) {
  360. return this;
  361. }
  362. var n = this.nodes.length;
  363. for ( var i = 0; i < n; i++ ) {
  364. parentNode.insertBefore(this.nodes[i], referenceNode);
  365. }
  366. return this;
  367. };
  368. /******************************************************************************/
  369. DOMList.prototype.clone = function(notDeep) {
  370. var r = new DOMList();
  371. var n = this.nodes.length;
  372. for ( var i = 0; i < n; i++ ) {
  373. addNodeToList(r, this.nodes[i].cloneNode(!notDeep));
  374. }
  375. return r;
  376. };
  377. /******************************************************************************/
  378. DOMList.prototype.nthOfType = function() {
  379. if ( this.nodes.length === 0 ) {
  380. return 0;
  381. }
  382. var node = this.nodes[0];
  383. var tagName = node.tagName;
  384. var i = 1;
  385. while ( node.previousElementSibling !== null ) {
  386. node = node.previousElementSibling;
  387. if ( typeof node.tagName !== 'string' ) {
  388. continue;
  389. }
  390. if ( node.tagName !== tagName ) {
  391. continue;
  392. }
  393. i++;
  394. }
  395. return i;
  396. };
  397. /******************************************************************************/
  398. DOMList.prototype.attr = function(attr, value) {
  399. var i = this.nodes.length;
  400. if ( value === undefined && typeof attr !== 'object' ) {
  401. return i ? this.nodes[0].getAttribute(attr) : undefined;
  402. }
  403. if ( typeof attr === 'object' ) {
  404. var attrNames = Object.keys(attr);
  405. var node, j, attrName;
  406. while ( i-- ) {
  407. node = this.nodes[i];
  408. j = attrNames.length;
  409. while ( j-- ) {
  410. attrName = attrNames[j];
  411. node.setAttribute(attrName, attr[attrName]);
  412. }
  413. }
  414. } else {
  415. while ( i-- ) {
  416. this.nodes[i].setAttribute(attr, value);
  417. }
  418. }
  419. return this;
  420. };
  421. /******************************************************************************/
  422. DOMList.prototype.prop = function(prop, value) {
  423. var i = this.nodes.length;
  424. if ( value === undefined ) {
  425. return i !== 0 ? this.nodes[0][prop] : undefined;
  426. }
  427. while ( i-- ) {
  428. this.nodes[i][prop] = value;
  429. }
  430. return this;
  431. };
  432. /******************************************************************************/
  433. DOMList.prototype.css = function(prop, value) {
  434. var i = this.nodes.length;
  435. if ( value === undefined ) {
  436. return i ? this.nodes[0].style[prop] : undefined;
  437. }
  438. if ( value !== '' ) {
  439. while ( i-- ) {
  440. this.nodes[i].style.setProperty(prop, value);
  441. }
  442. return this;
  443. }
  444. while ( i-- ) {
  445. this.nodes[i].style.removeProperty(prop);
  446. }
  447. return this;
  448. };
  449. /******************************************************************************/
  450. DOMList.prototype.val = function(value) {
  451. return this.prop('value', value);
  452. };
  453. /******************************************************************************/
  454. DOMList.prototype.html = function(html) {
  455. var i = this.nodes.length;
  456. if ( html === undefined ) {
  457. return i ? this.nodes[0].innerHTML : '';
  458. }
  459. while ( i-- ) {
  460. vAPI.insertHTML(this.nodes[i], html);
  461. }
  462. return this;
  463. };
  464. /******************************************************************************/
  465. DOMList.prototype.text = function(text) {
  466. var i = this.nodes.length;
  467. if ( text === undefined ) {
  468. return i ? this.nodes[0].textContent : '';
  469. }
  470. while ( i-- ) {
  471. this.nodes[i].textContent = text;
  472. }
  473. return this;
  474. };
  475. /******************************************************************************/
  476. var toggleClass = function(node, className, targetState) {
  477. var tokenList = node.classList;
  478. if ( tokenList instanceof DOMTokenList === false ) {
  479. return;
  480. }
  481. var currentState = tokenList.contains(className);
  482. var newState = targetState;
  483. if ( newState === undefined ) {
  484. newState = !currentState;
  485. }
  486. if ( newState === currentState ) {
  487. return;
  488. }
  489. tokenList.toggle(className, newState);
  490. };
  491. /******************************************************************************/
  492. DOMList.prototype.hasClass = function(className) {
  493. if ( !this.nodes.length ) {
  494. return false;
  495. }
  496. var tokenList = this.nodes[0].classList;
  497. return tokenList instanceof DOMTokenList &&
  498. tokenList.contains(className);
  499. };
  500. DOMList.prototype.hasClassName = DOMList.prototype.hasClass;
  501. DOMList.prototype.addClass = function(className) {
  502. return this.toggleClass(className, true);
  503. };
  504. DOMList.prototype.removeClass = function(className) {
  505. if ( className !== undefined ) {
  506. return this.toggleClass(className, false);
  507. }
  508. var i = this.nodes.length;
  509. while ( i-- ) {
  510. this.nodes[i].className = '';
  511. }
  512. return this;
  513. };
  514. /******************************************************************************/
  515. DOMList.prototype.toggleClass = function(className, targetState) {
  516. if ( className.indexOf(' ') !== -1 ) {
  517. return this.toggleClasses(className, targetState);
  518. }
  519. var i = this.nodes.length;
  520. while ( i-- ) {
  521. toggleClass(this.nodes[i], className, targetState);
  522. }
  523. return this;
  524. };
  525. /******************************************************************************/
  526. DOMList.prototype.toggleClasses = function(classNames, targetState) {
  527. var tokens = classNames.split(/\s+/);
  528. var i = this.nodes.length;
  529. var node, j;
  530. while ( i-- ) {
  531. node = this.nodes[i];
  532. j = tokens.length;
  533. while ( j-- ) {
  534. toggleClass(node, tokens[j], targetState);
  535. }
  536. }
  537. return this;
  538. };
  539. /******************************************************************************/
  540. var listenerEntries = [];
  541. var ListenerEntry = function(target, type, capture, callback) {
  542. this.target = target;
  543. this.type = type;
  544. this.capture = capture;
  545. this.callback = callback;
  546. target.addEventListener(type, callback, capture);
  547. };
  548. ListenerEntry.prototype.dispose = function() {
  549. this.target.removeEventListener(this.type, this.callback, this.capture);
  550. this.target = null;
  551. this.callback = null;
  552. };
  553. /******************************************************************************/
  554. var makeEventHandler = function(selector, callback) {
  555. return function(event) {
  556. var dispatcher = event.currentTarget;
  557. if ( !dispatcher || typeof dispatcher.querySelectorAll !== 'function' ) {
  558. return;
  559. }
  560. var receiver = event.target;
  561. if ( nodeInNodeList(receiver, dispatcher.querySelectorAll(selector)) ) {
  562. callback.call(receiver, event);
  563. }
  564. };
  565. };
  566. DOMList.prototype.on = function(etype, selector, callback) {
  567. if ( typeof selector === 'function' ) {
  568. callback = selector;
  569. selector = undefined;
  570. } else {
  571. callback = makeEventHandler(selector, callback);
  572. }
  573. var i = this.nodes.length;
  574. while ( i-- ) {
  575. listenerEntries.push(new ListenerEntry(this.nodes[i], etype, selector !== undefined, callback));
  576. }
  577. return this;
  578. };
  579. /******************************************************************************/
  580. // TODO: Won't work for delegated handlers. Need to figure
  581. // what needs to be done.
  582. DOMList.prototype.off = function(evtype, callback) {
  583. var i = this.nodes.length;
  584. while ( i-- ) {
  585. this.nodes[i].removeEventListener(evtype, callback);
  586. }
  587. return this;
  588. };
  589. /******************************************************************************/
  590. DOMList.prototype.trigger = function(etype) {
  591. var ev = new CustomEvent(etype);
  592. var i = this.nodes.length;
  593. while ( i-- ) {
  594. this.nodes[i].dispatchEvent(ev);
  595. }
  596. return this;
  597. };
  598. /******************************************************************************/
  599. // Cleanup
  600. var onBeforeUnload = function() {
  601. var entry;
  602. while ( (entry = listenerEntries.pop()) ) {
  603. entry.dispose();
  604. }
  605. window.removeEventListener('beforeunload', onBeforeUnload);
  606. };
  607. window.addEventListener('beforeunload', onBeforeUnload);
  608. /******************************************************************************/
  609. return DOMListFactory;
  610. })();