diff --git a/.gitignore b/.gitignore index 10d09b5..c5a0c24 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ npm-debug.log .idea test/domassist.test.dist.js dist +*js.map diff --git a/lib/idStorage.js b/lib/idStorage.js new file mode 100644 index 0000000..23f3106 --- /dev/null +++ b/lib/idStorage.js @@ -0,0 +1,61 @@ +const storage = new Map(); + +const idStorage = { + areSimilar(a, b) { + const everyKey = f => Object.keys(a).every(f); + const type = ({}).toString + .call(a) + .match(/\[object (\w+)\]/)[1]; + + switch (type) { + case 'Array': + return a.length === b.length && + everyKey(k => idStorage.areSimilar(a.sort()[k], b.sort()[k])); + case 'Object': + return Object.keys(a).length === Object.keys(b).length && + everyKey(k => idStorage.areSimilar(a[k], b[k])); + default: + return a === b; + } + }, + + add(data) { + const id = idStorage.generateID(); + storage.set(id, data); + + return id; + }, + + generateID() { + const h = n => (n | 0).toString(16); + const s = n => h((Math.random() * (1 << (n << 2))) ^ Date.now()).slice(-n); + + return [ + s(4) + s(4), s(4), + `4${s(3)}`, + h(8 | (Math.random() * 4)) + s(3), + Date.now().toString(16).slice(-10) + s(2) + ].join('-'); + }, + + removeFromID(id) { + const val = storage.get(id); + storage.delete(id); + + return val; + }, + + removeFromValue(value) { + let found = null; + + storage.forEach((val, key) => { + if (!found && idStorage.areSimilar(val, value)) { + found = key; + } + }); + + return idStorage.removeFromID(found); + } +}; + +export default idStorage; diff --git a/lib/modify.js b/lib/modify.js index 2a89fc7..48e48a2 100644 --- a/lib/modify.js +++ b/lib/modify.js @@ -6,9 +6,7 @@ import on from './on'; import styles from './styles'; function bindEvents(el, events) { - Object.keys(events).forEach((event) => { - on(el, event, events[event]); - }); + return Object.keys(events).map(event => on(el, event, events[event])); } function modify(selector, params) { @@ -25,11 +23,12 @@ function modify(selector, params) { const els = find(selector); if (els.length) { els.forEach((el) => { - Object.keys(params).forEach((param, index) => { + Object.keys(params).forEach(param => { if (param in modules) { if (param === 'events') { - bindEvents(el, params[param]); + return bindEvents(el, params[param]); } + modules[param](el, params[param]); } }); diff --git a/lib/off.js b/lib/off.js index a859db8..980fafd 100644 --- a/lib/off.js +++ b/lib/off.js @@ -1,22 +1,33 @@ import find from './find'; +import idStorage from './idStorage'; -function off(selector, event) { +function off(selector, event, handler, capture = false) { if (Array.isArray(selector)) { - selector.forEach((item) => off(item, event)); - } - if (!window._domassistevents) { - window._domassistevents = {}; + selector.forEach((item) => off(item, event, handler, capture)); } - const data = window._domassistevents[`_${event}`]; + // ID + if (typeof selector === 'string' && typeof event === 'undefined') { + const data = idStorage.removeFromID(selector); + + if (!data) { + return; + } - if (!data) { - return; + return off(data.el, data.event, data.handler, data.capture); } + const el = find(selector); + if (el.length) { el.forEach((item) => { - item.removeEventListener(event, data.cb, data.capture); + item.removeEventListener(event, handler); + idStorage.removeFromValue({ + el: item, + event, + handler, + capture + }); }); } } diff --git a/lib/on.js b/lib/on.js index 713ade5..a83dc77 100644 --- a/lib/on.js +++ b/lib/on.js @@ -1,27 +1,33 @@ import find from './find'; +import idStorage from './idStorage'; -function on(selector, event, cb, capture = false) { +function on(selector, event, handler, capture = false) { if (Array.isArray(selector)) { - selector.forEach((item) => on(item, event, cb, capture)); - return; + return Array.prototype.concat.apply([], + selector.map(item => on(item, event, handler, capture))); } - const data = { - cb, - capture - }; - - if (!window._domassistevents) { - window._domassistevents = {}; - } - - window._domassistevents[`_${event}`] = data; const el = find(selector); + let ids = null; + if (el.length) { - el.forEach((item) => { - item.addEventListener(event, cb, capture); + ids = el.map(item => { + item.addEventListener(event, handler); + + return idStorage.add({ + el: item, + event, + handler, + capture + }); }); + + if (ids.length === 1) { + ids = ids[0]; + } } + + return ids; } export default on; diff --git a/lib/once.js b/lib/once.js index 6582f68..5069cbd 100644 --- a/lib/once.js +++ b/lib/once.js @@ -1,10 +1,24 @@ import on from './on'; import off from './off'; +import find from './find'; -function once(el, event, run, capture = false) { - on(el, event, e => { - off(el, event); - run(e); +function once(selector, event, handler, capture = false) { + if (Array.isArray(selector)) { + selector.forEach(item => once(item, event, handler, capture)); + return; + } + + const el = find(selector); + + if (el.length > 1) { + // Need to get the event id for each element so once runs properly + once(el, event, handler, capture); + return; + } + + const eventId = on(el, event, e => { + off(eventId); + handler(e); }, capture); } diff --git a/package.json b/package.json index 7686366..2f51348 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "eslint-config-firstandthird": "3.2.0", "eslint-plugin-import": "^2.1.0", + "map-polyfill": "0.0.1", "phantomjs-prebuilt": "^2.1.14", "scriptkit": "0.2.0", "static-server": "^2.0.3", diff --git a/test/domassist.test.js b/test/domassist.test.js index 1c372dc..7fef05c 100644 --- a/test/domassist.test.js +++ b/test/domassist.test.js @@ -1,5 +1,6 @@ /* eslint no-console: 0 */ +import '../node_modules/map-polyfill/dist/map.min'; import domassist from '../domassist'; import test from 'tape-rollup'; import { teardown } from './setup'; @@ -14,8 +15,7 @@ import './modify.test'; import './show-hide.test'; import './styles.test'; import './append.test'; - -const page = window.phantom.page; +import TestUtils from './test-utils'; test('ready', assert => { // if you add more assertions update this number @@ -117,9 +117,7 @@ test('Events - delegate', assert => { `; const button = domassist.findOne('button', el); - const pos = button.getBoundingClientRect(); - - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); + button.click(); }); test('Events - once', assert => { @@ -130,17 +128,15 @@ test('Events - once', assert => { `; const link = domassist.findOne('a', el); - const pos = link.getBoundingClientRect(); let clicks = 0; - domassist.once(link, 'click', e => { clicks++; }); - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); + link.click(); + link.click(); + link.click(); setTimeout(() => { assert.equal(clicks, 1, 'Only fired once'); @@ -152,21 +148,21 @@ test('Events - hover', assert => { const el = domassist.findOne('#domassist'); el.innerHTML = ` -
+
`; const box = domassist.findOne('div', el); - const pos = box.getBoundingClientRect(); domassist.hover(box, e => { - assert.ok(e instanceof MouseEvent, 'Enter fired'); + assert.pass('Enter fired'); assert.equal(e.type, 'mouseenter', 'Correct event'); }, e => { - assert.ok(e instanceof MouseEvent, 'Leave fired'); + assert.pass('Leave fired'); assert.equal(e.type, 'mouseleave', 'Correct event'); - assert.end(); }); - page.sendEvent('mousemove', pos.left + pos.width / 2, pos.top + pos.height / 2); - page.sendEvent('mousemove', pos.left + pos.width + 100, pos.top + pos.height + 100); + TestUtils.fireEvent(box, 'mouseenter'); + TestUtils.fireEvent(box, 'mouseleave'); + + assert.end(); }); diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..cb44594 --- /dev/null +++ b/test/index.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + + + diff --git a/test/modify.test.js b/test/modify.test.js index 2061ffa..9c75777 100644 --- a/test/modify.test.js +++ b/test/modify.test.js @@ -1,5 +1,6 @@ import domassist from '../domassist'; import test from 'tape-rollup'; +import TestUtils from './test-utils'; const setup = (total) => { const frag = document.createDocumentFragment(); @@ -21,8 +22,6 @@ const teardown = (el) => { } }; -const page = window.phantom.page; - test('modify - add class', assert => { const el = domassist.findOne('#domassist'); domassist.modify(el, { @@ -71,33 +70,31 @@ test('modify - html', assert => { }); // test('modify - events', assert => { - // assert.plan(5); const el = domassist.findOne('#domassist'); el.innerHTML = ` Click `; const link = domassist.findOne('a'); - const pos = link.getBoundingClientRect(); domassist.modify(link, { events: { click: (e) => { - assert.ok(e instanceof MouseEvent, 'Click event fired'); + assert.pass('Click fired'); }, mouseenter: (e) => { - assert.ok(e instanceof MouseEvent, 'Enter fired'); + assert.pass('Enter fired'); assert.equal(e.type, 'mouseenter', 'Correct event'); }, mouseleave: (e) => { - assert.ok(e instanceof MouseEvent, 'Leave fired'); + assert.pass('Leave fired'); assert.equal(e.type, 'mouseleave', 'Correct event'); - assert.end(); } } }); - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); - page.sendEvent('mousemove', pos.left + pos.width / 2, pos.top + pos.height / 2); - page.sendEvent('mousemove', pos.left + pos.width + 100, pos.top + pos.height + 100); + link.click(); + TestUtils.fireEvent(link, 'mouseenter'); + TestUtils.fireEvent(link, 'mouseleave'); + assert.end(); }); test('modify - styles', assert => { const el = domassist.findOne('#domassist'); diff --git a/test/off.test.js b/test/off.test.js index 87a8b9d..9fef0cb 100644 --- a/test/off.test.js +++ b/test/off.test.js @@ -1,36 +1,73 @@ import domassist from '../domassist'; import test from 'tape-rollup'; -const page = window.phantom.page; - -test('Events - off single element', assert => { +test('Events - off single element single event', assert => { const el = domassist.findOne('#domassist'); - assert.plan(1); el.innerHTML = ` Click `; const link = domassist.findOne('a', el); - const pos = link.getBoundingClientRect(); + const handler = () => { + assert.fail('I should not fire'); + }; + + domassist.on(link, 'click', handler); + domassist.off(link, 'click', handler); + + link.click(); + + assert.ok('No events fired'); + assert.end(); +}); + +test('Events - off multiple elements', assert => { + const el = domassist.findOne('#domassist'); + el.innerHTML = ` + Click + Click + Click + Click + `; + + const links = domassist.find('a', el); + const handler = () => { + assert.fail('I should never fire'); + }; - let clicked = false; + domassist.on(links, 'click', handler); + domassist.off(links, 'click', handler); - domassist.on(link, 'click', e => { - clicked = true; + links.forEach(item => { + item.click(); }); - domassist.off(link, 'click'); + assert.ok('No events fired'); + assert.end(); +}); + +test('Events - off with id', assert => { + const el = domassist.findOne('#domassist'); + el.innerHTML = ` + Click + `; + + const link = domassist.findOne('a', el); + const handler = () => { + assert.fail('I should not fire'); + }; - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); + const id = domassist.on(link, 'click', handler); + domassist.off(id); - setTimeout(() => { - assert.ok(!clicked, 'Event not fired'); - }, 500); + link.click(); + + assert.ok('No events fired'); + assert.end(); }); -test('Events - off multiple elements', assert => { +test('Events - off with array of ids', assert => { const el = domassist.findOne('#domassist'); - assert.plan(4); el.innerHTML = ` Click Click @@ -39,20 +76,20 @@ test('Events - off multiple elements', assert => { `; const links = domassist.find('a', el); - domassist.on(links, 'click', e => { - const id = parseInt(e.target.dataset.id.replace('link-', ''), 10); - const div = document.createElement('div'); - div.id = `id-${id}`; - el.appendChild(div); - }); - // domassist.off(links, 'click'); - links.forEach((item, index) => { - const pos = item.getBoundingClientRect(); - const id = `id-${index + 1}`; - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); - const div = domassist.findOne(`#${id}`); - setTimeout(() => { - assert.equal(div.id, id, `Element with ID link-${index + 1} has no click event`); - }, 500); + const handler = () => { + assert.fail('I should never fire'); + }; + + const ids = domassist.on(links, 'click', handler); + + assert.ok(Array.isArray(ids), 'Ids should be an array'); + assert.equal(ids.length, 4, 'Should have 4 ids'); + + domassist.off(ids); + links.forEach(item => { + item.click(); }); + + assert.ok('No events fired'); + assert.end(); }); diff --git a/test/on.test.js b/test/on.test.js index a38d0fd..80f6be5 100644 --- a/test/on.test.js +++ b/test/on.test.js @@ -1,8 +1,6 @@ import domassist from '../domassist'; import test from 'tape-rollup'; -const page = window.phantom.page; - test('Events - on single element', assert => { const el = domassist.findOne('#domassist'); assert.plan(1); @@ -10,13 +8,12 @@ test('Events - on single element', assert => { Click `; const link = domassist.findOne('a'); - const pos = link.getBoundingClientRect(); domassist.on(link, 'click', e => { assert.ok(e instanceof MouseEvent, 'Event fired'); }); - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); + link.click(); }); test('Events - on multiple elements', assert => { @@ -34,8 +31,7 @@ test('Events - on multiple elements', assert => { assert.equal(e.target.id, `link-${index}`, `Link with id of link-${index} fired`); }); links.forEach((item) => { - const pos = item.getBoundingClientRect(); - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); + item.click(); index += 1; }); }); diff --git a/test/test-utils.js b/test/test-utils.js new file mode 100644 index 0000000..18957f4 --- /dev/null +++ b/test/test-utils.js @@ -0,0 +1,8 @@ +export default { + fireEvent(el, type) { + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(type, false, false, null); + + el.dispatchEvent(evt); + } +};