medium-editor-rails 1.3.0 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
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
+ }()));