udom.js 16 KB

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