Skip to content

Commit

Permalink
Merge pull request #1124 from yabwe/paste-handler-fix
Browse files Browse the repository at this point in the history
Fixes for paste and placeholder extensions + add/remove element events
  • Loading branch information
nmielnik authored Jun 20, 2016
2 parents d71cf26 + e37199d commit 258d1df
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 20 deletions.
42 changes: 36 additions & 6 deletions CUSTOM-EVENTS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# MediumEditor Custom Events (v5.0.0)

MediumEditor exposes a variety of custom events for convienience when using the editor with your web application. You can attach and detach listeners to these custom events, as well as manually trigger any custom events including your own custom events.
MediumEditor exposes a variety of custom events for convenience when using the editor with your web application. You can attach and detach listeners to these custom events, as well as manually trigger any custom events including your own custom events.

**NOTE:**

Custom event listeners are triggered in the order that they were 'subscribed' to. Most functionality within medium-editor uses these custom events to trigger updates, so in general, it can be assumed that most of the built-in functionality has already been completed before any of your custom event listeners will be called.

If you need to override the editor's bult-in behavior, try overriding the built-in extensions with your own [custom extension](src/js/extensions).
If you need to override the editor's built-in behavior, try overriding the built-in extensions with your own [custom extension](src/js/extensions).

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
Expand All @@ -16,10 +16,12 @@ If you need to override the editor's bult-in behavior, try overriding the built-
- [`MediumEditor.unsubscribe(name, listener)`](#mediumeditorunsubscribename-listener)
- [`MediumEditor.trigger(name, data, editable)`](#mediumeditortriggername-data-editable)
- [Custom Events](#custom-events)
- [`addElement`](#addelement)
- [`blur`](#blur)
- [`editableInput`](#editableinput)
- [`externalInteraction`](#externalinteraction)
- [`focus`](#focus)
- [`removeElement`](#removeelement)
- [Toolbar Custom Events](#toolbar-custom-events)
- [`hideToolbar`](#hidetoolbar)
- [`positionToolbar`](#positiontoolbar)
Expand Down Expand Up @@ -56,7 +58,7 @@ Attaches a listener for the specified custom event name.

* Name of the event to listen to. See the list of built-in [Custom Events](#custom-events) below.

2. _**listener(data, editable)** (`function`)_:
2. _**listener(data, editable)** (`function`)_:

* Listener method that will be called whenever the custom event is triggered.

Expand All @@ -80,7 +82,7 @@ Detaches a custom event listener for the specified custom event name.

* Name of the event to detach the listener for.

2. _**listener** (`function`)_:
2. _**listener** (`function`)_:

* A reference to the listener to detach. This must be a match by-reference and not a copy.

Expand Down Expand Up @@ -109,6 +111,20 @@ Manually triggers a custom event.

These events are custom to MediumEditor so there may be one or more native events that can trigger them.

### `addElement`

`addElement` is triggered whenever an element is added to the editor after the editor has been instantiated. This custom event will be triggered **after** the element has already been initialized by the editor and added to the internal array of **elements**. If the element being added was a `<textarea>`, the element passed to the listener will be the created `<div contenteditable=true>` element and not the root `<textarea>`.

**Arguments to listener**

1. _**data** (`object`)_
* Properties of data object
* `target`: element which was added to the editor
* `currentTarget`: element which was added to the editor
2. _**editable** (`HTMLElement`)_
* element which was added to the editor

***
### `blur`

`blur` is triggered whenever a `contenteditable` element within an editor has lost focus to an element other than an editor maintained element (ie Toolbar, Anchor Preview, etc).
Expand Down Expand Up @@ -140,7 +156,21 @@ Example:
***
### `focus`

`focus` is triggered whenver a `contenteditable` element within an editor receives focus. If the user interacts with any editor maintained elements (ie toolbar), `blur` is NOT triggered because focus has not been lost. Thus, `focus` will only be triggered when an `contenteditable` element (or the editor that contains it) is first interacted with.
`focus` is triggered whenever a `contenteditable` element within an editor receives focus. If the user interacts with any editor maintained elements (ie toolbar), `blur` is NOT triggered because focus has not been lost. Thus, `focus` will only be triggered when an `contenteditable` element (or the editor that contains it) is first interacted with.

***
### `removeElement`

`removeElement` is triggered whenever an element is removed from the editor after the editor has been instantiated. This custom event will be triggered **after** the element has already been removed from the editor and any events attached to it have already been removed. If the element being removed was a `<div>` created to correspond to a `<textarea>`, the element will already have been removed from the DOM.

**Arguments to listener**

1. _**data** (`object`)_
* Properties of data object
* `target`: element which was removed from the editor
* `currentTarget`: element which was removed from the editor
2. _**editable** (`HTMLElement`)_
* element which was removed from the editor

## Toolbar Custom Events

Expand All @@ -161,7 +191,7 @@ These events are triggered by the toolbar when the toolbar extension has not bee

## Proxied Custom Events

These events are triggered whenever a native browser event is triggered for any of the `contenteditable` elements monitored by this instnace of MediumEditor.
These events are triggered whenever a native browser event is triggered for any of the `contenteditable` elements monitored by this instance of MediumEditor.

For example, the `editableClick` custom event will be triggered when a native `click` event is fired on any of the `contenteditable` elements. This provides a single event listener that can get fired for all elements, and also allows for the `contenteditable` element that triggered the event to be passed to the listener.

Expand Down
24 changes: 24 additions & 0 deletions spec/dyn-elements.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ describe('MediumEditor.DynamicElements TestCase', function () {
expect(editor.events.customEvents['editableKeydownEnter'].length).toBe(2, 'editableKeydownEnter should be subscribed to when adding a data-disbale-return element');
});

it('should trigger addElement custom event for each element', function () {
var editor = this.newMediumEditor('.editor'),
spy = jasmine.createSpy('handler');

editor.subscribe('addElement', spy);
editor.addElements('.add-one');
expect(spy).toHaveBeenCalledWith({ target: this.addOne, currentTarget: this.addOne }, this.addOne);

editor.addElements(document.getElementsByClassName('add-two'));
expect(spy).toHaveBeenCalledWith({ target: this.addTwo, currentTarget: this.addTwo }, this.addTwo);
});

function runAddTest(inputSupported) {
it('should re-attach element properly when removed from dom, cleaned up and injected to dom again', function () {
var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;
Expand Down Expand Up @@ -235,6 +247,18 @@ describe('MediumEditor.DynamicElements TestCase', function () {
editor.removeElements(this.el);
expect(attached.length).toBe(0);
});

it('should trigger removeElement custom event for each element', function () {
var editor = this.newMediumEditor('.editor, .add-one, .add-two'),
spy = jasmine.createSpy('handler');

editor.subscribe('removeElement', spy);
editor.removeElements('.add-one');
expect(spy).toHaveBeenCalledWith({ target: this.addOne, currentTarget: this.addOne }, this.addOne);

editor.removeElements(document.getElementsByClassName('add-two'));
expect(spy).toHaveBeenCalledWith({ target: this.addTwo, currentTarget: this.addTwo }, this.addTwo);
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion spec/paste.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ describe('Pasting content', function () {
var evt = prepareEvent(editorEl, 'paste');
firePreparedEvent(evt, editorEl, 'paste');
jasmine.clock().tick(1);
expect(spy).toHaveBeenCalledWith({ currentTarget: this.el, target: this.el }, this.el);
expect(spy).toHaveBeenCalledWith(evt, this.el);
});

it('should filter multi-line rich-text pastes when "insertHTML" command is not supported', function () {
Expand Down
21 changes: 21 additions & 0 deletions spec/placeholder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ describe('MediumEditor.extensions.placeholder TestCase', function () {
validatePlaceholderContent(editor.elements[0], MediumEditor.extensions.placeholder.prototype.text);
});

it('should add the default placeholder text when data-placeholder is not present on dynamically added elements', function () {
var editor = this.newMediumEditor('.editor');
expect(editor.elements.length).toBe(1);

var newEl = this.createElement('div', 'other-element');
editor.addElements(newEl);
validatePlaceholderContent(newEl, MediumEditor.extensions.placeholder.prototype.text);
});

it('should remove the added data-placeholder attribute when destroyed', function () {
expect(this.el.hasAttribute('data-placeholder')).toBe(false);

Expand All @@ -163,6 +172,18 @@ describe('MediumEditor.extensions.placeholder TestCase', function () {
expect(this.el.hasAttribute('data-placeholder')).toBe(false);
});

it('should remove the added data-placeholder attribute when elements are removed dynamically from the editor', function () {
var editor = this.newMediumEditor('.editor'),
newEl = this.createElement('div', 'other-element');

expect(newEl.hasAttribute('other-element')).toBe(false);
editor.addElements(newEl);
expect(newEl.getAttribute('data-placeholder')).toBe(MediumEditor.extensions.placeholder.prototype.text);

editor.removeElements('.other-element');
expect(newEl.hasAttribute('data-placeholder')).toBe(false);
});

it('should not remove custom data-placeholder attribute when destroyed', function () {
var placeholderText = 'Custom placeholder';
this.el.setAttribute('data-placeholder', placeholderText);
Expand Down
5 changes: 5 additions & 0 deletions src/js/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,9 @@

// Add new elements to our internal elements array
this.elements.push(element);

// Trigger event so extensions can know when an element has been added
this.trigger('addElement', { target: element, currentTarget: element }, element);
}, this);
},

Expand All @@ -1234,6 +1237,8 @@
if (element.getAttribute('medium-editor-textarea-id')) {
cleanupTextareaElement(element);
}
// Trigger event so extensions can clean-up elements that are being removed
this.trigger('removeElement', { target: element, currentTarget: element }, element);
return false;
}
return true;
Expand Down
4 changes: 3 additions & 1 deletion src/js/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@
// Detecting drop on the contenteditables
this.attachToEachElement('drop', this.handleDrop);
break;
// TODO: We need to have a custom 'paste' event separate from 'editablePaste'
// Need to think about the way to introduce this without breaking folks
case 'editablePaste':
// Detecting paste on the contenteditables
this.attachToEachElement('paste', this.handlePaste);
Expand Down Expand Up @@ -556,7 +558,7 @@
},

handlePaste: function (event) {
this.triggerCustomEvent('editablePaste', { currentTarget: event.currentTarget, target: event.target }, event.currentTarget);
this.triggerCustomEvent('editablePaste', event, event.currentTarget);
},

handleKeydown: function (event) {
Expand Down
11 changes: 10 additions & 1 deletion src/js/extensions/paste.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,20 @@
MediumEditor.Extension.prototype.init.apply(this, arguments);

if (this.forcePlainText || this.cleanPastedHTML) {
this.subscribe('editablePaste', this.handlePaste.bind(this));
this.subscribe('editableKeydown', this.handleKeydown.bind(this));
// We need access to the full event data in paste
// so we can't use the editablePaste event here
this.getEditorElements().forEach(function (element) {
this.on(element, 'paste', this.handlePaste.bind(this));
}, this);
this.subscribe('addElement', this.handleAddElement.bind(this));
}
},

handleAddElement: function (event, editable) {
this.on(editable, 'paste', this.handlePaste.bind(this));
},

destroy: function () {
// Make sure pastebin is destroyed in case it's still around for some reason
if (this.forcePlainText || this.cleanPastedHTML) {
Expand Down
38 changes: 27 additions & 11 deletions src/js/extensions/placeholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,32 @@
},

initPlaceholders: function () {
this.getEditorElements().forEach(function (el) {
if (!el.getAttribute('data-placeholder')) {
el.setAttribute('data-placeholder', this.text);
}
this.updatePlaceholder(el);
}, this);
this.getEditorElements().forEach(this.initElement, this);
},

handleAddElement: function (event, editable) {
this.initElement(editable);
},

initElement: function (el) {
if (!el.getAttribute('data-placeholder')) {
el.setAttribute('data-placeholder', this.text);
}
this.updatePlaceholder(el);
},

destroy: function () {
this.getEditorElements().forEach(function (el) {
if (el.getAttribute('data-placeholder') === this.text) {
el.removeAttribute('data-placeholder');
}
}, this);
this.getEditorElements().forEach(this.cleanupElement, this);
},

handleRemoveElement: function (event, editable) {
this.cleanupElement(editable);
},

cleanupElement: function (el) {
if (el.getAttribute('data-placeholder') === this.text) {
el.removeAttribute('data-placeholder');
}
},

showPlaceholder: function (el) {
Expand Down Expand Up @@ -86,6 +98,10 @@

// When the editor loses focus, check if the placeholder should be visible
this.subscribe('blur', this.handleBlur.bind(this));

// Need to know when elements are added/removed from the editor
this.subscribe('addElement', this.handleAddElement.bind(this));
this.subscribe('removeElement', this.handleRemoveElement.bind(this));
},

handleInput: function (event, element) {
Expand Down

0 comments on commit 258d1df

Please sign in to comment.