medium-editor-rails 1.3.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 91d565261c4f78127bd277b71c67000ef8385ac8
4
- data.tar.gz: 1780af7bf1a9bda664d0093fb4715dd706dbb4c2
3
+ metadata.gz: 4845247670516b2ca15fb5f3723f7f1aa360ab22
4
+ data.tar.gz: 0b0d17dadcd0c6dafab41c723311841fabe3927e
5
5
  SHA512:
6
- metadata.gz: 956fc017044e1056bebe177ca2c6006e1d164f93d28b7daa57211cbe7de76f0695a99757794dc8cb32f36ba3fe9f21d06bd3fad4a6b52b8a4deea00ad7a459da
7
- data.tar.gz: 5a1c9016bee4ade393655346e42d34335366ddca0fa7fe0817f36ba60f4ebac8791c174145f1c882edd3a117160b6887f56d4a5289f2444bad9fedc46f6f2ae1
6
+ metadata.gz: cf8dd7040eaa863a61b86c2a136622f8770950b43c9149438639e50487a7263e7b24b983fd3d7d7bf524bb4a224a08eee7eb9efedf19a453d4dab7ff9c25a281
7
+ data.tar.gz: 4623476567bee1b1812d5e6dcbc1db59deb435548b6b30a2bc191e1686037cec318d86bd4e4f56ad4ecd88e0ea16aba5e9985064b143c3127d7c930fe09e2ba1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
 
2
2
  #### [Current]
3
+ * [cad5c5c](../../commit/cad5c5c) - __(Ahmet Sezgin Duran)__ Update Medium Editor files
4
+ * [2666922](../../commit/2666922) - __(Ahmet Sezgin Duran)__ Merge tag '1.3.0' into develop
5
+
6
+ 1.3.0
7
+
8
+ #### 1.3.0
9
+ * [88e8565](../../commit/88e8565) - __(Ahmet Sezgin Duran)__ Bump versions 1.3.0 and 2.3.0
3
10
  * [0ce611b](../../commit/0ce611b) - __(Ahmet Sezgin Duran)__ Update Medium Editor files
4
11
  * [423eaf7](../../commit/423eaf7) - __(Ahmet Sezgin Duran)__ Merge tag '1.2.0' into develop
5
12
 
data/README.md CHANGED
@@ -8,7 +8,7 @@ This gem integrates [Medium Editor](https://github.com/daviferreira/medium-edito
8
8
 
9
9
  ## Version
10
10
 
11
- The latest version of Medium Editor bundled by this gem is [2.3.0](https://github.com/daviferreira/medium-editor/releases)
11
+ The latest version of Medium Editor bundled by this gem is [2.4.2](https://github.com/daviferreira/medium-editor/releases)
12
12
 
13
13
  ## Installation
14
14
 
@@ -1,6 +1,6 @@
1
1
  module MediumEditorRails
2
2
  module Rails
3
- VERSION = '1.3.0'
4
- MEDIUM_EDITOR_VERSION = '2.3.0'
3
+ VERSION = '1.4.2'
4
+ MEDIUM_EDITOR_VERSION = '2.4.2'
5
5
  end
6
6
  end
@@ -1,216 +1,599 @@
1
- function MediumEditor(elements, options) {
1
+ (function (root, factory) {
2
2
  'use strict';
3
- return this.init(elements, options);
4
- }
3
+ if (typeof module === 'object') {
4
+ module.exports = factory;
5
+ } else if (typeof define === 'function' && define.amd) {
6
+ define(factory);
7
+ } else {
8
+ root.MediumEditor = factory;
9
+ }
10
+ }(this, function () {
5
11
 
6
- if (typeof module === 'object') {
7
- module.exports = MediumEditor;
8
- // AMD support
9
- } else if (typeof define === 'function' && define.amd) {
10
- define(function () {
11
- 'use strict';
12
- return MediumEditor;
13
- });
14
- }
12
+ 'use strict';
13
+
14
+ var mediumEditorUtil;
15
15
 
16
16
  (function (window, document) {
17
17
  'use strict';
18
18
 
19
- var now,
20
- keyCode,
21
- DefaultButton,
22
- ButtonsData = {
23
- 'bold': {
24
- name: 'bold',
25
- action: 'bold',
26
- aria: 'bold',
27
- tagNames: ['b', 'strong'],
28
- contentDefault: '<b>B</b>',
29
- contentFA: '<i class="fa fa-bold"></i>'
30
- },
31
- 'italic': {
32
- name: 'italic',
33
- action: 'italic',
34
- aria: 'italic',
35
- tagNames: ['i', 'em'],
36
- style: {
37
- prop: 'font-style',
38
- value: 'italic'
39
- },
40
- contentDefault: '<b><i>I</i></b>',
41
- contentFA: '<i class="fa fa-italic"></i>'
42
- },
43
- 'underline': {
44
- name: 'underline',
45
- action: 'underline',
46
- aria: 'underline',
47
- tagNames: ['u'],
48
- contentDefault: '<b><u>U</u></b>',
49
- contentFA: '<i class="fa fa-underline"></i>'
50
- },
51
- 'strikethrough': {
52
- name: 'strikethrough',
53
- action: 'strikethrough',
54
- aria: 'strike through',
55
- tagNames: ['strike'],
56
- contentDefault: '<s>A</s>',
57
- contentFA: '<i class="fa fa-strikethrough"></i>'
58
- },
59
- 'superscript': {
60
- name: 'superscript',
61
- action: 'superscript',
62
- aria: 'superscript',
63
- tagNames: ['sup'],
64
- contentDefault: '<b>x<sup>1</sup></b>',
65
- contentFA: '<i class="fa fa-superscript"></i>'
66
- },
67
- 'subscript': {
68
- name: 'subscript',
69
- action: 'subscript',
70
- aria: 'subscript',
71
- tagNames: ['sub'],
72
- contentDefault: '<b>x<sub>1</sub></b>',
73
- contentFA: '<i class="fa fa-subscript"></i>'
19
+ mediumEditorUtil = {
20
+
21
+ // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
22
+ // by rg89
23
+ isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
24
+
25
+ // https://github.com/jashkenas/underscore
26
+ keyCode: {
27
+ BACKSPACE: 8,
28
+ TAB: 9,
29
+ ENTER: 13,
30
+ ESCAPE: 27,
31
+ SPACE: 32,
32
+ DELETE: 46
33
+ },
34
+
35
+ parentElements: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'],
36
+
37
+ extend: function extend(b, a) {
38
+ var prop;
39
+ if (b === undefined) {
40
+ return a;
41
+ }
42
+ for (prop in a) {
43
+ if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
44
+ b[prop] = a[prop];
45
+ }
46
+ }
47
+ return b;
48
+ },
49
+
50
+ // Find the next node in the DOM tree that represents any text that is being
51
+ // displayed directly next to the targetNode (passed as an argument)
52
+ // Text that appears directly next to the current node can be:
53
+ // - A sibling text node
54
+ // - A descendant of a sibling element
55
+ // - A sibling text node of an ancestor
56
+ // - A descendant of a sibling element of an ancestor
57
+ findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
58
+ var pastTarget = false,
59
+ nextNode,
60
+ nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
61
+
62
+ // Use a native NodeIterator to iterate over all the text nodes that are descendants
63
+ // of the rootNode. Once past the targetNode, choose the first non-empty text node
64
+ nextNode = nodeIterator.nextNode();
65
+ while (nextNode) {
66
+ if (nextNode === targetNode) {
67
+ pastTarget = true;
68
+ } else if (pastTarget) {
69
+ if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
70
+ break;
71
+ }
72
+ }
73
+ nextNode = nodeIterator.nextNode();
74
+ }
75
+
76
+ return nextNode;
77
+ },
78
+
79
+ isDescendant: function isDescendant(parent, child) {
80
+ var node = child.parentNode;
81
+ while (node !== null) {
82
+ if (node === parent) {
83
+ return true;
84
+ }
85
+ node = node.parentNode;
86
+ }
87
+ return false;
88
+ },
89
+
90
+ // https://github.com/jashkenas/underscore
91
+ isElement: function isElement(obj) {
92
+ return !!(obj && obj.nodeType === 1);
93
+ },
94
+
95
+ now: function now() {
96
+ return Date.now || new Date().getTime();
97
+ },
98
+
99
+ // https://github.com/jashkenas/underscore
100
+ throttle: function throttle(func, wait) {
101
+ var THROTTLE_INTERVAL = 50,
102
+ context,
103
+ args,
104
+ result,
105
+ timeout = null,
106
+ previous = 0,
107
+ later;
108
+
109
+ if (!wait && wait !== 0) {
110
+ wait = THROTTLE_INTERVAL;
111
+ }
112
+
113
+ later = function () {
114
+ previous = mediumEditorUtil.now();
115
+ timeout = null;
116
+ result = func.apply(context, args);
117
+ if (!timeout) {
118
+ context = args = null;
119
+ }
120
+ };
121
+
122
+ return function () {
123
+ var currNow = mediumEditorUtil.now(),
124
+ remaining = wait - (currNow - previous);
125
+ context = this;
126
+ args = arguments;
127
+ if (remaining <= 0 || remaining > wait) {
128
+ clearTimeout(timeout);
129
+ timeout = null;
130
+ previous = currNow;
131
+ result = func.apply(context, args);
132
+ if (!timeout) {
133
+ context = args = null;
134
+ }
135
+ } else if (!timeout) {
136
+ timeout = setTimeout(later, remaining);
137
+ }
138
+ return result;
139
+ };
140
+ },
141
+
142
+ traverseUp: function (current, testElementFunction) {
143
+
144
+ do {
145
+ if (current.nodeType === 1) {
146
+ if (testElementFunction(current)) {
147
+ return current;
148
+ }
149
+ // do not traverse upwards past the nearest containing editor
150
+ if (current.getAttribute('data-medium-element')) {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ current = current.parentNode;
156
+ } while (current);
157
+
158
+ return false;
159
+
160
+ },
161
+
162
+ htmlEntities: function (str) {
163
+ // converts special characters (like <) into their escaped/encoded values (like &lt;).
164
+ // This allows you to show to display the string without the browser reading it as HTML.
165
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
166
+ },
167
+
168
+ // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
169
+ insertHTMLCommand: function (doc, html) {
170
+ var selection, range, el, fragment, node, lastNode;
171
+
172
+ if (doc.queryCommandSupported('insertHTML')) {
173
+ try {
174
+ return doc.execCommand('insertHTML', false, html);
175
+ } catch (ignore) {}
176
+ }
177
+
178
+ selection = window.getSelection();
179
+ if (selection.getRangeAt && selection.rangeCount) {
180
+ range = selection.getRangeAt(0);
181
+ range.deleteContents();
182
+
183
+ el = doc.createElement("div");
184
+ el.innerHTML = html;
185
+ fragment = doc.createDocumentFragment();
186
+ while (el.firstChild) {
187
+ node = el.firstChild;
188
+ lastNode = fragment.appendChild(node);
189
+ }
190
+ range.insertNode(fragment);
191
+
192
+ // Preserve the selection:
193
+ if (lastNode) {
194
+ range = range.cloneRange();
195
+ range.setStartAfter(lastNode);
196
+ range.collapse(true);
197
+ selection.removeAllRanges();
198
+ selection.addRange(range);
199
+ }
200
+ }
201
+ },
202
+
203
+ // TODO: not sure if this should be here
204
+ setTargetBlank: function (el) {
205
+ var i;
206
+ if (el.tagName.toLowerCase() === 'a') {
207
+ el.target = '_blank';
208
+ } else {
209
+ el = el.getElementsByTagName('a');
210
+
211
+ for (i = 0; i < el.length; i += 1) {
212
+ el[i].target = '_blank';
213
+ }
214
+ }
215
+ },
216
+
217
+ isListItemChild: function (node) {
218
+ var parentNode = node.parentNode,
219
+ tagName = parentNode.tagName.toLowerCase();
220
+ while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
221
+ if (tagName === 'li') {
222
+ return true;
223
+ }
224
+ parentNode = parentNode.parentNode;
225
+ if (parentNode && parentNode.tagName) {
226
+ tagName = parentNode.tagName.toLowerCase();
227
+ } else {
228
+ return false;
229
+ }
230
+ }
231
+ return false;
232
+ }
233
+ };
234
+ }(window, document));
235
+
236
+ var meSelection;
237
+
238
+ (function (window, document) {
239
+ 'use strict';
240
+
241
+ meSelection = {
242
+ // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
243
+ // by You
244
+ getSelectionStart: function (ownerDocument) {
245
+ var node = ownerDocument.getSelection().anchorNode,
246
+ startNode = (node && node.nodeType === 3 ? node.parentNode : node);
247
+ return startNode;
248
+ },
249
+
250
+ findMatchingSelectionParent: function (testElementFunction, contentWindow) {
251
+ var selection = contentWindow.getSelection(), range, current;
252
+
253
+ if (selection.rangeCount === 0) {
254
+ return false;
255
+ }
256
+
257
+ range = selection.getRangeAt(0);
258
+ current = range.commonAncestorContainer;
259
+
260
+ return mediumEditorUtil.traverseUp(current, testElementFunction);
261
+ },
262
+
263
+ getSelectionElement: function (contentWindow) {
264
+ return this.findMatchingSelectionParent(function (el) {
265
+ return el.getAttribute('data-medium-element');
266
+ }, contentWindow);
267
+ },
268
+
269
+ selectionInContentEditableFalse: function (contentWindow) {
270
+ return this.findMatchingSelectionParent(function (el) {
271
+ return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
272
+ }, contentWindow);
273
+ },
274
+
275
+ // http://stackoverflow.com/questions/4176923/html-of-selected-text
276
+ // by Tim Down
277
+ getSelectionHtml: function getSelectionHtml() {
278
+ var i,
279
+ html = '',
280
+ sel,
281
+ len,
282
+ container;
283
+ if (this.options.contentWindow.getSelection !== undefined) {
284
+ sel = this.options.contentWindow.getSelection();
285
+ if (sel.rangeCount) {
286
+ container = this.options.ownerDocument.createElement('div');
287
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
288
+ container.appendChild(sel.getRangeAt(i).cloneContents());
289
+ }
290
+ html = container.innerHTML;
291
+ }
292
+ } else if (this.options.ownerDocument.selection !== undefined) {
293
+ if (this.options.ownerDocument.selection.type === 'Text') {
294
+ html = this.options.ownerDocument.selection.createRange().htmlText;
295
+ }
296
+ }
297
+ return html;
298
+ },
299
+
300
+ /**
301
+ * Find the caret position within an element irrespective of any inline tags it may contain.
302
+ *
303
+ * @param {DOMElement} An element containing the cursor to find offsets relative to.
304
+ * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
305
+ * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
306
+ */
307
+ getCaretOffsets: function getCaretOffsets(element, range) {
308
+ var preCaretRange, postCaretRange;
309
+
310
+ if (!range) {
311
+ range = window.getSelection().getRangeAt(0);
312
+ }
313
+
314
+ preCaretRange = range.cloneRange();
315
+ postCaretRange = range.cloneRange();
316
+
317
+ preCaretRange.selectNodeContents(element);
318
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
319
+
320
+ postCaretRange.selectNodeContents(element);
321
+ postCaretRange.setStart(range.endContainer, range.endOffset);
322
+
323
+ return {
324
+ left: preCaretRange.toString().length,
325
+ right: postCaretRange.toString().length
326
+ };
327
+ },
328
+
329
+ // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
330
+ rangeSelectsSingleNode: function (range) {
331
+ var startNode = range.startContainer;
332
+ return startNode === range.endContainer &&
333
+ startNode.hasChildNodes() &&
334
+ range.endOffset === range.startOffset + 1;
335
+ },
336
+
337
+ getSelectedParentElement: function (range) {
338
+ var selectedParentElement = null;
339
+ if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
340
+ selectedParentElement = range.startContainer.childNodes[range.startOffset];
341
+ } else if (range.startContainer.nodeType === 3) {
342
+ selectedParentElement = range.startContainer.parentNode;
343
+ } else {
344
+ selectedParentElement = range.startContainer;
345
+ }
346
+ return selectedParentElement;
347
+ },
348
+
349
+ getSelectionData: function (el) {
350
+ var tagName;
351
+
352
+ if (el && el.tagName) {
353
+ tagName = el.tagName.toLowerCase();
354
+ }
355
+
356
+ while (el && mediumEditorUtil.parentElements.indexOf(tagName) === -1) {
357
+ el = el.parentNode;
358
+ if (el && el.tagName) {
359
+ tagName = el.tagName.toLowerCase();
360
+ }
361
+ }
362
+
363
+ return {
364
+ el: el,
365
+ tagName: tagName
366
+ };
367
+ }
368
+ };
369
+ }(document, window));
370
+
371
+ var DefaultButton,
372
+ ButtonsData;
373
+
374
+ (function (window, document) {
375
+ 'use strict';
376
+
377
+ ButtonsData = {
378
+ 'bold': {
379
+ name: 'bold',
380
+ action: 'bold',
381
+ aria: 'bold',
382
+ tagNames: ['b', 'strong'],
383
+ style: {
384
+ prop: 'font-weight',
385
+ value: '700|bold'
74
386
  },
75
- 'anchor': {
76
- name: 'anchor',
77
- action: 'anchor',
78
- aria: 'link',
79
- tagNames: ['a'],
80
- contentDefault: '<b>#</b>',
81
- contentFA: '<i class="fa fa-link"></i>'
387
+ useQueryState: true,
388
+ contentDefault: '<b>B</b>',
389
+ contentFA: '<i class="fa fa-bold"></i>'
390
+ },
391
+ 'italic': {
392
+ name: 'italic',
393
+ action: 'italic',
394
+ aria: 'italic',
395
+ tagNames: ['i', 'em'],
396
+ style: {
397
+ prop: 'font-style',
398
+ value: 'italic'
82
399
  },
83
- 'image': {
84
- name: 'image',
85
- action: 'image',
86
- aria: 'image',
87
- tagNames: ['img'],
88
- contentDefault: '<b>image</b>',
89
- contentFA: '<i class="fa fa-picture-o"></i>'
400
+ useQueryState: true,
401
+ contentDefault: '<b><i>I</i></b>',
402
+ contentFA: '<i class="fa fa-italic"></i>'
403
+ },
404
+ 'underline': {
405
+ name: 'underline',
406
+ action: 'underline',
407
+ aria: 'underline',
408
+ tagNames: ['u'],
409
+ style: {
410
+ prop: 'text-decoration',
411
+ value: 'underline'
90
412
  },
91
- 'quote': {
92
- name: 'quote',
93
- action: 'append-blockquote',
94
- aria: 'blockquote',
95
- tagNames: ['blockquote'],
96
- contentDefault: '<b>&ldquo;</b>',
97
- contentFA: '<i class="fa fa-quote-right"></i>'
413
+ useQueryState: true,
414
+ contentDefault: '<b><u>U</u></b>',
415
+ contentFA: '<i class="fa fa-underline"></i>'
416
+ },
417
+ 'strikethrough': {
418
+ name: 'strikethrough',
419
+ action: 'strikethrough',
420
+ aria: 'strike through',
421
+ tagNames: ['strike'],
422
+ style: {
423
+ prop: 'text-decoration',
424
+ value: 'line-through'
98
425
  },
99
- 'orderedlist': {
100
- name: 'orderedlist',
101
- action: 'insertorderedlist',
102
- aria: 'ordered list',
103
- tagNames: ['ol'],
104
- contentDefault: '<b>1.</b>',
105
- contentFA: '<i class="fa fa-list-ol"></i>'
426
+ useQueryState: true,
427
+ contentDefault: '<s>A</s>',
428
+ contentFA: '<i class="fa fa-strikethrough"></i>'
429
+ },
430
+ 'superscript': {
431
+ name: 'superscript',
432
+ action: 'superscript',
433
+ aria: 'superscript',
434
+ tagNames: ['sup'],
435
+ /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript
436
+ https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
437
+ // useQueryState: true
438
+ contentDefault: '<b>x<sup>1</sup></b>',
439
+ contentFA: '<i class="fa fa-superscript"></i>'
440
+ },
441
+ 'subscript': {
442
+ name: 'subscript',
443
+ action: 'subscript',
444
+ aria: 'subscript',
445
+ tagNames: ['sub'],
446
+ /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript
447
+ https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
448
+ // useQueryState: true
449
+ contentDefault: '<b>x<sub>1</sub></b>',
450
+ contentFA: '<i class="fa fa-subscript"></i>'
451
+ },
452
+ 'anchor': {
453
+ name: 'anchor',
454
+ action: 'anchor',
455
+ aria: 'link',
456
+ tagNames: ['a'],
457
+ contentDefault: '<b>#</b>',
458
+ contentFA: '<i class="fa fa-link"></i>'
459
+ },
460
+ 'image': {
461
+ name: 'image',
462
+ action: 'image',
463
+ aria: 'image',
464
+ tagNames: ['img'],
465
+ contentDefault: '<b>image</b>',
466
+ contentFA: '<i class="fa fa-picture-o"></i>'
467
+ },
468
+ 'quote': {
469
+ name: 'quote',
470
+ action: 'append-blockquote',
471
+ aria: 'blockquote',
472
+ tagNames: ['blockquote'],
473
+ contentDefault: '<b>&ldquo;</b>',
474
+ contentFA: '<i class="fa fa-quote-right"></i>'
475
+ },
476
+ 'orderedlist': {
477
+ name: 'orderedlist',
478
+ action: 'insertorderedlist',
479
+ aria: 'ordered list',
480
+ tagNames: ['ol'],
481
+ useQueryState: true,
482
+ contentDefault: '<b>1.</b>',
483
+ contentFA: '<i class="fa fa-list-ol"></i>'
484
+ },
485
+ 'unorderedlist': {
486
+ name: 'unorderedlist',
487
+ action: 'insertunorderedlist',
488
+ aria: 'unordered list',
489
+ tagNames: ['ul'],
490
+ useQueryState: true,
491
+ contentDefault: '<b>&bull;</b>',
492
+ contentFA: '<i class="fa fa-list-ul"></i>'
493
+ },
494
+ 'pre': {
495
+ name: 'pre',
496
+ action: 'append-pre',
497
+ aria: 'preformatted text',
498
+ tagNames: ['pre'],
499
+ contentDefault: '<b>0101</b>',
500
+ contentFA: '<i class="fa fa-code fa-lg"></i>'
501
+ },
502
+ 'indent': {
503
+ name: 'indent',
504
+ action: 'indent',
505
+ aria: 'indent',
506
+ tagNames: [],
507
+ contentDefault: '<b>&rarr;</b>',
508
+ contentFA: '<i class="fa fa-indent"></i>'
509
+ },
510
+ 'outdent': {
511
+ name: 'outdent',
512
+ action: 'outdent',
513
+ aria: 'outdent',
514
+ tagNames: [],
515
+ contentDefault: '<b>&larr;</b>',
516
+ contentFA: '<i class="fa fa-outdent"></i>'
517
+ },
518
+ 'justifyCenter': {
519
+ name: 'justifyCenter',
520
+ action: 'justifyCenter',
521
+ aria: 'center justify',
522
+ tagNames: [],
523
+ style: {
524
+ prop: 'text-align',
525
+ value: 'center'
106
526
  },
107
- 'unorderedlist': {
108
- name: 'unorderedlist',
109
- action: 'insertunorderedlist',
110
- aria: 'unordered list',
111
- tagNames: ['ul'],
112
- contentDefault: '<b>&bull;</b>',
113
- contentFA: '<i class="fa fa-list-ul"></i>'
527
+ useQueryState: true,
528
+ contentDefault: '<b>C</b>',
529
+ contentFA: '<i class="fa fa-align-center"></i>'
530
+ },
531
+ 'justifyFull': {
532
+ name: 'justifyFull',
533
+ action: 'justifyFull',
534
+ aria: 'full justify',
535
+ tagNames: [],
536
+ style: {
537
+ prop: 'text-align',
538
+ value: 'justify'
114
539
  },
115
- 'pre': {
116
- name: 'pre',
117
- action: 'append-pre',
118
- aria: 'preformatted text',
119
- tagNames: ['pre'],
120
- contentDefault: '<b>0101</b>',
121
- contentFA: '<i class="fa fa-code fa-lg"></i>'
540
+ useQueryState: true,
541
+ contentDefault: '<b>J</b>',
542
+ contentFA: '<i class="fa fa-align-justify"></i>'
543
+ },
544
+ 'justifyLeft': {
545
+ name: 'justifyLeft',
546
+ action: 'justifyLeft',
547
+ aria: 'left justify',
548
+ tagNames: [],
549
+ style: {
550
+ prop: 'text-align',
551
+ value: 'left'
122
552
  },
123
- 'indent': {
124
- name: 'indent',
125
- action: 'indent',
126
- aria: 'indent',
127
- tagNames: [],
128
- contentDefault: '<b>&rarr;</b>',
129
- contentFA: '<i class="fa fa-indent"></i>'
553
+ useQueryState: true,
554
+ contentDefault: '<b>L</b>',
555
+ contentFA: '<i class="fa fa-align-left"></i>'
556
+ },
557
+ 'justifyRight': {
558
+ name: 'justifyRight',
559
+ action: 'justifyRight',
560
+ aria: 'right justify',
561
+ tagNames: [],
562
+ style: {
563
+ prop: 'text-align',
564
+ value: 'right'
130
565
  },
131
- 'outdent': {
132
- name: 'outdent',
133
- action: 'outdent',
134
- aria: 'outdent',
135
- tagNames: [],
136
- contentDefault: '<b>&larr;</b>',
137
- contentFA: '<i class="fa fa-outdent"></i>'
566
+ useQueryState: true,
567
+ contentDefault: '<b>R</b>',
568
+ contentFA: '<i class="fa fa-align-right"></i>'
569
+ },
570
+ 'header1': {
571
+ name: 'header1',
572
+ action: function (options) {
573
+ return 'append-' + options.firstHeader;
138
574
  },
139
- 'justifyCenter': {
140
- name: 'justifyCenter',
141
- action: 'justifyCenter',
142
- aria: 'center justify',
143
- tagNames: [],
144
- style: {
145
- prop: 'text-align',
146
- value: 'center'
147
- },
148
- contentDefault: '<b>C</b>',
149
- contentFA: '<i class="fa fa-align-center"></i>'
575
+ aria: function (options) {
576
+ return options.firstHeader;
150
577
  },
151
- 'justifyFull': {
152
- name: 'justifyFull',
153
- action: 'justifyFull',
154
- aria: 'full justify',
155
- tagNames: [],
156
- style: {
157
- prop: 'text-align',
158
- value: 'justify'
159
- },
160
- contentDefault: '<b>J</b>',
161
- contentFA: '<i class="fa fa-align-justify"></i>'
578
+ tagNames: function (options) {
579
+ return [options.firstHeader];
162
580
  },
163
- 'justifyLeft': {
164
- name: 'justifyLeft',
165
- action: 'justifyLeft',
166
- aria: 'left justify',
167
- tagNames: [],
168
- style: {
169
- prop: 'text-align',
170
- value: 'left'
171
- },
172
- contentDefault: '<b>L</b>',
173
- contentFA: '<i class="fa fa-align-left"></i>'
581
+ contentDefault: '<b>H1</b>'
582
+ },
583
+ 'header2': {
584
+ name: 'header2',
585
+ action: function (options) {
586
+ return 'append-' + options.secondHeader;
174
587
  },
175
- 'justifyRight': {
176
- name: 'justifyRight',
177
- action: 'justifyRight',
178
- aria: 'right justify',
179
- tagNames: [],
180
- style: {
181
- prop: 'text-align',
182
- value: 'right'
183
- },
184
- contentDefault: '<b>R</b>',
185
- contentFA: '<i class="fa fa-align-right"></i>'
588
+ aria: function (options) {
589
+ return options.secondHeader;
186
590
  },
187
- 'header1': {
188
- name: 'header1',
189
- action: function (options) {
190
- return 'append-' + options.firstHeader;
191
- },
192
- aria: function (options) {
193
- return options.firstHeader;
194
- },
195
- tagNames: function (options) {
196
- return [options.firstHeader];
197
- },
198
- contentDefault: '<b>H1</b>'
591
+ tagNames: function (options) {
592
+ return [options.secondHeader];
199
593
  },
200
- 'header2': {
201
- name: 'header2',
202
- action: function (options) {
203
- return 'append-' + options.secondHeader;
204
- },
205
- aria: function (options) {
206
- return options.secondHeader;
207
- },
208
- tagNames: function (options) {
209
- return [options.secondHeader];
210
- },
211
- contentDefault: '<b>H2</b>'
212
- }
213
- };
594
+ contentDefault: '<b>H2</b>'
595
+ }
596
+ };
214
597
 
215
598
  DefaultButton = function (options, instance) {
216
599
  this.options = options;
@@ -286,235 +669,487 @@ if (typeof module === 'object') {
286
669
  this.button.classList.add(this.base.options.activeButtonClass);
287
670
  delete this.knownState;
288
671
  },
672
+ queryCommandState: function () {
673
+ var queryState = null;
674
+ if (this.options.useQueryState) {
675
+ try {
676
+ queryState = this.base.options.ownerDocument.queryCommandState(this.getAction());
677
+ } catch (exc) {
678
+ queryState = null;
679
+ }
680
+ }
681
+ return queryState;
682
+ },
289
683
  shouldActivate: function (node) {
290
684
  var isMatch = false,
291
- tagNames = this.getTagNames();
685
+ tagNames = this.getTagNames(),
686
+ styleVals,
687
+ computedStyle;
292
688
  if (this.knownState === false || this.knownState === true) {
293
689
  return this.knownState;
294
690
  }
295
691
 
296
- if (tagNames && tagNames.length > 0 && node.tagName) {
297
- isMatch = tagNames.indexOf(node.tagName.toLowerCase()) !== -1;
692
+ if (tagNames && tagNames.length > 0 && node.tagName) {
693
+ isMatch = tagNames.indexOf(node.tagName.toLowerCase()) !== -1;
694
+ }
695
+
696
+ if (!isMatch && this.options.style) {
697
+ styleVals = this.options.style.value.split('|');
698
+ computedStyle = this.base.options.contentWindow.getComputedStyle(node, null).getPropertyValue(this.options.style.prop);
699
+ styleVals.forEach(function (val) {
700
+ if (!this.knownState) {
701
+ this.knownState = isMatch = (computedStyle.indexOf(val) !== -1);
702
+ }
703
+ }.bind(this));
704
+ }
705
+
706
+ return isMatch;
707
+ }
708
+ };
709
+ }(window, document));
710
+ var pasteHandler;
711
+
712
+ (function (window, document) {
713
+ 'use strict';
714
+ /*jslint regexp: true*/
715
+ /*
716
+ jslint does not allow character negation, because the negation
717
+ will not match any unicode characters. In the regexes in this
718
+ block, negation is used specifically to match the end of an html
719
+ tag, and in fact unicode characters *should* be allowed.
720
+ */
721
+ function createReplacements() {
722
+ return [
723
+
724
+ // replace two bogus tags that begin pastes from google docs
725
+ [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ""],
726
+ [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ""],
727
+
728
+ // un-html spaces and newlines inserted by OS X
729
+ [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
730
+ [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
731
+
732
+ // replace google docs italics+bold with a span to be replaced once the html is inserted
733
+ [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
734
+
735
+ // replace google docs italics with a span to be replaced once the html is inserted
736
+ [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
737
+
738
+ //[replace google docs bolds with a span to be replaced once the html is inserted
739
+ [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
740
+
741
+ // replace manually entered b/i/a tags with real ones
742
+ [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
743
+
744
+ // replace manually a tags with real ones, converting smart-quotes from google docs
745
+ [new RegExp(/&lt;a\s+href=(&quot;|&rdquo;|&ldquo;|“|”)([^&]+)(&quot;|&rdquo;|&ldquo;|“|”)&gt;/gi), '<a href="$2">']
746
+
747
+ ];
748
+ }
749
+ /*jslint regexp: false*/
750
+
751
+ pasteHandler = {
752
+ handlePaste: function (element, evt, options) {
753
+ var paragraphs,
754
+ html = '',
755
+ p,
756
+ dataFormatHTML = 'text/html',
757
+ dataFormatPlain = 'text/plain';
758
+
759
+ element.classList.remove('medium-editor-placeholder');
760
+ if (!options.forcePlainText && !options.cleanPastedHTML) {
761
+ return element;
762
+ }
763
+
764
+ if (options.contentWindow.clipboardData && evt.clipboardData === undefined) {
765
+ evt.clipboardData = options.contentWindow.clipboardData;
766
+ // If window.clipboardData exists, but e.clipboardData doesn't exist,
767
+ // we're probably in IE. IE only has two possibilities for clipboard
768
+ // data format: 'Text' and 'URL'.
769
+ //
770
+ // Of the two, we want 'Text':
771
+ dataFormatHTML = 'Text';
772
+ dataFormatPlain = 'Text';
773
+ }
774
+
775
+ if (evt.clipboardData && evt.clipboardData.getData && !evt.defaultPrevented) {
776
+ evt.preventDefault();
777
+
778
+ if (options.cleanPastedHTML && evt.clipboardData.getData(dataFormatHTML)) {
779
+ return this.cleanPaste(evt.clipboardData.getData(dataFormatHTML), options);
780
+ }
781
+ if (!(options.disableReturn || element.getAttribute('data-disable-return'))) {
782
+ paragraphs = evt.clipboardData.getData(dataFormatPlain).split(/[\r\n]/g);
783
+ for (p = 0; p < paragraphs.length; p += 1) {
784
+ if (paragraphs[p] !== '') {
785
+ html += '<p>' + mediumEditorUtil.htmlEntities(paragraphs[p]) + '</p>';
786
+ }
787
+ }
788
+ mediumEditorUtil.insertHTMLCommand(options.ownerDocument, html);
789
+ } else {
790
+ html = mediumEditorUtil.htmlEntities(evt.clipboardData.getData(dataFormatPlain));
791
+ mediumEditorUtil.insertHTMLCommand(options.ownerDocument, html);
792
+ }
793
+ }
794
+ },
795
+
796
+ cleanPaste: function (text, options) {
797
+ var i, elList, workEl,
798
+ el = meSelection.getSelectionElement(options.contentWindow),
799
+ multiline = /<p|<br|<div/.test(text),
800
+ replacements = createReplacements();
801
+
802
+ for (i = 0; i < replacements.length; i += 1) {
803
+ text = text.replace(replacements[i][0], replacements[i][1]);
804
+ }
805
+
806
+ if (multiline) {
807
+ // double br's aren't converted to p tags, but we want paragraphs.
808
+ elList = text.split('<br><br>');
809
+
810
+ this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>', options.ownerDocument);
811
+ options.ownerDocument.execCommand('insertText', false, "\n");
812
+
813
+ // block element cleanup
814
+ elList = el.querySelectorAll('a,p,div,br');
815
+ for (i = 0; i < elList.length; i += 1) {
816
+ workEl = elList[i];
817
+
818
+ switch (workEl.tagName.toLowerCase()) {
819
+ case 'a':
820
+ if (options.targetBlank) {
821
+ mediumEditorUtil.setTargetBlank(workEl);
822
+ }
823
+ break;
824
+ case 'p':
825
+ case 'div':
826
+ this.filterCommonBlocks(workEl);
827
+ break;
828
+ case 'br':
829
+ this.filterLineBreak(workEl);
830
+ break;
831
+ }
832
+ }
833
+ } else {
834
+ this.pasteHTML(text, options.ownerDocument);
835
+ }
836
+ },
837
+
838
+ pasteHTML: function (html, ownerDocument) {
839
+ var elList, workEl, i, fragmentBody, pasteBlock = ownerDocument.createDocumentFragment();
840
+
841
+ pasteBlock.appendChild(ownerDocument.createElement('body'));
842
+
843
+ fragmentBody = pasteBlock.querySelector('body');
844
+ fragmentBody.innerHTML = html;
845
+
846
+ this.cleanupSpans(fragmentBody, ownerDocument);
847
+
848
+ elList = fragmentBody.querySelectorAll('*');
849
+ for (i = 0; i < elList.length; i += 1) {
850
+ workEl = elList[i];
851
+
852
+ // delete ugly attributes
853
+ workEl.removeAttribute('class');
854
+ workEl.removeAttribute('style');
855
+ workEl.removeAttribute('dir');
856
+
857
+ if (workEl.tagName.toLowerCase() === 'meta') {
858
+ workEl.parentNode.removeChild(workEl);
859
+ }
860
+ }
861
+ mediumEditorUtil.insertHTMLCommand(ownerDocument, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
862
+ },
863
+ isCommonBlock: function (el) {
864
+ return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
865
+ },
866
+ filterCommonBlocks: function (el) {
867
+ if (/^\s*$/.test(el.textContent)) {
868
+ el.parentNode.removeChild(el);
869
+ }
870
+ },
871
+ filterLineBreak: function (el) {
872
+ if (this.isCommonBlock(el.previousElementSibling)) {
873
+ // remove stray br's following common block elements
874
+ el.parentNode.removeChild(el);
875
+ } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
876
+ // remove br's just inside open or close tags of a div/p
877
+ el.parentNode.removeChild(el);
878
+ } else if (el.parentNode.childElementCount === 1) {
879
+ // and br's that are the only child of a div/p
880
+ this.removeWithParent(el);
881
+ }
882
+
883
+ },
884
+
885
+ // remove an element, including its parent, if it is the only element within its parent
886
+ removeWithParent: function (el) {
887
+ if (el && el.parentNode) {
888
+ if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
889
+ el.parentNode.parentNode.removeChild(el.parentNode);
890
+ } else {
891
+ el.parentNode.removeChild(el.parentNode);
892
+ }
893
+ }
894
+ },
895
+
896
+ cleanupSpans: function (container_el, ownerDocument) {
897
+ var i,
898
+ el,
899
+ new_el,
900
+ spans = container_el.querySelectorAll('.replace-with'),
901
+ isCEF = function (el) {
902
+ return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
903
+ };
904
+
905
+ for (i = 0; i < spans.length; i += 1) {
906
+ el = spans[i];
907
+ new_el = ownerDocument.createElement(el.classList.contains('bold') ? 'b' : 'i');
908
+
909
+ if (el.classList.contains('bold') && el.classList.contains('italic')) {
910
+ // add an i tag as well if this has both italics and bold
911
+ new_el.innerHTML = '<i>' + el.innerHTML + '</i>';
912
+ } else {
913
+ new_el.innerHTML = el.innerHTML;
914
+ }
915
+ el.parentNode.replaceChild(new_el, el);
916
+ }
917
+
918
+ spans = container_el.querySelectorAll('span');
919
+ for (i = 0; i < spans.length; i += 1) {
920
+ el = spans[i];
921
+
922
+ // bail if span is in contenteditable = false
923
+ if (mediumEditorUtil.traverseUp(el, isCEF)) {
924
+ return false;
925
+ }
926
+
927
+ // remove empty spans, replace others with their contents
928
+ if (/^\s*$/.test()) {
929
+ el.parentNode.removeChild(el);
930
+ } else {
931
+ el.parentNode.replaceChild(ownerDocument.createTextNode(el.textContent), el);
932
+ }
933
+ }
934
+ }
935
+ };
936
+ }(window, document));
937
+
938
+ var AnchorExtension;
939
+
940
+ (function (window, document) {
941
+ 'use strict';
942
+
943
+ AnchorExtension = function (instance) {
944
+ this.base = instance;
945
+ };
946
+
947
+ AnchorExtension.prototype = {
948
+
949
+ getForm: function () {
950
+ if (!this.anchorForm) {
951
+ this.anchorForm = this.createForm();
952
+ }
953
+ return this.anchorForm;
954
+ },
955
+
956
+ getInput: function () {
957
+ return this.getForm().querySelector('input.medium-editor-toolbar-input');
958
+ },
959
+
960
+ deactivate: function () {
961
+ if (!this.anchorForm) {
962
+ return false;
298
963
  }
299
964
 
300
- if (!isMatch && this.options.style) {
301
- this.knownState = isMatch = (this.base.options.contentWindow.getComputedStyle(node, null).getPropertyValue(this.options.style.prop).indexOf(this.options.style.value) !== -1);
965
+ if (this.anchorForm.parentNode) {
966
+ this.anchorForm.parentNode.removeChild(this.anchorForm);
302
967
  }
303
968
 
304
- return isMatch;
305
- }
306
- };
969
+ delete this.anchorForm;
970
+ },
307
971
 
308
- function extend(b, a) {
309
- var prop;
310
- if (b === undefined) {
311
- return a;
312
- }
313
- for (prop in a) {
314
- if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
315
- b[prop] = a[prop];
972
+ doLinkCreation: function () {
973
+ var button = null,
974
+ target,
975
+ targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'),
976
+ buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button');
977
+
978
+ if (targetCheckbox && targetCheckbox.checked) {
979
+ target = "_blank";
980
+ } else {
981
+ target = "_self";
316
982
  }
317
- }
318
- return b;
319
- }
320
983
 
321
- // https://github.com/jashkenas/underscore
322
- now = Date.now || function () {
323
- return new Date().getTime();
324
- };
984
+ if (buttonCheckbox && buttonCheckbox.checked) {
985
+ button = this.base.options.anchorButtonClass;
986
+ }
325
987
 
326
- keyCode = {
327
- BACKSPACE: 8,
328
- TAB: 9,
329
- ENTER: 13,
330
- ESCAPE: 27,
331
- SPACE: 32,
332
- DELETE: 46
333
- };
988
+ this.base.createLink(this.getInput(), target, button);
989
+ },
334
990
 
335
- // https://github.com/jashkenas/underscore
336
- function throttle(func, wait) {
337
- var THROTTLE_INTERVAL = 50,
338
- context,
339
- args,
340
- result,
341
- timeout = null,
342
- previous = 0,
343
- later;
344
-
345
- if (!wait && wait !== 0) {
346
- wait = THROTTLE_INTERVAL;
347
- }
991
+ doFormCancel: function () {
992
+ this.base.showToolbarActions();
993
+ this.base.restoreSelection();
994
+ },
348
995
 
349
- later = function () {
350
- previous = now();
351
- timeout = null;
352
- result = func.apply(context, args);
353
- if (!timeout) {
354
- context = args = null;
355
- }
356
- };
357
-
358
- return function () {
359
- var currNow = now(),
360
- remaining = wait - (currNow - previous);
361
- context = this;
362
- args = arguments;
363
- if (remaining <= 0 || remaining > wait) {
364
- clearTimeout(timeout);
365
- timeout = null;
366
- previous = currNow;
367
- result = func.apply(context, args);
368
- if (!timeout) {
369
- context = args = null;
370
- }
371
- } else if (!timeout) {
372
- timeout = setTimeout(later, remaining);
996
+ handleOutsideInteraction: function (event) {
997
+ if (event.target !== this.getForm() &&
998
+ !mediumEditorUtil.isDescendant(this.getForm(), event.target) &&
999
+ !mediumEditorUtil.isDescendant(this.base.toolbarActions, event.target)) {
1000
+ this.base.keepToolbarAlive = false;
1001
+ this.base.checkSelection();
373
1002
  }
374
- return result;
375
- };
376
- }
1003
+ },
377
1004
 
378
- function isDescendant(parent, child) {
379
- var node = child.parentNode;
380
- while (node !== null) {
381
- if (node === parent) {
382
- return true;
383
- }
384
- node = node.parentNode;
385
- }
386
- return false;
387
- }
1005
+ createForm: function () {
1006
+ var doc = this.base.options.ownerDocument,
1007
+ form = doc.createElement('div'),
1008
+ input = doc.createElement('input'),
1009
+ close = doc.createElement('a'),
1010
+ save = doc.createElement('a'),
1011
+ target,
1012
+ target_label,
1013
+ button,
1014
+ button_label;
388
1015
 
389
- // Find the next node in the DOM tree that represents any text that is being
390
- // displayed directly next to the targetNode (passed as an argument)
391
- // Text that appears directly next to the current node can be:
392
- // - A sibling text node
393
- // - A descendant of a sibling element
394
- // - A sibling text node of an ancestor
395
- // - A descendant of a sibling element of an ancestor
396
- function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
397
- var pastTarget = false,
398
- nextNode,
399
- nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
400
-
401
- // Use a native NodeIterator to iterate over all the text nodes that are descendants
402
- // of the rootNode. Once past the targetNode, choose the first non-empty text node
403
- nextNode = nodeIterator.nextNode();
404
- while (nextNode) {
405
- if (nextNode === targetNode) {
406
- pastTarget = true;
407
- } else if (pastTarget) {
408
- if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
409
- break;
1016
+ // Anchor Form (div)
1017
+ form.className = 'medium-editor-toolbar-form';
1018
+ form.id = 'medium-editor-toolbar-form-anchor-' + this.base.id;
1019
+
1020
+ // Handle clicks on the form itself
1021
+ this.base.on(form, 'click', function (event) {
1022
+ event.stopPropagation();
1023
+ this.base.keepToolbarAlive = true;
1024
+ }.bind(this));
1025
+
1026
+ // Add url textbox
1027
+ input.setAttribute('type', 'text');
1028
+ input.className = 'medium-editor-toolbar-input';
1029
+ input.setAttribute('placeholder', this.base.options.anchorInputPlaceholder);
1030
+ form.appendChild(input);
1031
+
1032
+ // Handle typing in the textbox
1033
+ this.base.on(input, 'keyup', function (event) {
1034
+ // For ENTER -> create the anchor
1035
+ if (event.keyCode === mediumEditorUtil.keyCode.ENTER) {
1036
+ event.preventDefault();
1037
+ this.doLinkCreation();
1038
+ return;
410
1039
  }
411
- }
412
- nextNode = nodeIterator.nextNode();
413
- }
414
1040
 
415
- return nextNode;
416
- }
1041
+ // For ESCAPE -> close the form
1042
+ if (event.keyCode === mediumEditorUtil.keyCode.ESCAPE) {
1043
+ event.preventDefault();
1044
+ this.doFormCancel();
1045
+ }
1046
+ }.bind(this));
417
1047
 
418
- // http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element
419
- // by Tim Down
420
- function saveSelection() {
421
- var i,
422
- len,
423
- ranges,
424
- sel = this.options.contentWindow.getSelection();
425
- if (sel.getRangeAt && sel.rangeCount) {
426
- ranges = [];
427
- for (i = 0, len = sel.rangeCount; i < len; i += 1) {
428
- ranges.push(sel.getRangeAt(i));
429
- }
430
- return ranges;
431
- }
432
- return null;
433
- }
1048
+ // Handle clicks into the textbox
1049
+ this.base.on(input, 'click', function (event) {
1050
+ // make sure not to hide form when cliking into the input
1051
+ event.stopPropagation();
1052
+ this.base.keepToolbarAlive = true;
1053
+ }.bind(this));
434
1054
 
435
- function restoreSelection(savedSel) {
436
- var i,
437
- len,
438
- sel = this.options.contentWindow.getSelection();
439
- if (savedSel) {
440
- sel.removeAllRanges();
441
- for (i = 0, len = savedSel.length; i < len; i += 1) {
442
- sel.addRange(savedSel[i]);
443
- }
444
- }
445
- }
1055
+ // Add save buton
1056
+ save.setAttribute('href', '#');
1057
+ save.className = 'medium-editor-toobar-save';
1058
+ save.innerHTML = '&#10003;';
1059
+ form.appendChild(save);
446
1060
 
447
- // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
448
- // by You
449
- function getSelectionStart() {
450
- var node = this.options.ownerDocument.getSelection().anchorNode,
451
- startNode = (node && node.nodeType === 3 ? node.parentNode : node);
452
- return startNode;
453
- }
1061
+ // Handle save button clicks (capture)
1062
+ this.base.on(save, 'click', function (event) {
1063
+ // Clicking Save -> create the anchor
1064
+ event.preventDefault();
1065
+ this.doLinkCreation();
1066
+ }.bind(this), true);
454
1067
 
455
- // http://stackoverflow.com/questions/4176923/html-of-selected-text
456
- // by Tim Down
457
- function getSelectionHtml() {
458
- var i,
459
- html = '',
460
- sel,
461
- len,
462
- container;
463
- if (this.options.contentWindow.getSelection !== undefined) {
464
- sel = this.options.contentWindow.getSelection();
465
- if (sel.rangeCount) {
466
- container = this.options.ownerDocument.createElement('div');
467
- for (i = 0, len = sel.rangeCount; i < len; i += 1) {
468
- container.appendChild(sel.getRangeAt(i).cloneContents());
469
- }
470
- html = container.innerHTML;
1068
+ // Add close button
1069
+ close.setAttribute('href', '#');
1070
+ close.className = 'medium-editor-toobar-close';
1071
+ close.innerHTML = '&times;';
1072
+ form.appendChild(close);
1073
+
1074
+ // Handle close button clicks
1075
+ this.base.on(close, 'click', function (event) {
1076
+ // Click Close -> close the form
1077
+ event.preventDefault();
1078
+ this.doFormCancel();
1079
+ }.bind(this));
1080
+
1081
+ // (Optional) Add 'open in new window' checkbox
1082
+ if (this.base.options.anchorTarget) {
1083
+ target = doc.createElement('input');
1084
+ target.setAttribute('type', 'checkbox');
1085
+ target.className = 'medium-editor-toolbar-anchor-target';
1086
+
1087
+ target_label = doc.createElement('label');
1088
+ target_label.innerHTML = this.base.options.anchorInputCheckboxLabel;
1089
+ target_label.insertBefore(target, target_label.firstChild);
1090
+
1091
+ form.appendChild(target_label);
471
1092
  }
472
- } else if (this.options.ownerDocument.selection !== undefined) {
473
- if (this.options.ownerDocument.selection.type === 'Text') {
474
- html = this.options.ownerDocument.selection.createRange().htmlText;
1093
+
1094
+ // (Optional) Add 'add button class to anchor' checkbox
1095
+ if (this.base.options.anchorButton) {
1096
+ button = doc.createElement('input');
1097
+ button.setAttribute('type', 'checkbox');
1098
+ button.className = 'medium-editor-toolbar-anchor-button';
1099
+
1100
+ button_label = doc.createElement('label');
1101
+ button_label.innerHTML = "Button";
1102
+ button_label.insertBefore(button, button_label.firstChild);
1103
+
1104
+ form.appendChild(button_label);
475
1105
  }
476
- }
477
- return html;
478
- }
479
1106
 
480
- /**
481
- * Find the caret position within an element irrespective of any inline tags it may contain.
482
- *
483
- * @param {DOMElement} An element containing the cursor to find offsets relative to.
484
- * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
485
- * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
486
- */
487
- function getCaretOffsets(element, range) {
488
- var preCaretRange, postCaretRange;
489
-
490
- if (!range) {
491
- range = window.getSelection().getRangeAt(0);
492
- }
1107
+ // Handle click (capture) & focus (capture) outside of the form
1108
+ this.base.on(doc.body, 'click', this.handleOutsideInteraction.bind(this), true);
1109
+ this.base.on(doc.body, 'focus', this.handleOutsideInteraction.bind(this), true);
1110
+
1111
+ return form;
1112
+ },
493
1113
 
494
- preCaretRange = range.cloneRange();
495
- postCaretRange = range.cloneRange();
1114
+ focus: function (value) {
1115
+ var input = this.getInput();
1116
+ input.focus();
1117
+ input.value = value || '';
1118
+ },
496
1119
 
497
- preCaretRange.selectNodeContents(element);
498
- preCaretRange.setEnd(range.endContainer, range.endOffset);
1120
+ hideForm: function () {
1121
+ this.getForm().style.display = 'none';
1122
+ },
499
1123
 
500
- postCaretRange.selectNodeContents(element);
501
- postCaretRange.setStart(range.endContainer, range.endOffset);
1124
+ showForm: function () {
1125
+ this.getForm().style.display = 'block';
1126
+ },
502
1127
 
503
- return {
504
- left: preCaretRange.toString().length,
505
- right: postCaretRange.toString().length
506
- };
507
- }
1128
+ isDisplayed: function () {
1129
+ return this.getForm().style.display === 'block';
1130
+ },
508
1131
 
1132
+ isClickIntoForm: function (event) {
1133
+ return (event &&
1134
+ event.type &&
1135
+ event.type.toLowerCase() === 'blur' &&
1136
+ event.relatedTarget &&
1137
+ event.relatedTarget === this.getInput());
1138
+ }
1139
+ };
1140
+ }(window, document));
1141
+ function MediumEditor(elements, options) {
1142
+ 'use strict';
1143
+ return this.init(elements, options);
1144
+ }
509
1145
 
510
- // https://github.com/jashkenas/underscore
511
- function isElement(obj) {
512
- return !!(obj && obj.nodeType === 1);
513
- }
1146
+ (function () {
1147
+ 'use strict';
514
1148
 
515
1149
  MediumEditor.statics = {
516
1150
  ButtonsData: ButtonsData,
517
- DefaultButton: DefaultButton
1151
+ DefaultButton: DefaultButton,
1152
+ AnchorExtension: AnchorExtension
518
1153
  };
519
1154
 
520
1155
  MediumEditor.prototype = {
@@ -555,19 +1190,15 @@ if (typeof module === 'object') {
555
1190
  lastButtonClass: 'medium-editor-button-last'
556
1191
  },
557
1192
 
558
- // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
559
- // by rg89
560
- isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
561
-
562
1193
  init: function (elements, options) {
563
1194
  var uniqueId = 1;
564
1195
 
565
- this.options = extend(options, this.defaults);
1196
+ this.options = mediumEditorUtil.extend(options, this.defaults);
566
1197
  this.setElementSelection(elements);
567
1198
  if (this.elements.length === 0) {
568
1199
  return;
569
1200
  }
570
- this.parentElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'];
1201
+
571
1202
  if (!this.options.elementsContainer) {
572
1203
  this.options.elementsContainer = this.options.ownerDocument.body;
573
1204
  }
@@ -643,7 +1274,7 @@ if (typeof module === 'object') {
643
1274
  // handleResize is throttled because:
644
1275
  // - It will be called when the browser is resizing, which can fire many times very quickly
645
1276
  // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
646
- this.handleResize = throttle(function () {
1277
+ this.handleResize = mediumEditorUtil.throttle(function () {
647
1278
  if (self.isActive) {
648
1279
  self.positionToolbarIfShown();
649
1280
  }
@@ -653,7 +1284,7 @@ if (typeof module === 'object') {
653
1284
  // - This method could be called many times due to the type of event handlers that are calling it
654
1285
  // - We want a slight delay so that other events in the stack can run, some of which may
655
1286
  // prevent the toolbar from being hidden (via this.keepToolbarAlive).
656
- this.handleBlur = throttle(function () {
1287
+ this.handleBlur = mediumEditorUtil.throttle(function () {
657
1288
  if (self.isActive && !self.keepToolbarAlive) {
658
1289
  self.hideToolbarActions();
659
1290
  }
@@ -684,7 +1315,6 @@ if (typeof module === 'object') {
684
1315
  if (addToolbar) {
685
1316
  this.initToolbar()
686
1317
  .bindButtons()
687
- .bindAnchorForm()
688
1318
  .bindAnchorPreview();
689
1319
  }
690
1320
  return this;
@@ -699,7 +1329,7 @@ if (typeof module === 'object') {
699
1329
  selector = this.options.ownerDocument.querySelectorAll(selector);
700
1330
  }
701
1331
  // If element, put into array
702
- if (isElement(selector)) {
1332
+ if (mediumEditorUtil.isElement(selector)) {
703
1333
  selector = [selector];
704
1334
  }
705
1335
  // Convert NodeList (or other array like object) into an array
@@ -712,7 +1342,7 @@ if (typeof module === 'object') {
712
1342
  var isDescendantOfEditorElements = false,
713
1343
  i;
714
1344
  for (i = 0; i < self.elements.length; i += 1) {
715
- if (isDescendant(self.elements[i], e.target)) {
1345
+ if (mediumEditorUtil.isDescendant(self.elements[i], e.target)) {
716
1346
  isDescendantOfEditorElements = true;
717
1347
  break;
718
1348
  }
@@ -721,8 +1351,8 @@ if (typeof module === 'object') {
721
1351
  if (e.target !== self.toolbar
722
1352
  && self.elements.indexOf(e.target) === -1
723
1353
  && !isDescendantOfEditorElements
724
- && !isDescendant(self.toolbar, e.target)
725
- && !isDescendant(self.anchorPreview, e.target)) {
1354
+ && !mediumEditorUtil.isDescendant(self.toolbar, e.target)
1355
+ && !mediumEditorUtil.isDescendant(self.anchorPreview, e.target)) {
726
1356
 
727
1357
  // Activate the placeholder
728
1358
  if (!self.options.disablePlaceholders) {
@@ -882,8 +1512,8 @@ if (typeof module === 'object') {
882
1512
  this.on(this.elements[index], 'keypress', function (e) {
883
1513
  var node,
884
1514
  tagName;
885
- if (e.which === keyCode.SPACE) {
886
- node = getSelectionStart.call(self);
1515
+ if (e.which === mediumEditorUtil.keyCode.SPACE) {
1516
+ node = meSelection.getSelectionStart(self.options.ownerDocument);
887
1517
  tagName = node.tagName.toLowerCase();
888
1518
  if (tagName === 'a') {
889
1519
  self.options.ownerDocument.execCommand('unlink', false, null);
@@ -892,20 +1522,20 @@ if (typeof module === 'object') {
892
1522
  });
893
1523
 
894
1524
  this.on(this.elements[index], 'keyup', function (e) {
895
- var node = getSelectionStart.call(self),
1525
+ var node = meSelection.getSelectionStart(self.options.ownerDocument),
896
1526
  tagName,
897
1527
  editorElement;
898
1528
 
899
1529
  if (node && node.getAttribute('data-medium-element') && node.children.length === 0 && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
900
1530
  self.options.ownerDocument.execCommand('formatBlock', false, 'p');
901
1531
  }
902
- if (e.which === keyCode.ENTER) {
903
- node = getSelectionStart.call(self);
1532
+ if (e.which === mediumEditorUtil.keyCode.ENTER) {
1533
+ node = meSelection.getSelectionStart(self.options.ownerDocument);
904
1534
  tagName = node.tagName.toLowerCase();
905
- editorElement = self.getSelectionElement();
1535
+ editorElement = meSelection.getSelectionElement(self.options.contentWindow);
906
1536
 
907
1537
  if (!(self.options.disableReturn || editorElement.getAttribute('data-disable-return')) &&
908
- tagName !== 'li' && !self.isListItemChild(node)) {
1538
+ tagName !== 'li' && !mediumEditorUtil.isListItemChild(node)) {
909
1539
  if (!e.shiftKey) {
910
1540
 
911
1541
  // paragraph creation should not be forced within a header tag
@@ -922,32 +1552,15 @@ if (typeof module === 'object') {
922
1552
  return this;
923
1553
  },
924
1554
 
925
- isListItemChild: function (node) {
926
- var parentNode = node.parentNode,
927
- tagName = parentNode.tagName.toLowerCase();
928
- while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
929
- if (tagName === 'li') {
930
- return true;
931
- }
932
- parentNode = parentNode.parentNode;
933
- if (parentNode && parentNode.tagName) {
934
- tagName = parentNode.tagName.toLowerCase();
935
- } else {
936
- return false;
937
- }
938
- }
939
- return false;
940
- },
941
-
942
1555
  bindReturn: function (index) {
943
1556
  var self = this;
944
1557
  this.on(this.elements[index], 'keypress', function (e) {
945
- if (e.which === keyCode.ENTER) {
1558
+ if (e.which === mediumEditorUtil.keyCode.ENTER) {
946
1559
  if (self.options.disableReturn || this.getAttribute('data-disable-return')) {
947
1560
  e.preventDefault();
948
1561
  } else if (self.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
949
- var node = getSelectionStart.call(self);
950
- if (node && node.textContent === '\n') {
1562
+ var node = meSelection.getSelectionStart(self.options.contentWindow);
1563
+ if (node && node.textContent.trim() === '') {
951
1564
  e.preventDefault();
952
1565
  }
953
1566
  }
@@ -960,9 +1573,9 @@ if (typeof module === 'object') {
960
1573
  var self = this;
961
1574
  this.on(this.elements[index], 'keydown', function (e) {
962
1575
 
963
- if (e.which === keyCode.TAB) {
1576
+ if (e.which === mediumEditorUtil.keyCode.TAB) {
964
1577
  // Override tab only for pre nodes
965
- var node = getSelectionStart.call(self) || e.target,
1578
+ var node = meSelection.getSelectionStart(self.options.ownerDocument),
966
1579
  tag = node && node.tagName.toLowerCase();
967
1580
 
968
1581
  if (tag === 'pre') {
@@ -971,7 +1584,7 @@ if (typeof module === 'object') {
971
1584
  }
972
1585
 
973
1586
  // Tab to indent list structures!
974
- if (tag === 'li' || self.isListItemChild(node)) {
1587
+ if (tag === 'li' || mediumEditorUtil.isListItemChild(node)) {
975
1588
  e.preventDefault();
976
1589
 
977
1590
  // If Shift is down, outdent, otherwise indent
@@ -981,7 +1594,7 @@ if (typeof module === 'object') {
981
1594
  self.options.ownerDocument.execCommand('indent', e);
982
1595
  }
983
1596
  }
984
- } else if (e.which === keyCode.BACKSPACE || e.which === keyCode.DELETE || e.which === keyCode.ENTER) {
1597
+ } else if (e.which === mediumEditorUtil.keyCode.BACKSPACE || e.which === mediumEditorUtil.keyCode.DELETE || e.which === mediumEditorUtil.keyCode.ENTER) {
985
1598
 
986
1599
  // Bind keys which can create or destroy a block element: backspace, delete, return
987
1600
  self.onBlockModifier(e);
@@ -992,24 +1605,24 @@ if (typeof module === 'object') {
992
1605
  },
993
1606
 
994
1607
  onBlockModifier: function (e) {
995
- var range, sel, p, node = getSelectionStart.call(this),
1608
+ var range, sel, p, node = meSelection.getSelectionStart(this.options.ownerDocument),
996
1609
  tagName = node.tagName.toLowerCase(),
997
1610
  isEmpty = /^(\s+|<br\/?>)?$/i,
998
1611
  isHeader = /h\d/i;
999
1612
 
1000
- if ((e.which === keyCode.BACKSPACE || e.which === keyCode.ENTER)
1613
+ if ((e.which === mediumEditorUtil.keyCode.BACKSPACE || e.which === mediumEditorUtil.keyCode.ENTER)
1001
1614
  && node.previousElementSibling
1002
1615
  // in a header
1003
1616
  && isHeader.test(tagName)
1004
1617
  // at the very end of the block
1005
- && getCaretOffsets(node).left === 0) {
1006
- if (e.which === keyCode.BACKSPACE && isEmpty.test(node.previousElementSibling.innerHTML)) {
1618
+ && meSelection.getCaretOffsets(node).left === 0) {
1619
+ if (e.which === mediumEditorUtil.keyCode.BACKSPACE && isEmpty.test(node.previousElementSibling.innerHTML)) {
1007
1620
  // backspacing the begining of a header into an empty previous element will
1008
1621
  // change the tagName of the current node to prevent one
1009
1622
  // instead delete previous node and cancel the event.
1010
1623
  node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
1011
1624
  e.preventDefault();
1012
- } else if (e.which === keyCode.ENTER) {
1625
+ } else if (e.which === mediumEditorUtil.keyCode.ENTER) {
1013
1626
  // hitting return in the begining of a header will create empty header elements before the current one
1014
1627
  // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
1015
1628
  p = this.options.ownerDocument.createElement('p');
@@ -1017,7 +1630,7 @@ if (typeof module === 'object') {
1017
1630
  node.previousElementSibling.parentNode.insertBefore(p, node);
1018
1631
  e.preventDefault();
1019
1632
  }
1020
- } else if (e.which === keyCode.DELETE
1633
+ } else if (e.which === mediumEditorUtil.keyCode.DELETE
1021
1634
  && node.nextElementSibling
1022
1635
  && node.previousElementSibling
1023
1636
  // not in a header
@@ -1056,13 +1669,6 @@ if (typeof module === 'object') {
1056
1669
  this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions');
1057
1670
  this.anchorPreview = this.createAnchorPreview();
1058
1671
 
1059
- if (!this.options.disableAnchorForm) {
1060
- this.anchorForm = this.toolbar.querySelector('.medium-editor-toolbar-form');
1061
- this.anchorInput = this.anchorForm.querySelector('input.medium-editor-toolbar-input');
1062
- this.anchorTarget = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-target');
1063
- this.anchorButton = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-button');
1064
- }
1065
-
1066
1672
  this.addExtensionForms();
1067
1673
 
1068
1674
  return this;
@@ -1081,7 +1687,8 @@ if (typeof module === 'object') {
1081
1687
 
1082
1688
  toolbar.appendChild(this.toolbarButtons());
1083
1689
  if (!this.options.disableAnchorForm) {
1084
- toolbar.appendChild(this.toolbarFormAnchor());
1690
+ this.anchorExtension = new AnchorExtension(this);
1691
+ toolbar.appendChild(this.anchorExtension.getForm());
1085
1692
  }
1086
1693
  this.options.elementsContainer.appendChild(toolbar);
1087
1694
  return toolbar;
@@ -1100,7 +1707,7 @@ if (typeof module === 'object') {
1100
1707
  if (typeof extension.getButton === 'function') {
1101
1708
  btn = extension.getButton(this);
1102
1709
  li = this.options.ownerDocument.createElement('li');
1103
- if (isElement(btn)) {
1710
+ if (mediumEditorUtil.isElement(btn)) {
1104
1711
  li.appendChild(btn);
1105
1712
  } else {
1106
1713
  li.innerHTML = btn;
@@ -1129,84 +1736,32 @@ if (typeof module === 'object') {
1129
1736
  }.bind(this));
1130
1737
  },
1131
1738
 
1132
- toolbarFormAnchor: function () {
1133
- var anchor = this.options.ownerDocument.createElement('div'),
1134
- input = this.options.ownerDocument.createElement('input'),
1135
- target_label = this.options.ownerDocument.createElement('label'),
1136
- target = this.options.ownerDocument.createElement('input'),
1137
- button_label = this.options.ownerDocument.createElement('label'),
1138
- button = this.options.ownerDocument.createElement('input'),
1139
- close = this.options.ownerDocument.createElement('a'),
1140
- save = this.options.ownerDocument.createElement('a');
1141
-
1142
- close.setAttribute('href', '#');
1143
- close.className = 'medium-editor-toobar-close';
1144
- close.innerHTML = '&times;';
1145
-
1146
- save.setAttribute('href', '#');
1147
- save.className = 'medium-editor-toobar-save';
1148
- save.innerHTML = '&#10003;';
1149
-
1150
- input.setAttribute('type', 'text');
1151
- input.className = 'medium-editor-toolbar-input';
1152
- input.setAttribute('placeholder', this.options.anchorInputPlaceholder);
1153
-
1154
-
1155
- target.setAttribute('type', 'checkbox');
1156
- target.className = 'medium-editor-toolbar-anchor-target';
1157
- target_label.innerHTML = this.options.anchorInputCheckboxLabel;
1158
- target_label.insertBefore(target, target_label.firstChild);
1159
-
1160
- button.setAttribute('type', 'checkbox');
1161
- button.className = 'medium-editor-toolbar-anchor-button';
1162
- button_label.innerHTML = "Button";
1163
- button_label.insertBefore(button, button_label.firstChild);
1164
-
1165
-
1166
- anchor.className = 'medium-editor-toolbar-form';
1167
- anchor.id = 'medium-editor-toolbar-form-anchor-' + this.id;
1168
- anchor.appendChild(input);
1169
-
1170
- anchor.appendChild(save);
1171
- anchor.appendChild(close);
1172
-
1173
- if (this.options.anchorTarget) {
1174
- anchor.appendChild(target_label);
1175
- }
1176
-
1177
- if (this.options.anchorButton) {
1178
- anchor.appendChild(button_label);
1179
- }
1180
-
1181
- return anchor;
1182
- },
1183
-
1184
1739
  bindSelect: function () {
1185
1740
  var self = this,
1186
1741
  i,
1187
- timer;
1742
+ timeoutHelper;
1188
1743
 
1189
1744
  this.checkSelectionWrapper = function (e) {
1190
- e.stopPropagation();
1191
-
1192
- clearTimeout(timer);
1193
-
1194
1745
  // Do not close the toolbar when bluring the editable area and clicking into the anchor form
1195
- if (!self.options.disableAnchorForm && e && self.clickingIntoArchorForm(e)) {
1746
+ if (e && this.anchorExtension && this.anchorExtension.isClickIntoForm(e)) {
1196
1747
  return false;
1197
1748
  }
1198
1749
 
1199
- timer = setTimeout(function () {
1200
- self.checkSelection();
1201
- }, 10);
1750
+ self.checkSelection();
1202
1751
  };
1203
1752
 
1753
+ timeoutHelper = function (event) {
1754
+ setTimeout(function () {
1755
+ this.checkSelectionWrapper(event);
1756
+ }.bind(this), 0);
1757
+ }.bind(this);
1758
+
1204
1759
  this.on(this.options.ownerDocument.documentElement, 'mouseup', this.checkSelectionWrapper);
1205
1760
 
1206
1761
  for (i = 0; i < this.elements.length; i += 1) {
1207
1762
  this.on(this.elements[i], 'keyup', this.checkSelectionWrapper);
1208
1763
  this.on(this.elements[i], 'blur', this.checkSelectionWrapper);
1209
- this.on(this.elements[i], 'mouseup', this.checkSelectionWrapper);
1764
+ this.on(this.elements[i], 'click', timeoutHelper);
1210
1765
  }
1211
1766
 
1212
1767
  return this;
@@ -1279,7 +1834,7 @@ if (typeof module === 'object') {
1279
1834
  fileReader.readAsDataURL(file);
1280
1835
 
1281
1836
  id = 'medium-img-' + (+new Date());
1282
- self.insertHTML('<img class="medium-image-loading" id="' + id + '" />');
1837
+ mediumEditorUtil.insertHTMLCommand(self.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
1283
1838
 
1284
1839
  fileReader.onload = function () {
1285
1840
  var img = document.getElementById(id);
@@ -1314,7 +1869,6 @@ if (typeof module === 'object') {
1314
1869
  },
1315
1870
 
1316
1871
  checkSelection: function () {
1317
-
1318
1872
  var newSelection,
1319
1873
  selectionElement;
1320
1874
 
@@ -1326,17 +1880,17 @@ if (typeof module === 'object') {
1326
1880
 
1327
1881
  if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
1328
1882
  (this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs()) ||
1329
- this.selectionInContentEditableFalse()) {
1883
+ meSelection.selectionInContentEditableFalse(this.options.contentWindow)) {
1330
1884
 
1331
1885
  if (!this.options.staticToolbar) {
1332
1886
  this.hideToolbarActions();
1333
- } else if (this.anchorForm && this.anchorForm.style.display === 'block') {
1887
+ } else if (this.anchorExtension && this.anchorExtension.isDisplayed()) {
1334
1888
  this.setToolbarButtonStates();
1335
1889
  this.showToolbarActions();
1336
1890
  }
1337
1891
 
1338
1892
  } else {
1339
- selectionElement = this.getSelectionElement();
1893
+ selectionElement = meSelection.getSelectionElement(this.options.contentWindow);
1340
1894
  if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
1341
1895
  if (!this.options.staticToolbar) {
1342
1896
  this.hideToolbarActions();
@@ -1349,18 +1903,8 @@ if (typeof module === 'object') {
1349
1903
  return this;
1350
1904
  },
1351
1905
 
1352
- clickingIntoArchorForm: function (e) {
1353
- var self = this;
1354
-
1355
- if (e.type && e.type.toLowerCase() === 'blur' && e.relatedTarget && e.relatedTarget === self.anchorInput) {
1356
- return true;
1357
- }
1358
-
1359
- return false;
1360
- },
1361
-
1362
1906
  hasMultiParagraphs: function () {
1363
- var selectionHtml = getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
1907
+ var selectionHtml = meSelection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
1364
1908
  hasMultiParagraphs = selectionHtml.match(/<(p|h[0-6]|blockquote)>([\s\S]*?)<\/(p|h[0-6]|blockquote)>/g);
1365
1909
 
1366
1910
  return (hasMultiParagraphs ? hasMultiParagraphs.length : 0);
@@ -1393,7 +1937,7 @@ if (typeof module === 'object') {
1393
1937
  if (this.options.standardizeSelectionStart &&
1394
1938
  this.selectionRange.startContainer.nodeValue &&
1395
1939
  (this.selectionRange.startOffset === this.selectionRange.startContainer.nodeValue.length)) {
1396
- adjacentNode = findAdjacentTextNodeWithContent(this.getSelectionElement(), this.selectionRange.startContainer, this.options.ownerDocument);
1940
+ adjacentNode = mediumEditorUtil.findAdjacentTextNodeWithContent(meSelection.getSelectionElement(this.options.contentWindow), this.selectionRange.startContainer, this.options.ownerDocument);
1397
1941
  if (adjacentNode) {
1398
1942
  offset = 0;
1399
1943
  while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
@@ -1422,52 +1966,6 @@ if (typeof module === 'object') {
1422
1966
  }
1423
1967
  },
1424
1968
 
1425
- traverseUp: function (current, testElementFunction) {
1426
-
1427
- do {
1428
- if (current.nodeType === 1) {
1429
- if (testElementFunction(current)) {
1430
- return current;
1431
- }
1432
- // do not traverse upwards past the nearest containing editor
1433
- if (current.getAttribute('data-medium-element')) {
1434
- return false;
1435
- }
1436
- }
1437
-
1438
- current = current.parentNode;
1439
- } while (current);
1440
-
1441
- return false;
1442
-
1443
- },
1444
-
1445
- findMatchingSelectionParent: function (testElementFunction) {
1446
- var selection = this.options.contentWindow.getSelection(), range, current;
1447
-
1448
- if (selection.rangeCount === 0) {
1449
- return false;
1450
- }
1451
-
1452
- range = selection.getRangeAt(0);
1453
- current = range.commonAncestorContainer;
1454
-
1455
- return this.traverseUp(current, testElementFunction);
1456
-
1457
- },
1458
-
1459
- getSelectionElement: function () {
1460
- return this.findMatchingSelectionParent(function (el) {
1461
- return el.getAttribute('data-medium-element');
1462
- });
1463
- },
1464
-
1465
- selectionInContentEditableFalse: function () {
1466
- return this.findMatchingSelectionParent(function (el) {
1467
- return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
1468
- });
1469
- },
1470
-
1471
1969
  setToolbarPosition: function () {
1472
1970
  // document.documentElement for IE 9
1473
1971
  var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
@@ -1563,7 +2061,9 @@ if (typeof module === 'object') {
1563
2061
 
1564
2062
  checkActiveButtons: function () {
1565
2063
  var elements = Array.prototype.slice.call(this.elements),
1566
- parentNode = this.getSelectedParentElement(),
2064
+ manualStateChecks = [],
2065
+ queryState = null,
2066
+ parentNode = meSelection.getSelectedParentElement(this.selectionRange),
1567
2067
  checkExtension = function (extension) {
1568
2068
  if (typeof extension.checkState === 'function') {
1569
2069
  extension.checkState(parentNode);
@@ -1573,9 +2073,29 @@ if (typeof module === 'object') {
1573
2073
  }
1574
2074
  }
1575
2075
  };
1576
- while (parentNode.tagName !== undefined && this.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
2076
+
2077
+ // Loop through all commands
2078
+ this.commands.forEach(function (command) {
2079
+ // For those commands where we can use document.queryCommandState(), do so
2080
+ if (typeof command.queryCommandState === 'function') {
2081
+ queryState = command.queryCommandState();
2082
+ // If queryCommandState returns a valid value, we can trust the browser
2083
+ // and don't need to do our manual checks
2084
+ if (queryState !== null) {
2085
+ if (queryState) {
2086
+ command.activate();
2087
+ }
2088
+ return;
2089
+ }
2090
+ }
2091
+ // We can't use queryCommandState for this command, so add to manualStateChecks
2092
+ manualStateChecks.push(command);
2093
+ });
2094
+
2095
+ // Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node
2096
+ while (parentNode.tagName !== undefined && mediumEditorUtil.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
1577
2097
  this.activateButton(parentNode.tagName.toLowerCase());
1578
- this.commands.forEach(checkExtension.bind(this));
2098
+ manualStateChecks.forEach(checkExtension.bind(this));
1579
2099
 
1580
2100
  // we can abort the search upwards if we leave the contentEditable element
1581
2101
  if (elements.indexOf(parentNode) !== -1) {
@@ -1644,9 +2164,10 @@ if (typeof module === 'object') {
1644
2164
  el.style.display = 'none';
1645
2165
  this.showToolbarActions();
1646
2166
  this.setToolbarPosition();
1647
- restoreSelection.call(this, this.savedSelection);
2167
+ this.restoreSelection();
1648
2168
  },
1649
2169
 
2170
+ // TODO: move these two methods to selection.js
1650
2171
  // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
1651
2172
  rangeSelectsSingleNode: function (range) {
1652
2173
  var startNode = range.startContainer;
@@ -1669,12 +2190,12 @@ if (typeof module === 'object') {
1669
2190
  },
1670
2191
 
1671
2192
  triggerAnchorAction: function () {
1672
- var selectedParentElement = this.getSelectedParentElement();
2193
+ var selectedParentElement = meSelection.getSelectedParentElement(this.selectionRange);
1673
2194
  if (selectedParentElement.tagName &&
1674
2195
  selectedParentElement.tagName.toLowerCase() === 'a') {
1675
2196
  this.options.ownerDocument.execCommand('unlink', false, null);
1676
- } else if (this.anchorForm) {
1677
- if (this.anchorForm.style.display === 'block') {
2197
+ } else if (this.anchorExtension) {
2198
+ if (this.anchorExtension.isDisplayed()) {
1678
2199
  this.showToolbarActions();
1679
2200
  } else {
1680
2201
  this.showAnchorForm();
@@ -1684,7 +2205,7 @@ if (typeof module === 'object') {
1684
2205
  },
1685
2206
 
1686
2207
  execFormatBlock: function (el) {
1687
- var selectionData = this.getSelectionData(this.selection.anchorNode);
2208
+ var selectionData = meSelection.getSelectionData(this.selection.anchorNode);
1688
2209
  // FF handles blockquote differently on formatBlock
1689
2210
  // allowing nesting, we need to use outdent
1690
2211
  // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
@@ -1699,7 +2220,7 @@ if (typeof module === 'object') {
1699
2220
  // blockquote needs to be called as indent
1700
2221
  // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
1701
2222
  // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
1702
- if (this.isIE) {
2223
+ if (mediumEditorUtil.isIE) {
1703
2224
  if (el === 'blockquote') {
1704
2225
  return this.options.ownerDocument.execCommand('indent', false, el);
1705
2226
  }
@@ -1708,34 +2229,6 @@ if (typeof module === 'object') {
1708
2229
  return this.options.ownerDocument.execCommand('formatBlock', false, el);
1709
2230
  },
1710
2231
 
1711
- getSelectionData: function (el) {
1712
- var tagName;
1713
-
1714
- if (el && el.tagName) {
1715
- tagName = el.tagName.toLowerCase();
1716
- }
1717
-
1718
- while (el && this.parentElements.indexOf(tagName) === -1) {
1719
- el = el.parentNode;
1720
- if (el && el.tagName) {
1721
- tagName = el.tagName.toLowerCase();
1722
- }
1723
- }
1724
-
1725
- return {
1726
- el: el,
1727
- tagName: tagName
1728
- };
1729
- },
1730
-
1731
- getFirstChild: function (el) {
1732
- var firstChild = el.firstChild;
1733
- while (firstChild !== null && firstChild.nodeType !== 1) {
1734
- firstChild = firstChild.nextSibling;
1735
- }
1736
- return firstChild;
1737
- },
1738
-
1739
2232
  isToolbarShown: function () {
1740
2233
  return this.toolbar && this.toolbar.classList.contains('medium-editor-toolbar-active');
1741
2234
  },
@@ -1769,123 +2262,121 @@ if (typeof module === 'object') {
1769
2262
  this.hideToolbar();
1770
2263
  },
1771
2264
 
1772
- showToolbarActions: function () {
1773
- var self = this;
1774
- if (this.anchorForm) {
1775
- this.anchorForm.style.display = 'none';
1776
- }
1777
- this.toolbarActions.style.display = 'block';
1778
- this.keepToolbarAlive = false;
1779
- // Using setTimeout + options.delay because:
1780
- // We will actually be displaying the toolbar, which should be controlled by options.delay
1781
- this.delay(function () {
1782
- self.showToolbar();
1783
- });
1784
- },
1785
-
1786
- saveSelection: function () {
1787
- this.savedSelection = saveSelection.call(this);
1788
- },
1789
-
1790
- restoreSelection: function () {
1791
- restoreSelection.call(this, this.savedSelection);
1792
- },
1793
-
1794
- showAnchorForm: function (link_value) {
1795
- if (!this.anchorForm) {
1796
- return;
1797
- }
1798
-
1799
- this.toolbarActions.style.display = 'none';
1800
- this.saveSelection();
1801
- this.anchorForm.style.display = 'block';
1802
- this.setToolbarPosition();
1803
- this.keepToolbarAlive = true;
1804
- this.anchorInput.focus();
1805
- this.anchorInput.value = link_value || '';
1806
- },
1807
-
1808
- bindAnchorForm: function () {
1809
- if (!this.anchorForm) {
1810
- return this;
2265
+ showToolbarActions: function () {
2266
+ var self = this;
2267
+ if (this.anchorExtension) {
2268
+ this.anchorExtension.hideForm();
1811
2269
  }
2270
+ this.toolbarActions.style.display = 'block';
2271
+ this.keepToolbarAlive = false;
2272
+ // Using setTimeout + options.delay because:
2273
+ // We will actually be displaying the toolbar, which should be controlled by options.delay
2274
+ this.delay(function () {
2275
+ self.showToolbar();
2276
+ });
2277
+ },
1812
2278
 
1813
- var linkCancel = this.anchorForm.querySelector('a.medium-editor-toobar-close'),
1814
- linkSave = this.anchorForm.querySelector('a.medium-editor-toobar-save'),
1815
- self = this;
2279
+ // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
2280
+ // Tim Down
2281
+ // TODO: move to selection.js and clean up old methods there
2282
+ saveSelection: function () {
2283
+ this.selectionState = null;
1816
2284
 
1817
- this.on(this.anchorForm, 'click', function (e) {
1818
- e.stopPropagation();
1819
- self.keepToolbarAlive = true;
1820
- });
2285
+ var selection = this.options.contentWindow.getSelection(),
2286
+ range,
2287
+ preSelectionRange,
2288
+ start,
2289
+ editableElementIndex = -1;
1821
2290
 
1822
- this.on(this.anchorInput, 'keyup', function (e) {
1823
- var button = null,
1824
- target;
2291
+ if (selection.rangeCount > 0) {
2292
+ range = selection.getRangeAt(0);
2293
+ preSelectionRange = range.cloneRange();
1825
2294
 
1826
- if (e.keyCode === keyCode.ENTER) {
1827
- e.preventDefault();
1828
- if (self.options.anchorTarget && self.anchorTarget.checked) {
1829
- target = "_blank";
1830
- } else {
1831
- target = "_self";
2295
+ // Find element current selection is inside
2296
+ this.elements.forEach(function (el, index) {
2297
+ if (el === range.startContainer || mediumEditorUtil.isDescendant(el, range.startContainer)) {
2298
+ editableElementIndex = index;
2299
+ return false;
1832
2300
  }
2301
+ });
1833
2302
 
1834
- if (self.options.anchorButton && self.anchorButton.checked) {
1835
- button = self.options.anchorButtonClass;
1836
- }
2303
+ if (editableElementIndex > -1) {
2304
+ preSelectionRange.selectNodeContents(this.elements[editableElementIndex]);
2305
+ preSelectionRange.setEnd(range.startContainer, range.startOffset);
2306
+ start = preSelectionRange.toString().length;
1837
2307
 
1838
- self.createLink(this, target, button);
1839
- } else if (e.keyCode === keyCode.ESCAPE) {
1840
- e.preventDefault();
1841
- self.showToolbarActions();
1842
- restoreSelection.call(self, self.savedSelection);
2308
+ this.selectionState = {
2309
+ start: start,
2310
+ end: start + range.toString().length,
2311
+ editableElementIndex: editableElementIndex
2312
+ };
1843
2313
  }
1844
- });
2314
+ }
2315
+ },
1845
2316
 
1846
- this.on(linkSave, 'click', function (e) {
1847
- var button = null,
1848
- target;
1849
- e.preventDefault();
1850
- if (self.options.anchorTarget && self.anchorTarget.checked) {
1851
- target = "_blank";
2317
+ // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
2318
+ // Tim Down
2319
+ // TODO: move to selection.js and clean up old methods there
2320
+ restoreSelection: function () {
2321
+ if (!this.selectionState) {
2322
+ return;
2323
+ }
2324
+
2325
+ var editableElement = this.elements[this.selectionState.editableElementIndex],
2326
+ charIndex = 0,
2327
+ range = this.options.ownerDocument.createRange(),
2328
+ nodeStack = [editableElement],
2329
+ node,
2330
+ foundStart = false,
2331
+ stop = false,
2332
+ i,
2333
+ sel,
2334
+ nextCharIndex;
2335
+
2336
+ range.setStart(editableElement, 0);
2337
+ range.collapse(true);
2338
+
2339
+ node = nodeStack.pop();
2340
+ while (!stop && node) {
2341
+ if (node.nodeType === 3) {
2342
+ nextCharIndex = charIndex + node.length;
2343
+ if (!foundStart && this.selectionState.start >= charIndex && this.selectionState.start <= nextCharIndex) {
2344
+ range.setStart(node, this.selectionState.start - charIndex);
2345
+ foundStart = true;
2346
+ }
2347
+ if (foundStart && this.selectionState.end >= charIndex && this.selectionState.end <= nextCharIndex) {
2348
+ range.setEnd(node, this.selectionState.end - charIndex);
2349
+ stop = true;
2350
+ }
2351
+ charIndex = nextCharIndex;
1852
2352
  } else {
1853
- target = "_self";
2353
+ i = node.childNodes.length - 1;
2354
+ while (i >= 0) {
2355
+ nodeStack.push(node.childNodes[i]);
2356
+ i -= 1;
2357
+ }
1854
2358
  }
1855
-
1856
- if (self.options.anchorButton && self.anchorButton.checked) {
1857
- button = self.options.anchorButtonClass;
2359
+ if (!stop) {
2360
+ node = nodeStack.pop();
1858
2361
  }
2362
+ }
1859
2363
 
1860
- self.createLink(self.anchorInput, target, button);
1861
- }, true);
1862
-
1863
- this.on(this.anchorInput, 'click', function (e) {
1864
- // make sure not to hide form when cliking into the input
1865
- e.stopPropagation();
1866
- self.keepToolbarAlive = true;
1867
- });
2364
+ sel = this.options.contentWindow.getSelection();
2365
+ sel.removeAllRanges();
2366
+ sel.addRange(range);
2367
+ },
1868
2368
 
1869
- // Hide the anchor form when focusing outside of it.
1870
- this.on(this.options.ownerDocument.body, 'click', function (e) {
1871
- if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
1872
- self.keepToolbarAlive = false;
1873
- self.checkSelection();
1874
- }
1875
- }, true);
1876
- this.on(this.options.ownerDocument.body, 'focus', function (e) {
1877
- if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
1878
- self.keepToolbarAlive = false;
1879
- self.checkSelection();
1880
- }
1881
- }, true);
2369
+ showAnchorForm: function (link_value) {
2370
+ if (!this.anchorExtension) {
2371
+ return;
2372
+ }
1882
2373
 
1883
- this.on(linkCancel, 'click', function (e) {
1884
- e.preventDefault();
1885
- self.showToolbarActions();
1886
- restoreSelection.call(self, self.savedSelection);
1887
- });
1888
- return this;
2374
+ this.toolbarActions.style.display = 'none';
2375
+ this.saveSelection();
2376
+ this.anchorExtension.showForm();
2377
+ this.setToolbarPosition();
2378
+ this.keepToolbarAlive = true;
2379
+ this.anchorExtension.focus(link_value);
1889
2380
  },
1890
2381
 
1891
2382
  hideAnchorPreview: function () {
@@ -2002,7 +2493,7 @@ if (typeof module === 'object') {
2002
2493
  sel.removeAllRanges();
2003
2494
  sel.addRange(range);
2004
2495
  // Using setTimeout + options.delay because:
2005
- // We may actually be displaying the anchor preview, which should be controlled by options.delay
2496
+ // We may actually be displaying the anchor form, which should be controlled by options.delay
2006
2497
  this.delay(function () {
2007
2498
  if (self.activeAnchor) {
2008
2499
  self.showAnchorForm(self.activeAnchor.attributes.href.value);
@@ -2067,22 +2558,8 @@ if (typeof module === 'object') {
2067
2558
  return (re.test(value) ? '' : 'http://') + value;
2068
2559
  },
2069
2560
 
2070
- setTargetBlank: function (el) {
2071
- var i;
2072
- el = el || getSelectionStart.call(this);
2073
- if (el.tagName.toLowerCase() === 'a') {
2074
- el.target = '_blank';
2075
- } else {
2076
- el = el.getElementsByTagName('a');
2077
-
2078
- for (i = 0; i < el.length; i += 1) {
2079
- el[i].target = '_blank';
2080
- }
2081
- }
2082
- },
2083
-
2084
2561
  setButtonClass: function (buttonClass) {
2085
- var el = getSelectionStart.call(this),
2562
+ var el = meSelection.getSelectionStart(this.options.ownerDocument),
2086
2563
  classes = buttonClass.split(' '),
2087
2564
  i,
2088
2565
  j;
@@ -2101,6 +2578,7 @@ if (typeof module === 'object') {
2101
2578
  },
2102
2579
 
2103
2580
  createLink: function (input, target, buttonClass) {
2581
+
2104
2582
  var i, event;
2105
2583
 
2106
2584
  this.createLinkInternal(input.value, target, buttonClass);
@@ -2124,7 +2602,7 @@ if (typeof module === 'object') {
2124
2602
  return;
2125
2603
  }
2126
2604
 
2127
- restoreSelection.call(this, this.savedSelection);
2605
+ this.restoreSelection();
2128
2606
 
2129
2607
  if (this.options.checkLinkFormat) {
2130
2608
  url = this.checkLinkFormat(url);
@@ -2133,7 +2611,7 @@ if (typeof module === 'object') {
2133
2611
  this.options.ownerDocument.execCommand('createLink', false, url);
2134
2612
 
2135
2613
  if (this.options.targetBlank || target === "_blank") {
2136
- this.setTargetBlank();
2614
+ mediumEditorUtil.setTargetBlank(meSelection.getSelectionStart(this.options.ownerDocument));
2137
2615
  }
2138
2616
 
2139
2617
  if (buttonClass) {
@@ -2195,59 +2673,23 @@ if (typeof module === 'object') {
2195
2673
  this.elements[i].removeAttribute('data-medium-element');
2196
2674
  }
2197
2675
 
2198
- this.removeAllEvents();
2199
- },
2676
+ this.commands.forEach(function (extension) {
2677
+ if (typeof extension.deactivate === 'function') {
2678
+ extension.deactivate();
2679
+ }
2680
+ }.bind(this));
2200
2681
 
2201
- htmlEntities: function (str) {
2202
- // converts special characters (like <) into their escaped/encoded values (like &lt;).
2203
- // This allows you to show to display the string without the browser reading it as HTML.
2204
- return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2682
+ if (this.anchorExtension) {
2683
+ this.anchorExtension.deactivate();
2684
+ }
2685
+
2686
+ this.removeAllEvents();
2205
2687
  },
2206
2688
 
2207
2689
  bindPaste: function () {
2208
2690
  var i, self = this;
2209
2691
  this.pasteWrapper = function (e) {
2210
- var paragraphs,
2211
- html = '',
2212
- p,
2213
- dataFormatHTML = 'text/html',
2214
- dataFormatPlain = 'text/plain';
2215
-
2216
- this.classList.remove('medium-editor-placeholder');
2217
- if (!self.options.forcePlainText && !self.options.cleanPastedHTML) {
2218
- return this;
2219
- }
2220
-
2221
- if (self.options.contentWindow.clipboardData && e.clipboardData === undefined) {
2222
- e.clipboardData = self.options.contentWindow.clipboardData;
2223
- // If window.clipboardData exists, but e.clipboardData doesn't exist,
2224
- // we're probably in IE. IE only has two possibilities for clipboard
2225
- // data format: 'Text' and 'URL'.
2226
- //
2227
- // Of the two, we want 'Text':
2228
- dataFormatHTML = 'Text';
2229
- dataFormatPlain = 'Text';
2230
- }
2231
-
2232
- if (e.clipboardData && e.clipboardData.getData && !e.defaultPrevented) {
2233
- e.preventDefault();
2234
-
2235
- if (self.options.cleanPastedHTML && e.clipboardData.getData(dataFormatHTML)) {
2236
- return self.cleanPaste(e.clipboardData.getData(dataFormatHTML));
2237
- }
2238
- if (!(self.options.disableReturn || this.getAttribute('data-disable-return'))) {
2239
- paragraphs = e.clipboardData.getData(dataFormatPlain).split(/[\r\n]/g);
2240
- for (p = 0; p < paragraphs.length; p += 1) {
2241
- if (paragraphs[p] !== '') {
2242
- html += '<p>' + self.htmlEntities(paragraphs[p]) + '</p>';
2243
- }
2244
- }
2245
- self.insertHTML(html);
2246
- } else {
2247
- html = self.htmlEntities(e.clipboardData.getData(dataFormatPlain));
2248
- self.insertHTML(html);
2249
- }
2250
- }
2692
+ pasteHandler.handlePaste(this, e, self.options);
2251
2693
  };
2252
2694
  for (i = 0; i < this.elements.length; i += 1) {
2253
2695
  this.on(this.elements[i], 'paste', this.pasteWrapper);
@@ -2268,205 +2710,15 @@ if (typeof module === 'object') {
2268
2710
  },
2269
2711
 
2270
2712
  cleanPaste: function (text) {
2271
-
2272
- /*jslint regexp: true*/
2273
- /*
2274
- jslint does not allow character negation, because the negation
2275
- will not match any unicode characters. In the regexes in this
2276
- block, negation is used specifically to match the end of an html
2277
- tag, and in fact unicode characters *should* be allowed.
2278
- */
2279
- var i, elList, workEl,
2280
- el = this.getSelectionElement(),
2281
- multiline = /<p|<br|<div/.test(text),
2282
- replacements = [
2283
-
2284
- // replace two bogus tags that begin pastes from google docs
2285
- [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ""],
2286
- [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ""],
2287
-
2288
- // un-html spaces and newlines inserted by OS X
2289
- [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
2290
- [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
2291
-
2292
- // replace google docs italics+bold with a span to be replaced once the html is inserted
2293
- [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
2294
-
2295
- // replace google docs italics with a span to be replaced once the html is inserted
2296
- [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
2297
-
2298
- //[replace google docs bolds with a span to be replaced once the html is inserted
2299
- [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
2300
-
2301
- // replace manually entered b/i/a tags with real ones
2302
- [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
2303
-
2304
- // replace manually a tags with real ones, converting smart-quotes from google docs
2305
- [new RegExp(/&lt;a\s+href=(&quot;|&rdquo;|&ldquo;|“|”)([^&]+)(&quot;|&rdquo;|&ldquo;|“|”)&gt;/gi), '<a href="$2">']
2306
-
2307
- ];
2308
- /*jslint regexp: false*/
2309
-
2310
- for (i = 0; i < replacements.length; i += 1) {
2311
- text = text.replace(replacements[i][0], replacements[i][1]);
2312
- }
2313
-
2314
- if (multiline) {
2315
-
2316
- // double br's aren't converted to p tags, but we want paragraphs.
2317
- elList = text.split('<br><br>');
2318
-
2319
- this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>');
2320
- this.options.ownerDocument.execCommand('insertText', false, "\n");
2321
-
2322
- // block element cleanup
2323
- elList = el.querySelectorAll('a,p,div,br');
2324
- for (i = 0; i < elList.length; i += 1) {
2325
-
2326
- workEl = elList[i];
2327
-
2328
- switch (workEl.tagName.toLowerCase()) {
2329
- case 'a':
2330
- if (this.options.targetBlank) {
2331
- this.setTargetBlank(workEl);
2332
- }
2333
- break;
2334
- case 'p':
2335
- case 'div':
2336
- this.filterCommonBlocks(workEl);
2337
- break;
2338
- case 'br':
2339
- this.filterLineBreak(workEl);
2340
- break;
2341
- }
2342
-
2343
- }
2344
-
2345
-
2346
- } else {
2347
-
2348
- this.pasteHTML(text);
2349
-
2350
- }
2351
-
2713
+ pasteHandler.cleanPaste(text, this.options);
2352
2714
  },
2353
2715
 
2354
2716
  pasteHTML: function (html) {
2355
- var elList, workEl, i, fragmentBody, pasteBlock = this.options.ownerDocument.createDocumentFragment();
2356
-
2357
- pasteBlock.appendChild(this.options.ownerDocument.createElement('body'));
2358
-
2359
- fragmentBody = pasteBlock.querySelector('body');
2360
- fragmentBody.innerHTML = html;
2361
-
2362
- this.cleanupSpans(fragmentBody);
2363
-
2364
- elList = fragmentBody.querySelectorAll('*');
2365
- for (i = 0; i < elList.length; i += 1) {
2366
-
2367
- workEl = elList[i];
2368
-
2369
- // delete ugly attributes
2370
- workEl.removeAttribute('class');
2371
- workEl.removeAttribute('style');
2372
- workEl.removeAttribute('dir');
2373
-
2374
- if (workEl.tagName.toLowerCase() === 'meta') {
2375
- workEl.parentNode.removeChild(workEl);
2376
- }
2377
-
2378
- }
2379
- this.insertHTML(fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
2380
- },
2381
- isCommonBlock: function (el) {
2382
- return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
2383
- },
2384
- filterCommonBlocks: function (el) {
2385
- if (/^\s*$/.test(el.textContent)) {
2386
- el.parentNode.removeChild(el);
2387
- }
2388
- },
2389
- filterLineBreak: function (el) {
2390
- if (this.isCommonBlock(el.previousElementSibling)) {
2391
-
2392
- // remove stray br's following common block elements
2393
- el.parentNode.removeChild(el);
2394
-
2395
- } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
2396
-
2397
- // remove br's just inside open or close tags of a div/p
2398
- el.parentNode.removeChild(el);
2399
-
2400
- } else if (el.parentNode.childElementCount === 1) {
2401
-
2402
- // and br's that are the only child of a div/p
2403
- this.removeWithParent(el);
2404
-
2405
- }
2406
-
2407
- },
2408
-
2409
- // remove an element, including its parent, if it is the only element within its parent
2410
- removeWithParent: function (el) {
2411
- if (el && el.parentNode) {
2412
- if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
2413
- el.parentNode.parentNode.removeChild(el.parentNode);
2414
- } else {
2415
- el.parentNode.removeChild(el.parentNode);
2416
- }
2417
- }
2418
- },
2419
-
2420
- cleanupSpans: function (container_el) {
2421
-
2422
- var i,
2423
- el,
2424
- new_el,
2425
- spans = container_el.querySelectorAll('.replace-with'),
2426
- isCEF = function (el) {
2427
- return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
2428
- };
2429
-
2430
- for (i = 0; i < spans.length; i += 1) {
2431
-
2432
- el = spans[i];
2433
- new_el = this.options.ownerDocument.createElement(el.classList.contains('bold') ? 'b' : 'i');
2434
-
2435
- if (el.classList.contains('bold') && el.classList.contains('italic')) {
2436
-
2437
- // add an i tag as well if this has both italics and bold
2438
- new_el.innerHTML = '<i>' + el.innerHTML + '</i>';
2439
-
2440
- } else {
2441
-
2442
- new_el.innerHTML = el.innerHTML;
2443
-
2444
- }
2445
- el.parentNode.replaceChild(new_el, el);
2446
-
2447
- }
2448
-
2449
- spans = container_el.querySelectorAll('span');
2450
- for (i = 0; i < spans.length; i += 1) {
2451
-
2452
- el = spans[i];
2453
-
2454
- // bail if span is in contenteditable = false
2455
- if (this.traverseUp(el, isCEF)) {
2456
- return false;
2457
- }
2458
-
2459
- // remove empty spans, replace others with their contents
2460
- if (/^\s*$/.test()) {
2461
- el.parentNode.removeChild(el);
2462
- } else {
2463
- el.parentNode.replaceChild(this.options.ownerDocument.createTextNode(el.textContent), el);
2464
- }
2465
-
2466
- }
2467
-
2717
+ pasteHandler.pasteHTML(html, this.options.ownerDocument);
2468
2718
  }
2469
-
2470
2719
  };
2471
2720
 
2472
- }(window, document));
2721
+ }());
2722
+
2723
+ return MediumEditor;
2724
+ }()));