├── tests ├── index.html ├── getters.js ├── setters.js ├── constructor.js └── inspiration.js ├── LICENSE ├── tests.html ├── README.md └── domProxy.js /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /tests/getters.js: -------------------------------------------------------------------------------- 1 | import $ from '../domProxy.js'; 2 | 3 | describe('getters primitives', function () { 4 | it('getter className on singel element', function () { 5 | document.body.className = 'test4'; 6 | chai.expect($('body').className).to.equal('test4'); 7 | }); 8 | it('getter on multiple elements (first should be returned)', function () { 9 | $('head').className = 'test_33'; 10 | chai.expect($('head, body').className).to.equal('test_33'); 11 | }); 12 | it('getter on void list should be null?', function () { 13 | chai.expect($('.doesNotExists').className).to.equal(null); 14 | }); 15 | it('ensureId()', function () { 16 | const genId = $('body').ensureId(); 17 | chai.expect(genId).to.not.equal(''); 18 | chai.expect(document.body.id).to.equal(genId); 19 | }); 20 | }); 21 | 22 | describe('getters methods', function () { 23 | }); 24 | 25 | 26 | describe('getters extended', function () { 27 | }); 28 | -------------------------------------------------------------------------------- /tests/setters.js: -------------------------------------------------------------------------------- 1 | import $ from '../domProxy.js'; 2 | 3 | describe('setter', function () { 4 | const body = document.body; 5 | it('setting className on singel element', function () { 6 | $(body).className = 'test'; 7 | chai.expect(body.className).to.equal('test'); 8 | }); 9 | it('setting className on multiple elements', function () { 10 | $('body, head').className = 'test2'; 11 | chai.expect(document.body.className).to.equal('test2'); 12 | chai.expect(document.head.className).to.equal('test2'); 13 | }); 14 | it('setting multiple events', function () { 15 | const listener = function(){ }; 16 | $('body, head').onmouseover = listener; 17 | chai.expect(document.body.onmouseover).to.equal(listener); 18 | chai.expect(document.head.onmouseover).to.equal(listener); 19 | }); 20 | it('setting on void list', function () { 21 | $('.doesnotExists').classList = 'xyz'; 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tobias Buschor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | domProxy tests 7 | 8 |

domProxy tests

9 | 10 | 27 | 28 | 29 | 66 | -------------------------------------------------------------------------------- /tests/constructor.js: -------------------------------------------------------------------------------- 1 | import $ from '../domProxy.js'; 2 | 3 | describe('constructor single element', function () { 4 | const body = document.body; 5 | it('first entry should be the element', function () { 6 | const domProxy = $(body); 7 | chai.expect(domProxy.values().next().value).to.equal(body); 8 | }); 9 | it('size should be 1', function () { 10 | const body = document.body; 11 | const domProxy = $(body); 12 | chai.expect(domProxy.size).to.equal(1); 13 | }); 14 | }); 15 | 16 | describe('constructor html content', function () { 17 | it('first element should be "TR"', function () { 18 | const domProxy = $(''); 19 | chai.expect(domProxy.values().next().value.tagName).to.equal('TR'); 20 | }); 21 | it('first element should be "svg', function () { 22 | const domProxy = $(''); 23 | chai.expect(domProxy.values().next().value.tagName).to.equal('svg'); 24 | }); 25 | it('should have 2 elements', function () { 26 | const domProxy = $('12'); 27 | const size = domProxy.size; 28 | chai.expect(size).to.equal(2); 29 | }); 30 | it('should have 3 elements (one textnode)', function () { 31 | const domProxy = $('
text
'); 32 | const size = domProxy.size; 33 | chai.expect(size).to.equal(3); 34 | }); 35 | }); 36 | 37 | describe('constructor selector', function () { 38 | it('selector ":root', function () { 39 | const domProxy = $(':root'); 40 | chai.expect(domProxy.values().next().value).to.equal(document.documentElement); 41 | }); 42 | }); 43 | 44 | 45 | describe('constructor nodeLists', function () { 46 | it('htmlCollection works (.children)', function () { 47 | const domProxy = $(document.documentElement.children); 48 | const size = domProxy.size; 49 | chai.expect(size).to.equal(2); 50 | }); 51 | it('nodeList works (qurySelectorAll)', function () { 52 | const domProxy = $(document.documentElement.querySelectorAll('head, body')); 53 | const size = domProxy.size; 54 | chai.expect(size).to.equal(2); 55 | }); 56 | it('array works', function () { 57 | const domProxy = $([document.body, document.head]); 58 | const size = domProxy.size; 59 | chai.expect(size).to.equal(2); 60 | }); 61 | }); 62 | 63 | describe('constructor Set', function () { 64 | it('ensure a clone is not a reference', function () { 65 | const original = $('body') 66 | const clone = $(original); 67 | original.add(document.head); 68 | chai.expect(clone.size).to.equal(1); 69 | chai.expect(original.size).to.equal(2); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # domProxy 2 | 3 | lightweight **under 1KB** (gzip) at the moment! 4 | I ask you to test and participate 5 | 6 | ## What jQuery would look like if it was released in 2021 7 | Some things i still miss from jQuery, like assigning multiple events to multiple elements in one go. 8 | `elements.addEventListener('click focus', listener)` 9 | Using js-proxies, its now possible to do this with very little code. 10 | The goal of this project is to integrate only the really useful apis. 11 | 12 | ### Ussage 13 | 14 | ```js 15 | import domProxy from './domProxy.js'; 16 | 17 | // children is also a domProxy 18 | domProxy('.el').children.hidden = true; 19 | // chaining 20 | domProxy('.el').setAttribute('data-b','x').setAttribute('data-b', 'y'); 21 | // traversal methods: second argument means including the element itself 22 | domProxy('.el').nextAll('.deletable', true).remove(); 23 | ``` 24 | 25 | ### How it works 26 | 27 | All nodes are wrapped with a proxy. 28 | When accessing a property of the nodeList, the lib first checks if there is an own property, if not, the correct property of the elements is used. 29 | If a method returns `undefined ` on elements, the domProxy will be returned to allow chaining. (addEventListener, setAttribute, ...) 30 | 31 | 32 | ### API 33 | 34 | ```js 35 | // constructor: 36 | domProxy(element) // or: 37 | domProxy([element, element, ...]) // also nodeList, htmlCollection, Set... or: 38 | domProxy('
html-elements

test

') // or: 39 | domProxy('.selector') 40 | 41 | nodeList.next(selector?, includingSelf?) 42 | // returns next sibling that matches 43 | 44 | nodeList.nextAll(selector?, includingSelf?) 45 | // returns all next siblings that matches 46 | 47 | nodeList.prev(selector?, includingSelf?) 48 | // returns previous sibling that matches 49 | 50 | nodeList.prevAll(selector?, includingSelf?) 51 | // returns all previous matching siblings 52 | 53 | nodeList.parent(selector?, includingSelf?) 54 | // returns next parent that matches 55 | 56 | nodeList.parentAll(selector?, includingSelf?) 57 | // returns all parents matching 58 | 59 | nodeList.ensureId() 60 | // returns the id of the Element, if it has none, it generates one 61 | 62 | nodeList.on(types, listener, options) 63 | // add event listeners for multiple types (eg.'click mouseover') 64 | 65 | nodeList.off(types, listener, options) 66 | // remove event listeners for multiple types (eg.'click mouseover') 67 | 68 | nodeList.trigger(type, init) 69 | // like dispatchEvent(new CustomEvent(type, init)) but defaults to bubbles:true 70 | 71 | nodeList.css(name, value?) 72 | // get or set (value) styles 73 | 74 | 75 | // ...and every api available on the Elements itself 76 | nodeList.getAttribute 77 | nodeList.setAttribute 78 | nodeList.hasAttribute 79 | nodeList.closest 80 | nodeList.matches 81 | nodeList.insertAdjacentElement 82 | nodeList.insertAdjacentText 83 | nodeList.getBoundingClientRect 84 | nodeList.querySelector 85 | nodeList.querySelectorAll 86 | nodeList.before 87 | nodeList.after 88 | nodeList.replaceWith 89 | nodeList.remove 90 | nodeList.prepend 91 | nodeList.append 92 | nodeList.innerHTML 93 | nodeList.children 94 | nodeList.cloneNode 95 | nodeList.addEventListener 96 | ... 97 | ... 98 | ``` 99 | 100 | ### Take part 101 | I am very happy about every participation, even if it is only spelling corrections. 102 | And leave a star if you like it! 103 | 104 | 105 | ### Similar projects 106 | 107 | https://gist.github.com/AdaRoseCannon/d95a7cbb8edd730443c62f0daff875ac 108 | 109 | https://github.com/WebReflection/handy-wrap 110 | -------------------------------------------------------------------------------- /domProxy.js: -------------------------------------------------------------------------------- 1 | 2 | export const domProxy = arg => { 3 | const list = new Set( 4 | arg instanceof Node 5 | ? [arg] 6 | : typeof arg === 'string' 7 | ? (arg[0] === '<' ? strToDom(arg) : d.querySelectorAll(arg)) 8 | : arg 9 | ); 10 | return new Proxy(list, handler); 11 | } 12 | 13 | const undef = void 0; 14 | const d = document; 15 | const strToDom = str => { 16 | const el = d.createElement('template'); 17 | el.innerHTML = str; 18 | return el.content.childNodes; 19 | } 20 | 21 | const handler = { 22 | get(elements, prop){ 23 | // first check if prop exists in the "Set" (keys, forEach, size, add, ...) 24 | if (prop in elements) { 25 | const property = elements[prop] 26 | return typeof property === 'function' ? property.bind(elements) : property; 27 | } 28 | // then check if an extended property exists 29 | if (Object.hasOwn(extensions, prop)) { 30 | return function(...args){ 31 | return returnFromElements(this, elements, element=>extensions[prop](element, ...args)) 32 | } 33 | } 34 | // then check if its a property from the elements 35 | const first = elements.values().next().value; 36 | if (!first) return null; 37 | if (typeof first[prop] === 'function') { // better ask the htmlElement-prototype if it should be function? 38 | return function(...args){ 39 | return returnFromElements(this, elements, element=>{ 40 | return element[prop].apply(element, args); 41 | }) 42 | } 43 | } 44 | return returnFromElements(this, elements, element=>element[prop]); 45 | }, 46 | set(elements, prop, value){ 47 | for (const element of elements) element[prop] = value; 48 | return true; 49 | } 50 | } 51 | 52 | const returnFromElements = (proxy, elements, call)=>{ 53 | const returns = new Set(); 54 | let returnsUndefined = false; 55 | for (const element of elements) { 56 | const value = call(element); 57 | if (typeof value === 'string') return value; // if its a string return from first element 58 | if (value === undef) { // chain if return value is undefined 59 | returnsUndefined = true; 60 | } else if (typeof value?.[Symbol.iterator] === 'function') { // add items if it is iterable 61 | for (const item of value) returns.add(item); 62 | } else if (value instanceof Node) { // add items if its node 63 | returns.add(value); 64 | } else { 65 | return value; // if others, return value from first element 66 | } 67 | } 68 | if (returnsUndefined) return proxy; 69 | return domProxy(returns); 70 | } 71 | 72 | function *walkGen(el, operation, selector, incMe){ 73 | if (!incMe) el = el[operation]; 74 | while (el) { 75 | if (!selector || el.matches(selector)) yield el; 76 | el = el[operation]; 77 | } 78 | } 79 | 80 | const extensions = { 81 | nextAll :(el, sel, incMe) => walkGen(el, 'nextElementSibling', sel, incMe), 82 | prevAll :(el, sel, incMe) => walkGen(el, 'previousElementSibling', sel, incMe) , 83 | parentAll:(el, sel, incMe) => walkGen(el, 'parentNode', sel, incMe) , 84 | next(...args) { return this.nextAll(...args).next().value }, 85 | prev(...args) { return this.prevAll(...args).next().value }, 86 | parent(...args){ return this.parentAll(...args).next().value }, 87 | ensureId:(el) => el.id || (el.id = 'gen-'+Math.random().toString(36).substring(2, 10)), 88 | on(el, types, listener, options){ 89 | for (const type of types.split(/\s/)) { 90 | el.addEventListener(type, listener, options); 91 | } 92 | }, 93 | off(el, types, listener, options){ 94 | for (const type of types.split(/\s/)) { 95 | el.removeEventListener(type, listener, options); 96 | } 97 | }, 98 | trigger(el, type, options={}){ 99 | options.bubbles ??= true; // default bubbles 100 | el.dispatchEvent(new Event(type, options)); 101 | }, 102 | css(el, prop, value){ 103 | if (value === undef) return getComputedStyle(el)[prop]; 104 | el.style[prop] = value; 105 | }, 106 | } 107 | 108 | export default domProxy; 109 | -------------------------------------------------------------------------------- /tests/inspiration.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 Tobias Buschor https://goo.gl/gl0mbf | MIT License https://goo.gl/HgajeK */ 2 | 3 | 4 | // allow object as arguments 5 | const objectifyArgs = fn=>{ 6 | return function(el, name, ...rest){ 7 | if (typeof name === 'string') { 8 | return fn.call(this, el, name, ...rest); 9 | } 10 | for (let key in name) { 11 | fn.call(this, el, key, name[key], ...rest); 12 | } 13 | } 14 | } 15 | extensions.css = objectifyArgs((el, prop, value)=>{ 16 | if (value === undef) return getComputedStyle(el)[prop]; 17 | el.style[prop] = value; 18 | }); 19 | 20 | 21 | // The.js 22 | attr(name,value){ 23 | if(value===undf) return this.getAttribute(name); 24 | if(value===null) return this.removeAttribute(name); 25 | this.setAttribute(name,value); 26 | }, 27 | addClass(v){ !this.hsCl(v) && (this.className += ' '+v); }, 28 | removeClass(v){ this.className = this.className.replace(new RegExp("(^|\\s)"+v+"(\\s|$)"), '');}, 29 | hasClass(v){ return this.className.contains(v,' '); }, 30 | is(sel){ 31 | if (this===d) return sel===this; // ie9 on document 32 | return (sel.dlg ? sel===this : this.matches(sel) ) && this; 33 | }, 34 | children(sel){ return sel ? this.ch().is(sel) : $.cNL(this.children); }, 35 | has(el,incMe){ return this===el ? (incMe?this:false) : this.contains(el) && this; }, 36 | add(el,who){ 37 | var trans = {after:'afterEnd',bottom:'beforeEnd',before:'beforeBegin',top:'afterBegin'}; 38 | this['insertAdjacent'+(el.p?'Element':'HTML')](trans[who||'bottom'],el); 39 | }, 40 | inject(el,who){ el.ad(this,who); }, 41 | delegate(sel, types, cb){ 42 | return this.on(types, function(event){ 43 | var t = event.target.p ? event.target : event.target.parentNode; // for textnodes 44 | var el = t.p(sel,1); 45 | el && el!==d && cb.call(el,event); 46 | }); 47 | }, 48 | zTop(){ 49 | var p=this.p(), z=p.$zTop; 50 | if(!z){ 51 | for (var i=0, el, cs=p.ch(), elZ; el = cs[i++];){ 52 | elZ = el.css('z-index')*1; 53 | z = Math.max(z,elZ);//elZ > z ? elZ : z; 54 | } 55 | } 56 | p!==d && p.zTop(); 57 | //p.style.zIndex = p.css('z-index')*1||0; // prevent mix with other contexts (override default auto) 58 | z = z||0; 59 | this.style.zIndex = p.$zTop = z+1; 60 | }, 61 | html(v){ this.innerHTML = v; }, 62 | 63 | 64 | 65 | 66 | /// c1.dom 67 | var d = document; 68 | 69 | q1.dom = function(id) { 70 | return document.getElementById(id); 71 | }; 72 | 73 | /* usefull */ 74 | Node.prototype.q1RemoveNode = function(removeChildren) { 75 | if (removeChildren) return this.remove(); 76 | var range = d.createRange(); 77 | range.selectNodeContents(this); 78 | return this.parentNode.replaceChild(range.extractContents(), this); 79 | }; 80 | Node.prototype.q1ReplaceNode = function(el) { 81 | this.parentNode && this.parentNode.insertBefore(el,this); 82 | el.appendChild(this); 83 | this.q1RemoveNode(); 84 | }; 85 | Node.prototype.q1Rect = function(rct) { 86 | if (rct) { 87 | this.style.top = rct.y+'px'; 88 | this.style.left = rct.x+'px'; 89 | this.style.width = rct.w+'px'; 90 | this.style.height = rct.h+'px'; 91 | } else { 92 | var pos = this.getBoundingClientRect(); 93 | return new Rect({ 94 | x:pos.left+pageXOffset, 95 | y:pos.top+pageYOffset, 96 | w:pos.width, 97 | h:pos.height 98 | }); 99 | } 100 | }; 101 | Node.prototype.q1Position = function(rct) { 102 | if (rct) { 103 | this.style.top = rct.y+'px'; 104 | this.style.left = rct.x+'px'; 105 | } else { 106 | var pos = this.getBoundingClientRect(); 107 | return new Rect({ 108 | x: pos.left+pageXOffset, 109 | y: pos.top+pageYOffset, 110 | w: 0, 111 | h: 0 112 | }); 113 | } 114 | }; 115 | 116 | 117 | class Rect { 118 | constructor(obj){ 119 | if (!(this instanceof q1.rect)) return new q1.rect(obj); 120 | this.x = obj.x; 121 | this.y = obj.y; 122 | this.w = obj.w = 0; 123 | this.h = obj.h = 0; 124 | } 125 | r() { return this.x + this.w } 126 | b() { return this.y + this.h; }, 127 | isInX(rct) { return rct.x > this.x && rct.r() < this.r(); }, 128 | isInY(rct) { return rct.y > this.y && rct.b() < this.b(); }, 129 | isIn(rct) { return this.isInX(rct) && this.isInY(rct); }, 130 | touchesX(rct) { return rct.x < this.r() && rct.r() > this.x; }, 131 | touchesY(rct) { return rct.y < this.b() && rct.b() > this.y; }, 132 | touches(rct) { return this.touchesX(rct) && this.touchesY(rct); }, 133 | grow(value) { this.w += value; this.h += value; }, 134 | area() { return this.h * this.w; }, 135 | } 136 | 137 | 138 | 139 | 140 | 141 | d.q1NodeFromPoint = function(x, y) { 142 | /*document.caretRangeFromPoint for chrome?*/ 143 | //caretPositionFromPoint 144 | if (y===undefined) { 145 | y = x.y; 146 | x = x.x; 147 | } 148 | var el = d.elementFromPoint(x, y); 149 | var nodes = el.childNodes; 150 | for (var i = 0, n; n = nodes[i++];) { 151 | if (n.nodeType === 3) { 152 | var r = d.createRange(); 153 | r.selectNode(n); 154 | var rects = r.getClientRects(); 155 | for (var j = 0, rect; rect = rects[j++];) { 156 | if (x > rect.left && x < rect.right && y > rect.top && y < rect.bottom) { 157 | return n; 158 | } 159 | } 160 | } 161 | } 162 | return el; 163 | }; 164 | --------------------------------------------------------------------------------