diff --git a/.gitignore b/.gitignore index 10d09b5..812ee6a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ npm-debug.log .idea test/domassist.test.dist.js dist +*.map +.DS_Store diff --git a/lib/modify.js b/lib/modify.js index 2a89fc7..e048e6b 100644 --- a/lib/modify.js +++ b/lib/modify.js @@ -19,17 +19,18 @@ function modify(selector, params) { addClass, removeClass, html, - events: on, - styles, + styles }; 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; } + modules[param](el, params[param]); } }); diff --git a/lib/off.js b/lib/off.js index a859db8..8eca807 100644 --- a/lib/off.js +++ b/lib/off.js @@ -1,22 +1,70 @@ import find from './find'; +import privData from './privData'; -function off(selector, event) { - if (Array.isArray(selector)) { - selector.forEach((item) => off(item, event)); +function extractFromDict(element, key, handler, justNameSpace) { + const eventDict = privData.get(element, 'events'); + let eventsToUnbind = []; + let type = key; + let namespace = ''; + + // Allowing namespace to continue + if (type.indexOf('.') > 0) { + const tmp = type.split('.'); + + type = tmp.shift(); + namespace = `.${tmp.join('.')}`; + + // Let's remove the namespaced events + extractFromDict(element, namespace, handler, true); } - if (!window._domassistevents) { - window._domassistevents = {}; + + let filtered = eventDict[type]; + + if (filtered) { + if (typeof handler !== 'undefined') { + // Just those that match the handler + filtered = filtered.filter(d => d.cb === handler); + } + + eventsToUnbind = eventsToUnbind.concat( + filtered.map(data => ({ type, data })) + ); + + // Actually removing from the dict + if (typeof handler !== 'undefined') { + eventDict[type] = eventDict[type].filter(d => d.cb !== handler); + } else { + eventDict[type].length = 0; + } + + // Don't keep the key, prevent leaks + if (eventDict[type].length === 0) { + eventDict[type] = undefined; + } + } + + // When unbinding for .namespace need to actually remove the normal events too + if (!justNameSpace && type.indexOf('.') === 0) { + // Flat map + eventsToUnbind = Array.prototype.concat.apply([], + eventsToUnbind.map(({ data }) => extractFromDict(element, data.type, data.cb))); } - const data = window._domassistevents[`_${event}`]; + return eventsToUnbind; +} - if (!data) { - return; +function off(selector, event, handler) { + if (Array.isArray(selector)) { + selector.forEach((item) => off(item, event, handler)); } + const el = find(selector); + if (el.length) { - el.forEach((item) => { - item.removeEventListener(event, data.cb, data.capture); + el.forEach(item => { + extractFromDict(item, event, handler).forEach(({ type, data }) => { + item.removeEventListener(type, data.cb, data.capture); + }); }); } } diff --git a/lib/on.js b/lib/on.js index 713ade5..b70aa1f 100644 --- a/lib/on.js +++ b/lib/on.js @@ -1,4 +1,14 @@ import find from './find'; +import privData from './privData'; + +function storeIntoDict(element, key, data) { + const eventDict = privData.get(element, 'events'); + if (!eventDict[key]) { + eventDict[key] = []; + } + + eventDict[key].push(data); +} function on(selector, event, cb, capture = false) { if (Array.isArray(selector)) { @@ -6,20 +16,33 @@ function on(selector, event, cb, capture = false) { return; } + const el = find(selector); + let type = event; + let storeKeys = [type]; + let namespaces = ''; + if (type.indexOf('.') > -1) { + const tmp = type.split('.'); + + // Not allowing to bind to .ns + if (!tmp[0]) { + return; + } + + type = tmp.shift(); + namespaces = `.${tmp.join('.')}`; + storeKeys = [type, namespaces]; + } + const data = { + type, cb, capture }; - if (!window._domassistevents) { - window._domassistevents = {}; - } - - window._domassistevents[`_${event}`] = data; - const el = find(selector); if (el.length) { - el.forEach((item) => { - item.addEventListener(event, cb, capture); + el.forEach(item => { + storeKeys.forEach(key => storeIntoDict(item, key, data)); + item.addEventListener(type, cb, capture); }); } } diff --git a/lib/privData.js b/lib/privData.js new file mode 100644 index 0000000..7417305 --- /dev/null +++ b/lib/privData.js @@ -0,0 +1,39 @@ +class Data { + constructor() { + this.uuid = `Domassist${`${Math.random()}`.replace(/\D/g, '')}`; + } + + cache(owner) { + let value = owner[this.uuid]; + + if (!value) { + value = {}; + + // If it's a DOM Node + if (owner.nodeType) { + owner[this.uuid] = value; + } else { + Object.defineProperty(owner, this.uuid, { + value, + configurable: true + }); + } + } + + return value; + } + + get(element, key, def = {}) { + const cache = this.cache(element); + let value = cache[key]; + + if (!value) { + cache[key] = def; + value = cache[key]; + } + + return value; + } +} + +export default new Data(); diff --git a/test/domassist.test.js b/test/domassist.test.js index 1c372dc..93dfc36 100644 --- a/test/domassist.test.js +++ b/test/domassist.test.js @@ -3,6 +3,7 @@ import domassist from '../domassist'; import test from 'tape-rollup'; import { teardown } from './setup'; +import './event-namespacing.test'; import './find.test'; import './classes.test'; import './attrs.test'; @@ -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,7 +128,6 @@ test('Events - once', assert => { `; const link = domassist.findOne('a', el); - const pos = link.getBoundingClientRect(); let clicks = 0; @@ -138,9 +135,9 @@ test('Events - once', assert => { 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 +149,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/event-namespacing.test.js b/test/event-namespacing.test.js new file mode 100644 index 0000000..3b61eea --- /dev/null +++ b/test/event-namespacing.test.js @@ -0,0 +1,72 @@ +import domassist from '../domassist'; +import test from 'tape-rollup'; +import TestUtils from './test-utils'; + +test('Events - Namespaced should fire', assert => { + const el = domassist.findOne('#domassist'); + assert.plan(1); + el.innerHTML = ` + Click + `; + const link = domassist.findOne('a'); + + domassist.on(link, 'click.domassist', e => { + assert.pass('Event has been fired'); + }); + + link.click(); + assert.end(); +}); + +test('Events - Should be possible to unbind single event and namespace', assert => { + const el = domassist.findOne('#domassist'); + assert.plan(1); + el.innerHTML = ` + Click + `; + + const link = domassist.findOne('a'); + + domassist.on(link, 'click.domassist', () => { + assert.fail('I should not fire'); + }); + + domassist.on(link, 'mouseenter.domassist', () => { + assert.pass('Mouse enter fired normally'); + }); + + domassist.off(link, 'click.domassist'); + + link.click(); + TestUtils.fireEvent(link, 'mouseenter'); + assert.end(); +}); + +test('Events - Should be possible to unbind whole namespace', assert => { + const el = domassist.findOne('#domassist'); + el.innerHTML = ` + Click + `; + + const link = domassist.findOne('a'); + + domassist.on(link, 'click.domassist', () => { + assert.fail('I should not fire'); + }); + + domassist.on(link, 'mouseenter.domassist', () => { + assert.fail('I should not fire'); + }); + + domassist.on(link, 'custom.domassist', () => { + assert.fail('I should not fire'); + }); + + domassist.off(link, '.domassist'); + + link.click(); + TestUtils.fireEvent(link, 'mouseenter'); + TestUtils.fireEvent(link, 'custom'); + assert.pass('No event have fired'); + 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..ff4d21a 100644 --- a/test/off.test.js +++ b/test/off.test.js @@ -1,9 +1,7 @@ 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 several events', assert => { const el = domassist.findOne('#domassist'); assert.plan(1); el.innerHTML = ` @@ -11,7 +9,6 @@ test('Events - off single element', assert => { `; const link = domassist.findOne('a', el); - const pos = link.getBoundingClientRect(); let clicked = false; @@ -19,18 +16,47 @@ test('Events - off single element', assert => { clicked = true; }); + domassist.on(link, 'click', e => { + clicked = true; + }); + + domassist.on(link, 'click', e => { + clicked = true; + }); + domassist.off(link, 'click'); - page.sendEvent('click', pos.left + pos.width / 2, pos.top + pos.height / 2); + link.click(); setTimeout(() => { assert.ok(!clicked, 'Event not fired'); }, 500); }); +test('Events - Several handlers, one unbound', assert => { + const el = domassist.findOne('#domassist'); + assert.plan(1); + el.innerHTML = ` + Click + `; + + const link = domassist.findOne('a', el); + const myRemovableHandler = () => { + assert.fail('This should never fire'); + }; + + domassist.on(link, 'click', () => { + assert.pass('Should only fire this event'); + }); + + domassist.on(link, 'click', myRemovableHandler); + domassist.off(link, 'click', myRemovableHandler); + + link.click(); +}); + test('Events - off multiple elements', assert => { const el = domassist.findOne('#domassist'); - assert.plan(4); el.innerHTML = ` Click Click @@ -39,20 +65,16 @@ 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.on(links, 'click', () => { + assert.fail('I should never fire'); }); - // 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); + + domassist.off(links, 'click'); + + links.forEach(item => { + item.click(); }); + + 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); + } +};