From 9ef8f5957e7cfb3194fa833f5645bd248e1eebb9 Mon Sep 17 00:00:00 2001 From: Cory Forsyth Date: Mon, 13 Jul 2015 18:17:26 -0400 Subject: [PATCH] use EventListener mixin to ensure listeners are destroyed by views --- src/js/commands/image.js | 1 + src/js/editor/editor.js | 16 ++--- src/js/utils/event-listener.js | 14 +++++ src/js/utils/mixin.js | 14 +++++ src/js/views/embed-intent.js | 15 +++-- src/js/views/prompt.js | 94 ++++++++++++++++------------- src/js/views/text-format-toolbar.js | 10 +-- src/js/views/toolbar-button.js | 6 +- src/js/views/toolbar.js | 2 +- src/js/views/tooltip.js | 4 +- src/js/views/view.js | 53 +++++++++------- 11 files changed, 139 insertions(+), 90 deletions(-) create mode 100644 src/js/utils/event-listener.js create mode 100644 src/js/utils/mixin.js diff --git a/src/js/commands/image.js b/src/js/commands/image.js index 846866be1..1f0a5e553 100644 --- a/src/js/commands/image.js +++ b/src/js/commands/image.js @@ -8,6 +8,7 @@ function createFileInput(command) { fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.className = 'ck-file-input'; + // FIXME should this listener be torn down when the ImageCommand is not active? fileInput.addEventListener('change', function(e) { command.handleFile(e); }); diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 3ea0e029e..1d175e365 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -26,6 +26,8 @@ import MobiledocRenderer from '../renderers/mobiledoc'; import { toArray, merge, mergeWithOptions } from 'content-kit-utils'; import { detectParentNode } from '../utils/dom-utils'; import { getData, setData } from '../utils/element-utils'; +import mixin from '../utils/mixin'; +import EventListenerMixin from '../utils/event-listener'; var defaults = { placeholder: 'Write here...', @@ -223,11 +225,6 @@ merge(Editor.prototype, { this._views.push(view); }, - addEventListener(context, eventName, callback) { - context.addEventListener(eventName, callback); - this._elementListeners.push([context, eventName, callback]); - }, - loadModel(post) { this.post = post; this.syncVisual(); @@ -431,12 +428,6 @@ merge(Editor.prototype, { return MobiledocRenderer.render(this.post); }, - removeAllEventListeners() { - this._elementListeners.forEach(([context, ...args]) => { - context.removeEventListener(...args); - }); - }, - removeAllViews() { this._views.forEach((v) => v.destroy()); this._views = []; @@ -446,7 +437,8 @@ merge(Editor.prototype, { this.removeAllEventListeners(); this.removeAllViews(); } - }); +mixin(Editor, EventListenerMixin); + export default Editor; diff --git a/src/js/utils/event-listener.js b/src/js/utils/event-listener.js new file mode 100644 index 000000000..aac0b8151 --- /dev/null +++ b/src/js/utils/event-listener.js @@ -0,0 +1,14 @@ +export default class EventListenerMixin { + addEventListener(context, eventName, listener) { + if (!this._eventListeners) { this._eventListeners = []; } + context.addEventListener(eventName, listener); + this._eventListeners.push([context, eventName, listener]); + } + + removeAllEventListeners() { + const listeners = this._eventListeners || []; + listeners.forEach(([context, ...args]) => { + context.removeEventListener(...args); + }); + } +} diff --git a/src/js/utils/mixin.js b/src/js/utils/mixin.js new file mode 100644 index 000000000..3bce8b88e --- /dev/null +++ b/src/js/utils/mixin.js @@ -0,0 +1,14 @@ +const CONSTRUCTOR_FN_NAME = 'constructor'; + +export default function mixin(target, source) { + target = target.prototype; + source = source.prototype; + + Object.getOwnPropertyNames(source).forEach((name) => { + if (name !== CONSTRUCTOR_FN_NAME) { + const descriptor = Object.getOwnPropertyDescriptor(source, name); + + Object.defineProperty(target, name, descriptor); + } + }); +} diff --git a/src/js/views/embed-intent.js b/src/js/views/embed-intent.js index 06754d67e..2344b0576 100644 --- a/src/js/views/embed-intent.js +++ b/src/js/views/embed-intent.js @@ -31,7 +31,8 @@ function EmbedIntent(options) { embedIntent.button.className = 'ck-embed-intent-btn'; embedIntent.button.title = 'Insert image or embed...'; embedIntent.element.appendChild(embedIntent.button); - embedIntent.button.addEventListener('mouseup', function(e) { + + this.addEventListener(embedIntent.button, 'mouseup', (e) => { if (embedIntent.isActive) { embedIntent.deactivate(); } else { @@ -57,18 +58,20 @@ function EmbedIntent(options) { } } - rootElement.addEventListener('keyup', embedIntentHandler); - document.addEventListener('mouseup', function() { - setTimeout(function() { embedIntentHandler(); }); + this.addEventListener(rootElement, 'keyup', embedIntentHandler); + this.addEventListener(document, 'mouseup', () => { + setTimeout(() => { + embedIntentHandler(); + }); }); - document.addEventListener('keyup', function(e) { + this.addEventListener(document, 'keyup', (e) => { if (e.keyCode === Keycodes.ESC) { embedIntent.hide(); } }); - window.addEventListener('resize', function() { + this.addEventListener(window, 'resize', () => { if(embedIntent.isShowing) { embedIntent.reposition(); } diff --git a/src/js/views/prompt.js b/src/js/views/prompt.js index bb0eecda4..eb97a935e 100644 --- a/src/js/views/prompt.js +++ b/src/js/views/prompt.js @@ -1,5 +1,4 @@ import View from './view'; -import { inherit } from 'content-kit-utils'; import { restoreRange } from '../utils/selection-utils'; import { createDiv, positionElementToRect } from '../utils/element-utils'; import Keycodes from '../utils/keycodes'; @@ -15,52 +14,63 @@ function positionHiliteRange(range) { positionElementToRect(hiliter, rect); } -function Prompt(options) { - var prompt = this; - options.tagName = 'input'; - View.call(prompt, options); +class Prompt extends View { + constructor(options) { + options.tagName = 'input'; + super(options); - prompt.command = options.command; - prompt.element.placeholder = options.placeholder || ''; - prompt.element.addEventListener('mouseup', function(e) { e.stopPropagation(); }); // prevents closing prompt when clicking input - prompt.element.addEventListener('keyup', function(e) { - var entry = this.value; - if(entry && prompt.range && !e.shiftKey && e.which === Keycodes.ENTER) { - restoreRange(prompt.range); - prompt.command.exec(entry); - if (prompt.onComplete) { prompt.onComplete(); } - } - }); + var prompt = this; - window.addEventListener('resize', function() { - var activeHilite = hiliter.parentNode; - var range = prompt.range; - if(activeHilite && range) { - positionHiliteRange(range); - } - }); -} -inherit(Prompt, View); + prompt.command = options.command; + prompt.element.placeholder = options.placeholder || ''; + this.addEventListener(prompt.element, 'mouseup', (e) => { + // prevents closing prompt when clicking input + e.stopPropagation(); + }); + this.addEventListener(prompt.element, 'keyup', (e) => { + const entry = prompt.element.value; + + if (entry && prompt.range && !e.shiftKey && e.which === Keycodes.ENTER) { + restoreRange(prompt.range); + this.command.exec(entry); + if (this.onComplete) { this.onComplete(); } + } + }); + + this.addEventListener(window, 'resize', () => { + var activeHilite = hiliter.parentNode; + var range = prompt.range; + if(activeHilite && range) { + positionHiliteRange(range); + } + }); + } + + show(callback) { + var element = this.element; + var selection = window.getSelection(); + var range = selection && selection.rangeCount && selection.getRangeAt(0); + element.value = null; + this.range = range || null; -Prompt.prototype.show = function(callback) { - var prompt = this; - var element = prompt.element; - var selection = window.getSelection(); - var range = selection && selection.rangeCount && selection.getRangeAt(0); - element.value = null; - prompt.range = range || null; - if (range) { - container.appendChild(hiliter); - positionHiliteRange(prompt.range); - setTimeout(function(){ element.focus(); }); // defer focus (disrupts mouseup events) - if (callback) { prompt.onComplete = callback; } + if (range) { + container.appendChild(hiliter); + positionHiliteRange(this.range); + setTimeout(() => { + // defer focus (disrupts mouseup events) + element.focus(); + }); + if (callback) { + this.onComplete = callback; + } + } } -}; -Prompt.prototype.hide = function() { - if (hiliter.parentNode) { - container.removeChild(hiliter); + hide() { + if (hiliter.parentNode) { + container.removeChild(hiliter); + } } -}; +} export default Prompt; diff --git a/src/js/views/text-format-toolbar.js b/src/js/views/text-format-toolbar.js index 381419ecf..10600d171 100644 --- a/src/js/views/text-format-toolbar.js +++ b/src/js/views/text-format-toolbar.js @@ -25,15 +25,17 @@ function TextFormatToolbar(options) { var toolbar = this; Toolbar.call(this, options); toolbar.rootElement = options.rootElement; - toolbar.rootElement.addEventListener('keyup', function() { handleTextSelection(toolbar); }); + this.addEventListener(toolbar.rootElement, 'keyup', () => { + handleTextSelection(toolbar); + }); - document.addEventListener('mouseup', function() { + this.addEventListener(document, 'mouseup', () => { setTimeout(function() { handleTextSelection(toolbar); }); }); - document.addEventListener('keyup', function(e) { + this.addEventListener(document, 'keyup', (e) => { var key = e.keyCode; if (key === 116) { //F5 toolbar.toggleSticky(); @@ -43,7 +45,7 @@ function TextFormatToolbar(options) { } }); - window.addEventListener('resize', function() { + this.addEventListener(window, 'resize', () => { if(!toolbar.sticky && toolbar.isShowing) { var activePromptRange = toolbar.activePrompt && toolbar.activePrompt.range; toolbar.positionToContent(activePromptRange ? activePromptRange : window.getSelection().getRangeAt(0)); diff --git a/src/js/views/toolbar-button.js b/src/js/views/toolbar-button.js index a3ecaf3e2..19e7010d5 100644 --- a/src/js/views/toolbar-button.js +++ b/src/js/views/toolbar-button.js @@ -1,4 +1,6 @@ var buttonClassName = 'ck-toolbar-btn'; +import mixin from '../utils/mixin'; +import EventListenerMixin from '../utils/event-listener'; function ToolbarButton(options) { var button = this; @@ -14,7 +16,7 @@ function ToolbarButton(options) { element.title = command.name; element.className = buttonClassName; element.innerHTML = command.button; - element.addEventListener('mouseup', function(e) { + this.addEventListener(element, 'mouseup', (e) => { if (!button.isActive && prompt) { toolbar.displayPrompt(prompt); } else { @@ -42,4 +44,6 @@ ToolbarButton.prototype = { } }; +mixin(ToolbarButton, EventListenerMixin); + export default ToolbarButton; diff --git a/src/js/views/toolbar.js b/src/js/views/toolbar.js index 7bfd198af..853e986b9 100644 --- a/src/js/views/toolbar.js +++ b/src/js/views/toolbar.js @@ -57,7 +57,7 @@ function Toolbar(options) { } // Closes prompt if displayed when changing selection - document.addEventListener('mouseup', function() { + this.addEventListener(document, 'mouseup', () => { toolbar.dismissPrompt(); }); } diff --git a/src/js/views/tooltip.js b/src/js/views/tooltip.js index 0d5d05e09..9ffc3cb15 100644 --- a/src/js/views/tooltip.js +++ b/src/js/views/tooltip.js @@ -10,7 +10,7 @@ function Tooltip(options) { options.classNames = ['ck-tooltip']; View.call(tooltip, options); - rootElement.addEventListener('mouseover', function(e) { + this.addEventListener(rootElement, 'mouseover', (e) => { var target = getEventTargetMatchingTag(options.showForTag, e.target, rootElement); if (target && target.isContentEditable) { timeout = setTimeout(function() { @@ -19,7 +19,7 @@ function Tooltip(options) { } }); - rootElement.addEventListener('mouseout', function(e) { + this.addEventListener(rootElement, 'mouseout', (e) => { clearTimeout(timeout); var toElement = e.toElement || e.relatedTarget; if (toElement && toElement.className !== tooltip.element.className) { diff --git a/src/js/views/view.js b/src/js/views/view.js index d11537218..8fcf2baf5 100644 --- a/src/js/views/view.js +++ b/src/js/views/view.js @@ -1,3 +1,6 @@ +import mixin from '../utils/mixin'; +import EventListenerMixin from '../utils/event-listener'; + function renderClasses(view) { var classNames = view.classNames; if (classNames && classNames.length) { @@ -7,55 +10,61 @@ function renderClasses(view) { } } -function View(options) { - options = options || {}; - this.tagName = options.tagName || 'div'; - this.classNames = options.classNames || []; - this.element = document.createElement(this.tagName); - this.container = options.container || document.body; - this.isShowing = false; - renderClasses(this); -} +class View { + constructor(options={}) { + this.tagName = options.tagName || 'div'; + this.classNames = options.classNames || []; + this.element = document.createElement(this.tagName); + this.container = options.container || document.body; + this.isShowing = false; + renderClasses(this); + } -View.prototype = { - show: function() { + show() { var view = this; if(!view.isShowing) { view.container.appendChild(view.element); view.isShowing = true; return true; } - }, - hide: function() { + } + + hide() { var view = this; if(view.isShowing) { view.container.removeChild(view.element); view.isShowing = false; return true; } - }, - addClass: function(className) { + } + + addClass(className) { var index = this.classNames && this.classNames.indexOf(className); if (index === -1) { this.classNames.push(className); renderClasses(this); } - }, - removeClass: function(className) { + } + + removeClass(className) { var index = this.classNames && this.classNames.indexOf(className); if (index > -1) { this.classNames.splice(index, 1); renderClasses(this); } - }, - setClasses: function(classNameArr) { + } + + setClasses(classNameArr) { this.classNames = classNameArr; renderClasses(this); - }, + } + destroy() { - // FIXME should also clean up event listeners + this.removeAllEventListeners(); this.hide(); } -}; +} + +mixin(View, EventListenerMixin); export default View;