Skip to content

Commit

Permalink
use EventListener mixin to ensure listeners are destroyed by views
Browse files Browse the repository at this point in the history
  • Loading branch information
bantic committed Jul 14, 2015
1 parent 2839ce5 commit 9ef8f59
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 90 deletions.
1 change: 1 addition & 0 deletions src/js/commands/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
16 changes: 4 additions & 12 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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...',
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 = [];
Expand All @@ -446,7 +437,8 @@ merge(Editor.prototype, {
this.removeAllEventListeners();
this.removeAllViews();
}

});

mixin(Editor, EventListenerMixin);

export default Editor;
14 changes: 14 additions & 0 deletions src/js/utils/event-listener.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
14 changes: 14 additions & 0 deletions src/js/utils/mixin.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
15 changes: 9 additions & 6 deletions src/js/views/embed-intent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
}
Expand Down
94 changes: 52 additions & 42 deletions src/js/views/prompt.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
10 changes: 6 additions & 4 deletions src/js/views/text-format-toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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));
Expand Down
6 changes: 5 additions & 1 deletion src/js/views/toolbar-button.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -42,4 +44,6 @@ ToolbarButton.prototype = {
}
};

mixin(ToolbarButton, EventListenerMixin);

export default ToolbarButton;
2 changes: 1 addition & 1 deletion src/js/views/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function Toolbar(options) {
}

// Closes prompt if displayed when changing selection
document.addEventListener('mouseup', function() {
this.addEventListener(document, 'mouseup', () => {
toolbar.dismissPrompt();
});
}
Expand Down
4 changes: 2 additions & 2 deletions src/js/views/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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) {
Expand Down
53 changes: 31 additions & 22 deletions src/js/views/view.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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;

0 comments on commit 9ef8f59

Please sign in to comment.