Skip to content

Commit 258d1df

Browse files
authored
Merge pull request yabwe#1124 from yabwe/paste-handler-fix
Fixes for paste and placeholder extensions + add/remove element events
2 parents d71cf26 + e37199d commit 258d1df

File tree

8 files changed

+127
-20
lines changed

8 files changed

+127
-20
lines changed

CUSTOM-EVENTS.md

+36-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# MediumEditor Custom Events (v5.0.0)
22

3-
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.
3+
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.
44

55
**NOTE:**
66

77
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.
88

9-
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).
9+
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).
1010

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

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

59-
2. _**listener(data, editable)** (`function`)_:
61+
2. _**listener(data, editable)** (`function`)_:
6062

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

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

8183
* Name of the event to detach the listener for.
8284

83-
2. _**listener** (`function`)_:
85+
2. _**listener** (`function`)_:
8486

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

@@ -109,6 +111,20 @@ Manually triggers a custom event.
109111

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

114+
### `addElement`
115+
116+
`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>`.
117+
118+
**Arguments to listener**
119+
120+
1. _**data** (`object`)_
121+
* Properties of data object
122+
* `target`: element which was added to the editor
123+
* `currentTarget`: element which was added to the editor
124+
2. _**editable** (`HTMLElement`)_
125+
* element which was added to the editor
126+
127+
***
112128
### `blur`
113129

114130
`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).
@@ -140,7 +156,21 @@ Example:
140156
***
141157
### `focus`
142158

143-
`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.
159+
`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.
160+
161+
***
162+
### `removeElement`
163+
164+
`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.
165+
166+
**Arguments to listener**
167+
168+
1. _**data** (`object`)_
169+
* Properties of data object
170+
* `target`: element which was removed from the editor
171+
* `currentTarget`: element which was removed from the editor
172+
2. _**editable** (`HTMLElement`)_
173+
* element which was removed from the editor
144174

145175
## Toolbar Custom Events
146176

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

162192
## Proxied Custom Events
163193

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

166196
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.
167197

spec/dyn-elements.spec.js

+24
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ describe('MediumEditor.DynamicElements TestCase', function () {
115115
expect(editor.events.customEvents['editableKeydownEnter'].length).toBe(2, 'editableKeydownEnter should be subscribed to when adding a data-disbale-return element');
116116
});
117117

118+
it('should trigger addElement custom event for each element', function () {
119+
var editor = this.newMediumEditor('.editor'),
120+
spy = jasmine.createSpy('handler');
121+
122+
editor.subscribe('addElement', spy);
123+
editor.addElements('.add-one');
124+
expect(spy).toHaveBeenCalledWith({ target: this.addOne, currentTarget: this.addOne }, this.addOne);
125+
126+
editor.addElements(document.getElementsByClassName('add-two'));
127+
expect(spy).toHaveBeenCalledWith({ target: this.addTwo, currentTarget: this.addTwo }, this.addTwo);
128+
});
129+
118130
function runAddTest(inputSupported) {
119131
it('should re-attach element properly when removed from dom, cleaned up and injected to dom again', function () {
120132
var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;
@@ -235,6 +247,18 @@ describe('MediumEditor.DynamicElements TestCase', function () {
235247
editor.removeElements(this.el);
236248
expect(attached.length).toBe(0);
237249
});
250+
251+
it('should trigger removeElement custom event for each element', function () {
252+
var editor = this.newMediumEditor('.editor, .add-one, .add-two'),
253+
spy = jasmine.createSpy('handler');
254+
255+
editor.subscribe('removeElement', spy);
256+
editor.removeElements('.add-one');
257+
expect(spy).toHaveBeenCalledWith({ target: this.addOne, currentTarget: this.addOne }, this.addOne);
258+
259+
editor.removeElements(document.getElementsByClassName('add-two'));
260+
expect(spy).toHaveBeenCalledWith({ target: this.addTwo, currentTarget: this.addTwo }, this.addTwo);
261+
});
238262
});
239263
});
240264

spec/paste.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ describe('Pasting content', function () {
205205
var evt = prepareEvent(editorEl, 'paste');
206206
firePreparedEvent(evt, editorEl, 'paste');
207207
jasmine.clock().tick(1);
208-
expect(spy).toHaveBeenCalledWith({ currentTarget: this.el, target: this.el }, this.el);
208+
expect(spy).toHaveBeenCalledWith(evt, this.el);
209209
});
210210

211211
it('should filter multi-line rich-text pastes when "insertHTML" command is not supported', function () {

spec/placeholder.spec.js

+21
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ describe('MediumEditor.extensions.placeholder TestCase', function () {
153153
validatePlaceholderContent(editor.elements[0], MediumEditor.extensions.placeholder.prototype.text);
154154
});
155155

156+
it('should add the default placeholder text when data-placeholder is not present on dynamically added elements', function () {
157+
var editor = this.newMediumEditor('.editor');
158+
expect(editor.elements.length).toBe(1);
159+
160+
var newEl = this.createElement('div', 'other-element');
161+
editor.addElements(newEl);
162+
validatePlaceholderContent(newEl, MediumEditor.extensions.placeholder.prototype.text);
163+
});
164+
156165
it('should remove the added data-placeholder attribute when destroyed', function () {
157166
expect(this.el.hasAttribute('data-placeholder')).toBe(false);
158167

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

175+
it('should remove the added data-placeholder attribute when elements are removed dynamically from the editor', function () {
176+
var editor = this.newMediumEditor('.editor'),
177+
newEl = this.createElement('div', 'other-element');
178+
179+
expect(newEl.hasAttribute('other-element')).toBe(false);
180+
editor.addElements(newEl);
181+
expect(newEl.getAttribute('data-placeholder')).toBe(MediumEditor.extensions.placeholder.prototype.text);
182+
183+
editor.removeElements('.other-element');
184+
expect(newEl.hasAttribute('data-placeholder')).toBe(false);
185+
});
186+
166187
it('should not remove custom data-placeholder attribute when destroyed', function () {
167188
var placeholderText = 'Custom placeholder';
168189
this.el.setAttribute('data-placeholder', placeholderText);

src/js/core.js

+5
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,9 @@
12121212

12131213
// Add new elements to our internal elements array
12141214
this.elements.push(element);
1215+
1216+
// Trigger event so extensions can know when an element has been added
1217+
this.trigger('addElement', { target: element, currentTarget: element }, element);
12151218
}, this);
12161219
},
12171220

@@ -1234,6 +1237,8 @@
12341237
if (element.getAttribute('medium-editor-textarea-id')) {
12351238
cleanupTextareaElement(element);
12361239
}
1240+
// Trigger event so extensions can clean-up elements that are being removed
1241+
this.trigger('removeElement', { target: element, currentTarget: element }, element);
12371242
return false;
12381243
}
12391244
return true;

src/js/events.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@
360360
// Detecting drop on the contenteditables
361361
this.attachToEachElement('drop', this.handleDrop);
362362
break;
363+
// TODO: We need to have a custom 'paste' event separate from 'editablePaste'
364+
// Need to think about the way to introduce this without breaking folks
363365
case 'editablePaste':
364366
// Detecting paste on the contenteditables
365367
this.attachToEachElement('paste', this.handlePaste);
@@ -556,7 +558,7 @@
556558
},
557559

558560
handlePaste: function (event) {
559-
this.triggerCustomEvent('editablePaste', { currentTarget: event.currentTarget, target: event.target }, event.currentTarget);
561+
this.triggerCustomEvent('editablePaste', event, event.currentTarget);
560562
},
561563

562564
handleKeydown: function (event) {

src/js/extensions/paste.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,20 @@
143143
MediumEditor.Extension.prototype.init.apply(this, arguments);
144144

145145
if (this.forcePlainText || this.cleanPastedHTML) {
146-
this.subscribe('editablePaste', this.handlePaste.bind(this));
147146
this.subscribe('editableKeydown', this.handleKeydown.bind(this));
147+
// We need access to the full event data in paste
148+
// so we can't use the editablePaste event here
149+
this.getEditorElements().forEach(function (element) {
150+
this.on(element, 'paste', this.handlePaste.bind(this));
151+
}, this);
152+
this.subscribe('addElement', this.handleAddElement.bind(this));
148153
}
149154
},
150155

156+
handleAddElement: function (event, editable) {
157+
this.on(editable, 'paste', this.handlePaste.bind(this));
158+
},
159+
151160
destroy: function () {
152161
// Make sure pastebin is destroyed in case it's still around for some reason
153162
if (this.forcePlainText || this.cleanPastedHTML) {

src/js/extensions/placeholder.js

+27-11
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,32 @@
2424
},
2525

2626
initPlaceholders: function () {
27-
this.getEditorElements().forEach(function (el) {
28-
if (!el.getAttribute('data-placeholder')) {
29-
el.setAttribute('data-placeholder', this.text);
30-
}
31-
this.updatePlaceholder(el);
32-
}, this);
27+
this.getEditorElements().forEach(this.initElement, this);
28+
},
29+
30+
handleAddElement: function (event, editable) {
31+
this.initElement(editable);
32+
},
33+
34+
initElement: function (el) {
35+
if (!el.getAttribute('data-placeholder')) {
36+
el.setAttribute('data-placeholder', this.text);
37+
}
38+
this.updatePlaceholder(el);
3339
},
3440

3541
destroy: function () {
36-
this.getEditorElements().forEach(function (el) {
37-
if (el.getAttribute('data-placeholder') === this.text) {
38-
el.removeAttribute('data-placeholder');
39-
}
40-
}, this);
42+
this.getEditorElements().forEach(this.cleanupElement, this);
43+
},
44+
45+
handleRemoveElement: function (event, editable) {
46+
this.cleanupElement(editable);
47+
},
48+
49+
cleanupElement: function (el) {
50+
if (el.getAttribute('data-placeholder') === this.text) {
51+
el.removeAttribute('data-placeholder');
52+
}
4153
},
4254

4355
showPlaceholder: function (el) {
@@ -86,6 +98,10 @@
8698

8799
// When the editor loses focus, check if the placeholder should be visible
88100
this.subscribe('blur', this.handleBlur.bind(this));
101+
102+
// Need to know when elements are added/removed from the editor
103+
this.subscribe('addElement', this.handleAddElement.bind(this));
104+
this.subscribe('removeElement', this.handleRemoveElement.bind(this));
89105
},
90106

91107
handleInput: function (event, element) {

0 commit comments

Comments
 (0)