From 225f3137766daba7db1b89e8fc7de42d0fb65fcb Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Sat, 13 May 2017 22:52:46 +0200 Subject: [PATCH 1/8] Ignoring files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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 From 0119edc4dd15790bba87d6f3ad91b4faede606cb Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Sat, 13 May 2017 22:53:04 +0200 Subject: [PATCH 2/8] Adding failing tests --- test/off.test.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/off.test.js b/test/off.test.js index 87a8b9d..1e1a6b8 100644 --- a/test/off.test.js +++ b/test/off.test.js @@ -3,7 +3,7 @@ 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 = ` @@ -19,6 +19,14 @@ 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); @@ -28,6 +36,28 @@ test('Events - off single element', assert => { }, 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); From c01d59ec8481a35658dbc770da36634b8cac62aa Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Sat, 13 May 2017 22:54:22 +0200 Subject: [PATCH 3/8] Storing event information into dom nodes test pass --- lib/off.js | 39 +++++++++++++++++++++++++++------------ lib/on.js | 13 +++++++------ lib/privData.js | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 lib/privData.js diff --git a/lib/off.js b/lib/off.js index a859db8..f5022af 100644 --- a/lib/off.js +++ b/lib/off.js @@ -1,22 +1,37 @@ import find from './find'; +import privData from './privData'; -function off(selector, event) { +function off(selector, event, handler) { if (Array.isArray(selector)) { - selector.forEach((item) => off(item, event)); + selector.forEach((item) => off(item, event, handler)); } - if (!window._domassistevents) { - window._domassistevents = {}; - } - - const data = window._domassistevents[`_${event}`]; - if (!data) { - return; - } const el = find(selector); + if (el.length) { - el.forEach((item) => { - item.removeEventListener(event, data.cb, data.capture); + el.forEach(item => { + const eventDict = privData.get(item, 'events'); + + if (eventDict[event]) { + // Just want to unbind given handler + if (typeof handler !== 'undefined') { + const handlerData = eventDict[event].filter(data => data.cb === handler)[0]; + eventDict[event] = eventDict[event].filter(data => data.cb !== handler); + + item.removeEventListener(event, handlerData.cb, handlerData.capture); + } else { + // Remove all handlers for a given event + eventDict[event].forEach(eventData => { + item.removeEventListener(event, eventData.cb, eventData.capture); + }); + + eventDict[event].length = 0; + } + + if (eventDict[event].length === 0) { + eventDict[event] = undefined; + } + } }); } } diff --git a/lib/on.js b/lib/on.js index 713ade5..daaa980 100644 --- a/lib/on.js +++ b/lib/on.js @@ -1,4 +1,5 @@ import find from './find'; +import privData from './privData'; function on(selector, event, cb, capture = false) { if (Array.isArray(selector)) { @@ -11,14 +12,14 @@ function on(selector, event, cb, capture = false) { capture }; - if (!window._domassistevents) { - window._domassistevents = {}; - } - - window._domassistevents[`_${event}`] = data; const el = find(selector); if (el.length) { - el.forEach((item) => { + el.forEach(item => { + const eventDict = privData.get(item, 'events'); + if (!eventDict[event]) { + eventDict[event] = []; + } + eventDict[event].push(data); item.addEventListener(event, 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(); From ba1b5e514e7f895dd4ea297abd44ec1cacd156a7 Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Mon, 15 May 2017 09:11:47 +0200 Subject: [PATCH 4/8] Removing the need for phantom.page --- test/domassist.test.js | 28 ++++++++++++---------------- test/modify.test.js | 19 ++++++++----------- test/off.test.js | 30 +++++++++++------------------- test/on.test.js | 8 ++------ test/test-utils.js | 8 ++++++++ 5 files changed, 41 insertions(+), 52 deletions(-) create mode 100644 test/test-utils.js diff --git a/test/domassist.test.js b/test/domassist.test.js index 1c372dc..e6effa4 100644 --- a/test/domassist.test.js +++ b/test/domassist.test.js @@ -14,8 +14,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 +116,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 +127,6 @@ test('Events - once', assert => { `; const link = domassist.findOne('a', el); - const pos = link.getBoundingClientRect(); let clicks = 0; @@ -138,9 +134,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 +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/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 1e1a6b8..ff4d21a 100644 --- a/test/off.test.js +++ b/test/off.test.js @@ -1,8 +1,6 @@ import domassist from '../domassist'; import test from 'tape-rollup'; -const page = window.phantom.page; - test('Events - off single element several events', assert => { const el = domassist.findOne('#domassist'); assert.plan(1); @@ -11,7 +9,6 @@ test('Events - off single element several events', assert => { `; const link = domassist.findOne('a', el); - const pos = link.getBoundingClientRect(); let clicked = false; @@ -29,7 +26,7 @@ test('Events - off single element several events', assert => { 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'); @@ -60,7 +57,6 @@ test('Events - Several handlers, one unbound', assert => { test('Events - off multiple elements', assert => { const el = domassist.findOne('#domassist'); - assert.plan(4); el.innerHTML = ` Click Click @@ -69,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); + } +}; From 4d73c77bb63806656d46c24bff230a79e675dbab Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Mon, 15 May 2017 09:12:03 +0200 Subject: [PATCH 5/8] No binding twice on modify --- lib/modify.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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]); } }); From f7b7c36975a015cf27052c798e162f9e2c4ca729 Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Mon, 15 May 2017 11:31:24 +0200 Subject: [PATCH 6/8] Adding HTML to be able to run tests on the browser --- test/index.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 test/index.html 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 + + + + + From 2c4ba0f8c2dbd3be32858f5d9daa3c49c3ed2639 Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Mon, 15 May 2017 11:31:50 +0200 Subject: [PATCH 7/8] Adding event namespace tests --- test/domassist.test.js | 1 + test/event-namespacing.test.js | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 test/event-namespacing.test.js diff --git a/test/domassist.test.js b/test/domassist.test.js index e6effa4..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'; 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(); +}); From 3636ca872138b6cb7de7c3598c75ceb42be75c49 Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Mon, 15 May 2017 11:39:09 +0200 Subject: [PATCH 8/8] Adding namespace support to events --- lib/off.js | 77 ++++++++++++++++++++++++++++++++++++++---------------- lib/on.js | 36 ++++++++++++++++++++----- 2 files changed, 84 insertions(+), 29 deletions(-) diff --git a/lib/off.js b/lib/off.js index f5022af..8eca807 100644 --- a/lib/off.js +++ b/lib/off.js @@ -1,6 +1,58 @@ import find from './find'; import privData from './privData'; +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); + } + + 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))); + } + + return eventsToUnbind; +} + function off(selector, event, handler) { if (Array.isArray(selector)) { selector.forEach((item) => off(item, event, handler)); @@ -10,28 +62,9 @@ function off(selector, event, handler) { if (el.length) { el.forEach(item => { - const eventDict = privData.get(item, 'events'); - - if (eventDict[event]) { - // Just want to unbind given handler - if (typeof handler !== 'undefined') { - const handlerData = eventDict[event].filter(data => data.cb === handler)[0]; - eventDict[event] = eventDict[event].filter(data => data.cb !== handler); - - item.removeEventListener(event, handlerData.cb, handlerData.capture); - } else { - // Remove all handlers for a given event - eventDict[event].forEach(eventData => { - item.removeEventListener(event, eventData.cb, eventData.capture); - }); - - eventDict[event].length = 0; - } - - if (eventDict[event].length === 0) { - eventDict[event] = undefined; - } - } + 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 daaa980..b70aa1f 100644 --- a/lib/on.js +++ b/lib/on.js @@ -1,26 +1,48 @@ 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)) { selector.forEach((item) => on(item, event, cb, capture)); 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 }; - const el = find(selector); if (el.length) { el.forEach(item => { - const eventDict = privData.get(item, 'events'); - if (!eventDict[event]) { - eventDict[event] = []; - } - eventDict[event].push(data); - item.addEventListener(event, cb, capture); + storeKeys.forEach(key => storeIntoDict(item, key, data)); + item.addEventListener(type, cb, capture); }); } }