scrivito-medium-editor 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 289648ffc2f698d06f4a295a40414752ce42109b
4
+ data.tar.gz: d75ed61c2ebdedace4a11af5cc8574d87d8795c1
5
+ SHA512:
6
+ metadata.gz: ec5150deb662c949b2f1aeec72eb6144c79af82e281f04da9c45fd8ce8eab77d822b4853a4b33e5fda592be0cc0533db133c9232de66ee9e3c084b569e9c1fd0
7
+ data.tar.gz: 354baf361e1fcb203d1c06213a76e94b199ab9ec031179125f5ea24ce116e375a44b6464e54edf133c85da457eda669828684257e416e8d138d2204ac1ec6674
data/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) 2009 - 2015 Infopark AG (http://www.infopark.com)
2
+
3
+ This software can be used and modified under the LGPL-3.0. Please refer to
4
+ http://www.gnu.org/licenses/lgpl-3.0.html for the license text.
@@ -0,0 +1,3 @@
1
+ require 'bundler'
2
+
3
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,35 @@
1
+ //= require medium-editor
2
+ //= require_self
3
+
4
+ ;(function() {
5
+
6
+ scrivito.on('content', function(content) {
7
+ if (!scrivito.in_editable_view()) { return; }
8
+
9
+ $(content).find('[data-editor=medium]').each(function() {
10
+ var contenteditable = $(this);
11
+
12
+ var config;
13
+ if (contenteditable.attr('data-medium-editor')) {
14
+ config = JSON.parse(contenteditable.attr('data-medium-editor'));
15
+ } else {
16
+ config = {buttons: [
17
+ 'bold',
18
+ 'italic',
19
+ 'underline',
20
+ 'header1',
21
+ 'header2',
22
+ 'unorderedlist',
23
+ 'orderedlist'
24
+ ]};
25
+ }
26
+
27
+ new MediumEditor(contenteditable, config);
28
+
29
+ contenteditable.on('input', function() {
30
+ contenteditable.scrivito('save', $(this).html());
31
+ });
32
+ });
33
+ });
34
+
35
+ }());
@@ -0,0 +1,4 @@
1
+ /*
2
+ *= require medium-editor
3
+ *= require medium-editor-theme-flat
4
+ */
@@ -0,0 +1,4 @@
1
+ require 'scrivito_medium_editor/engine'
2
+
3
+ module ScrivitoMediumEditor
4
+ end
@@ -0,0 +1,5 @@
1
+ module ScrivitoMediumEditor
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ScrivitoMediumEditor
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module ScrivitoMediumEditor
2
+ VERSION = '0.0.2'.freeze
3
+ end
@@ -0,0 +1,2180 @@
1
+ function MediumEditor(elements, options) {
2
+ 'use strict';
3
+ return this.init(elements, options);
4
+ }
5
+
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
+ }
15
+
16
+ (function (window, document) {
17
+ 'use strict';
18
+
19
+ function extend(b, a) {
20
+ var prop;
21
+ if (b === undefined) {
22
+ return a;
23
+ }
24
+ for (prop in a) {
25
+ if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
26
+ b[prop] = a[prop];
27
+ }
28
+ }
29
+ return b;
30
+ }
31
+
32
+ // https://github.com/jashkenas/underscore
33
+ var now = Date.now || function () {
34
+ return new Date().getTime();
35
+ };
36
+
37
+ // https://github.com/jashkenas/underscore
38
+ function throttle(func, wait) {
39
+ var THROTTLE_INTERVAL = 50,
40
+ context,
41
+ args,
42
+ result,
43
+ timeout = null,
44
+ previous = 0,
45
+ later;
46
+
47
+ if (!wait && wait !== 0) {
48
+ wait = THROTTLE_INTERVAL;
49
+ }
50
+
51
+ later = function () {
52
+ previous = now();
53
+ timeout = null;
54
+ result = func.apply(context, args);
55
+ if (!timeout) {
56
+ context = args = null;
57
+ }
58
+ };
59
+
60
+ return function () {
61
+ var currNow = now(),
62
+ remaining = wait - (currNow - previous);
63
+ context = this;
64
+ args = arguments;
65
+ if (remaining <= 0 || remaining > wait) {
66
+ clearTimeout(timeout);
67
+ timeout = null;
68
+ previous = currNow;
69
+ result = func.apply(context, args);
70
+ if (!timeout) {
71
+ context = args = null;
72
+ }
73
+ } else if (!timeout) {
74
+ timeout = setTimeout(later, remaining);
75
+ }
76
+ return result;
77
+ };
78
+ }
79
+
80
+ function isDescendant(parent, child) {
81
+ var node = child.parentNode;
82
+ while (node !== null) {
83
+ if (node === parent) {
84
+ return true;
85
+ }
86
+ node = node.parentNode;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ // Find the next node in the DOM tree that represents any text that is being
92
+ // displayed directly next to the targetNode (passed as an argument)
93
+ // Text that appears directly next to the current node can be:
94
+ // - A sibling text node
95
+ // - A descendant of a sibling element
96
+ // - A sibling text node of an ancestor
97
+ // - A descendant of a sibling element of an ancestor
98
+ function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
99
+ var pastTarget = false,
100
+ nextNode,
101
+ nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
102
+
103
+ // Use a native NodeIterator to iterate over all the text nodes that are descendants
104
+ // of the rootNode. Once past the targetNode, choose the first non-empty text node
105
+ nextNode = nodeIterator.nextNode();
106
+ while (nextNode) {
107
+ if (nextNode === targetNode) {
108
+ pastTarget = true;
109
+ } else if (pastTarget) {
110
+ if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
111
+ break;
112
+ }
113
+ }
114
+ nextNode = nodeIterator.nextNode();
115
+ }
116
+
117
+ return nextNode;
118
+ }
119
+
120
+ // http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element
121
+ // by Tim Down
122
+ function saveSelection() {
123
+ var i,
124
+ len,
125
+ ranges,
126
+ sel = this.options.contentWindow.getSelection();
127
+ if (sel.getRangeAt && sel.rangeCount) {
128
+ ranges = [];
129
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
130
+ ranges.push(sel.getRangeAt(i));
131
+ }
132
+ return ranges;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function restoreSelection(savedSel) {
138
+ var i,
139
+ len,
140
+ sel = this.options.contentWindow.getSelection();
141
+ if (savedSel) {
142
+ sel.removeAllRanges();
143
+ for (i = 0, len = savedSel.length; i < len; i += 1) {
144
+ sel.addRange(savedSel[i]);
145
+ }
146
+ }
147
+ }
148
+
149
+ // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
150
+ // by You
151
+ function getSelectionStart() {
152
+ var node = this.options.ownerDocument.getSelection().anchorNode,
153
+ startNode = (node && node.nodeType === 3 ? node.parentNode : node);
154
+ return startNode;
155
+ }
156
+
157
+ // http://stackoverflow.com/questions/4176923/html-of-selected-text
158
+ // by Tim Down
159
+ function getSelectionHtml() {
160
+ var i,
161
+ html = '',
162
+ sel,
163
+ len,
164
+ container;
165
+ if (this.options.contentWindow.getSelection !== undefined) {
166
+ sel = this.options.contentWindow.getSelection();
167
+ if (sel.rangeCount) {
168
+ container = this.options.ownerDocument.createElement('div');
169
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
170
+ container.appendChild(sel.getRangeAt(i).cloneContents());
171
+ }
172
+ html = container.innerHTML;
173
+ }
174
+ } else if (this.options.ownerDocument.selection !== undefined) {
175
+ if (this.options.ownerDocument.selection.type === 'Text') {
176
+ html = this.options.ownerDocument.selection.createRange().htmlText;
177
+ }
178
+ }
179
+ return html;
180
+ }
181
+
182
+ /**
183
+ * Find the caret position within an element irrespective of any inline tags it may contain.
184
+ *
185
+ * @param {DOMElement} An element containing the cursor to find offsets relative to.
186
+ * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
187
+ * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
188
+ */
189
+ function getCaretOffsets(element, range) {
190
+ var preCaretRange, postCaretRange;
191
+
192
+ if (!range) {
193
+ range = window.getSelection().getRangeAt(0);
194
+ }
195
+
196
+ preCaretRange = range.cloneRange();
197
+ postCaretRange = range.cloneRange();
198
+
199
+ preCaretRange.selectNodeContents(element);
200
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
201
+
202
+ postCaretRange.selectNodeContents(element);
203
+ postCaretRange.setStart(range.endContainer, range.endOffset);
204
+
205
+ return {
206
+ left: preCaretRange.toString().length,
207
+ right: postCaretRange.toString().length
208
+ };
209
+ }
210
+
211
+
212
+ // https://github.com/jashkenas/underscore
213
+ function isElement(obj) {
214
+ return !!(obj && obj.nodeType === 1);
215
+ }
216
+
217
+ // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
218
+ function insertHTMLCommand(doc, html) {
219
+ var selection, range, el, fragment, node, lastNode;
220
+
221
+ if (doc.queryCommandSupported('insertHTML')) {
222
+ return doc.execCommand('insertHTML', false, html);
223
+ }
224
+
225
+ selection = window.getSelection();
226
+ if (selection.getRangeAt && selection.rangeCount) {
227
+ range = selection.getRangeAt(0);
228
+ range.deleteContents();
229
+
230
+ el = doc.createElement("div");
231
+ el.innerHTML = html;
232
+ fragment = doc.createDocumentFragment();
233
+ while (el.firstChild) {
234
+ node = el.firstChild;
235
+ lastNode = fragment.appendChild(node);
236
+ }
237
+ range.insertNode(fragment);
238
+
239
+ // Preserve the selection:
240
+ if (lastNode) {
241
+ range = range.cloneRange();
242
+ range.setStartAfter(lastNode);
243
+ range.collapse(true);
244
+ selection.removeAllRanges();
245
+ selection.addRange(range);
246
+ }
247
+ }
248
+ }
249
+
250
+ MediumEditor.prototype = {
251
+ defaults: {
252
+ allowMultiParagraphSelection: true,
253
+ anchorInputPlaceholder: 'Paste or type a link',
254
+ anchorInputCheckboxLabel: 'Open in new window',
255
+ anchorPreviewHideDelay: 500,
256
+ buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
257
+ buttonLabels: false,
258
+ checkLinkFormat: false,
259
+ cleanPastedHTML: false,
260
+ delay: 0,
261
+ diffLeft: 0,
262
+ diffTop: -10,
263
+ disableReturn: false,
264
+ disableDoubleReturn: false,
265
+ disableToolbar: false,
266
+ disableEditing: false,
267
+ disableAnchorForm: false,
268
+ disablePlaceholders: false,
269
+ elementsContainer: false,
270
+ standardizeSelectionStart: false,
271
+ contentWindow: window,
272
+ ownerDocument: document,
273
+ firstHeader: 'h3',
274
+ forcePlainText: true,
275
+ placeholder: 'Type your text',
276
+ secondHeader: 'h4',
277
+ targetBlank: false,
278
+ anchorTarget: false,
279
+ anchorButton: false,
280
+ anchorButtonClass: 'btn',
281
+ extensions: {},
282
+ activeButtonClass: 'medium-editor-button-active',
283
+ firstButtonClass: 'medium-editor-button-first',
284
+ lastButtonClass: 'medium-editor-button-last'
285
+ },
286
+
287
+ // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
288
+ // by rg89
289
+ isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
290
+
291
+ init: function (elements, options) {
292
+ var uniqueId = 1;
293
+
294
+ this.options = extend(options, this.defaults);
295
+ this.setElementSelection(elements);
296
+ if (this.elements.length === 0) {
297
+ return;
298
+ }
299
+ this.parentElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'];
300
+ if (!this.options.elementsContainer) {
301
+ this.options.elementsContainer = this.options.ownerDocument.body;
302
+ }
303
+
304
+ while (this.options.elementsContainer.querySelector('#medium-editor-toolbar-' + uniqueId)) {
305
+ uniqueId = uniqueId + 1;
306
+ }
307
+
308
+ this.id = uniqueId;
309
+
310
+ return this.setup();
311
+ },
312
+
313
+ setup: function () {
314
+ this.events = [];
315
+ this.isActive = true;
316
+ this.initThrottledMethods()
317
+ .initElements()
318
+ .bindSelect()
319
+ .bindPaste()
320
+ .setPlaceholders()
321
+ .bindElementActions()
322
+ .bindWindowActions();
323
+ //.passInstance();
324
+ },
325
+
326
+ on: function (target, event, listener, useCapture) {
327
+ target.addEventListener(event, listener, useCapture);
328
+ this.events.push([target, event, listener, useCapture]);
329
+ },
330
+
331
+ off: function (target, event, listener, useCapture) {
332
+ var index = this.indexOfListener(target, event, listener, useCapture),
333
+ e;
334
+ if (index !== -1) {
335
+ e = this.events.splice(index, 1)[0];
336
+ e[0].removeEventListener(e[1], e[2], e[3]);
337
+ }
338
+ },
339
+
340
+ indexOfListener: function (target, event, listener, useCapture) {
341
+ var i, n, item;
342
+ for (i = 0, n = this.events.length; i < n; i = i + 1) {
343
+ item = this.events[i];
344
+ if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
345
+ return i;
346
+ }
347
+ }
348
+ return -1;
349
+ },
350
+
351
+ delay: function (fn) {
352
+ var self = this;
353
+ setTimeout(function () {
354
+ if (self.isActive) {
355
+ fn();
356
+ }
357
+ }, this.options.delay);
358
+ },
359
+
360
+ removeAllEvents: function () {
361
+ var e = this.events.pop();
362
+ while (e) {
363
+ e[0].removeEventListener(e[1], e[2], e[3]);
364
+ e = this.events.pop();
365
+ }
366
+ },
367
+
368
+ initThrottledMethods: function () {
369
+ var self = this;
370
+
371
+ // handleResize is throttled because:
372
+ // - It will be called when the browser is resizing, which can fire many times very quickly
373
+ // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
374
+ this.handleResize = throttle(function () {
375
+ if (self.isActive) {
376
+ self.positionToolbarIfShown();
377
+ }
378
+ });
379
+
380
+ // handleBlur is throttled because:
381
+ // - This method could be called many times due to the type of event handlers that are calling it
382
+ // - We want a slight delay so that other events in the stack can run, some of which may
383
+ // prevent the toolbar from being hidden (via this.keepToolbarAlive).
384
+ this.handleBlur = throttle(function () {
385
+ if (self.isActive && !self.keepToolbarAlive) {
386
+ self.hideToolbarActions();
387
+ }
388
+ });
389
+
390
+ return this;
391
+ },
392
+
393
+ initElements: function () {
394
+ var i,
395
+ addToolbar = false;
396
+ for (i = 0; i < this.elements.length; i += 1) {
397
+ if (!this.options.disableEditing && !this.elements[i].getAttribute('data-disable-editing')) {
398
+ this.elements[i].setAttribute('contentEditable', true);
399
+ }
400
+ if (!this.elements[i].getAttribute('data-placeholder')) {
401
+ this.elements[i].setAttribute('data-placeholder', this.options.placeholder);
402
+ }
403
+ this.elements[i].setAttribute('data-medium-element', true);
404
+ this.elements[i].setAttribute('role', 'textbox');
405
+ this.elements[i].setAttribute('aria-multiline', true);
406
+ this.bindParagraphCreation(i);
407
+ if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) {
408
+ addToolbar = true;
409
+ }
410
+ }
411
+ // Init toolbar
412
+ if (addToolbar) {
413
+ this.passInstance()
414
+ .callExtensions('init')
415
+ .initToolbar()
416
+ .bindButtons()
417
+ .bindAnchorForm()
418
+ .bindAnchorPreview();
419
+ }
420
+ return this;
421
+ },
422
+
423
+ setElementSelection: function (selector) {
424
+ if (!selector) {
425
+ selector = [];
426
+ }
427
+ // If string, use as query selector
428
+ if (typeof selector === 'string') {
429
+ selector = this.options.ownerDocument.querySelectorAll(selector);
430
+ }
431
+ // If element, put into array
432
+ if (isElement(selector)) {
433
+ selector = [selector];
434
+ }
435
+ // Convert NodeList (or other array like object) into an array
436
+ this.elements = Array.prototype.slice.apply(selector);
437
+ },
438
+
439
+ bindBlur: function () {
440
+ var self = this,
441
+ blurFunction = function (e) {
442
+ var isDescendantOfEditorElements = false,
443
+ i;
444
+ for (i = 0; i < self.elements.length; i += 1) {
445
+ if (isDescendant(self.elements[i], e.target)) {
446
+ isDescendantOfEditorElements = true;
447
+ break;
448
+ }
449
+ }
450
+ // If it's not part of the editor, or the toolbar
451
+ if (e.target !== self.toolbar
452
+ && self.elements.indexOf(e.target) === -1
453
+ && !isDescendantOfEditorElements
454
+ && !isDescendant(self.toolbar, e.target)
455
+ && !isDescendant(self.anchorPreview, e.target)) {
456
+
457
+ // Activate the placeholder
458
+ if (!self.options.disablePlaceholders) {
459
+ self.placeholderWrapper(e, self.elements[0]);
460
+ }
461
+
462
+ // Hide the toolbar after a small delay so we can prevent this on toolbar click
463
+ self.handleBlur();
464
+ }
465
+ };
466
+
467
+ // Hide the toolbar when focusing outside of the editor.
468
+ this.on(this.options.ownerDocument.body, 'click', blurFunction, true);
469
+ this.on(this.options.ownerDocument.body, 'focus', blurFunction, true);
470
+
471
+ return this;
472
+ },
473
+
474
+ bindClick: function (i) {
475
+ var self = this;
476
+
477
+ this.on(this.elements[i], 'click', function () {
478
+ if (!self.options.disablePlaceholders) {
479
+ // Remove placeholder
480
+ this.classList.remove('medium-editor-placeholder');
481
+ }
482
+
483
+ if (self.options.staticToolbar) {
484
+ self.setToolbarPosition();
485
+ }
486
+ });
487
+
488
+ return this;
489
+ },
490
+
491
+ /**
492
+ * This handles blur and keypress events on elements
493
+ * Including Placeholders, and tooldbar hiding on blur
494
+ */
495
+ bindElementActions: function () {
496
+ var i;
497
+
498
+ for (i = 0; i < this.elements.length; i += 1) {
499
+
500
+ if (!this.options.disablePlaceholders) {
501
+ // Active all of the placeholders
502
+ this.activatePlaceholder(this.elements[i]);
503
+ }
504
+
505
+ // Bind the return and tab keypress events
506
+ this.bindReturn(i)
507
+ .bindKeydown(i)
508
+ .bindBlur()
509
+ .bindClick(i);
510
+ }
511
+
512
+ return this;
513
+ },
514
+
515
+ // Two functions to handle placeholders
516
+ activatePlaceholder: function (el) {
517
+ if (!(el.querySelector('img')) &&
518
+ !(el.querySelector('blockquote')) &&
519
+ el.textContent.replace(/^\s+|\s+$/g, '') === '') {
520
+
521
+ el.classList.add('medium-editor-placeholder');
522
+ }
523
+ },
524
+ placeholderWrapper: function (evt, el) {
525
+ el = el || evt.target;
526
+ el.classList.remove('medium-editor-placeholder');
527
+ if (evt.type !== 'keypress') {
528
+ this.activatePlaceholder(el);
529
+ }
530
+ },
531
+
532
+ serialize: function () {
533
+ var i,
534
+ elementid,
535
+ content = {};
536
+ for (i = 0; i < this.elements.length; i += 1) {
537
+ elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
538
+ content[elementid] = {
539
+ value: this.elements[i].innerHTML.trim()
540
+ };
541
+ }
542
+ return content;
543
+ },
544
+
545
+ /**
546
+ * Helper function to call a method with a number of parameters on all registered extensions.
547
+ * The function assures that the function exists before calling.
548
+ *
549
+ * @param {string} funcName name of the function to call
550
+ * @param [args] arguments passed into funcName
551
+ */
552
+ callExtensions: function (funcName) {
553
+ if (arguments.length < 1) {
554
+ return;
555
+ }
556
+
557
+ var args = Array.prototype.slice.call(arguments, 1),
558
+ ext,
559
+ name;
560
+
561
+ for (name in this.options.extensions) {
562
+ if (this.options.extensions.hasOwnProperty(name)) {
563
+ ext = this.options.extensions[name];
564
+ if (ext[funcName] !== undefined) {
565
+ ext[funcName].apply(ext, args);
566
+ }
567
+ }
568
+ }
569
+ return this;
570
+ },
571
+
572
+ /**
573
+ * Pass current Medium Editor instance to all extensions
574
+ * if extension constructor has 'parent' attribute set to 'true'
575
+ *
576
+ */
577
+ passInstance: function () {
578
+ var self = this,
579
+ ext,
580
+ name;
581
+
582
+ for (name in self.options.extensions) {
583
+ if (self.options.extensions.hasOwnProperty(name)) {
584
+ ext = self.options.extensions[name];
585
+
586
+ if (ext.parent) {
587
+ ext.base = self;
588
+ }
589
+ }
590
+ }
591
+
592
+ return self;
593
+ },
594
+
595
+ bindParagraphCreation: function (index) {
596
+ var self = this;
597
+ this.on(this.elements[index], 'keypress', function (e) {
598
+ var node,
599
+ tagName;
600
+ if (e.which === 32) {
601
+ node = getSelectionStart.call(self);
602
+ tagName = node.tagName.toLowerCase();
603
+ if (tagName === 'a') {
604
+ self.options.ownerDocument.execCommand('unlink', false, null);
605
+ }
606
+ }
607
+ });
608
+
609
+ this.on(this.elements[index], 'keyup', function (e) {
610
+ var node = getSelectionStart.call(self),
611
+ tagName,
612
+ editorElement;
613
+
614
+ if (node && node.getAttribute('data-medium-element') && node.children.length === 0 && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
615
+ self.options.ownerDocument.execCommand('formatBlock', false, 'p');
616
+ }
617
+ if (e.which === 13) {
618
+ node = getSelectionStart.call(self);
619
+ tagName = node.tagName.toLowerCase();
620
+ editorElement = self.getSelectionElement();
621
+
622
+ if (!(self.options.disableReturn || editorElement.getAttribute('data-disable-return')) &&
623
+ tagName !== 'li' && !self.isListItemChild(node)) {
624
+ if (!e.shiftKey) {
625
+
626
+ // paragraph creation should not be forced within a header tag
627
+ if (!/h\d/.test(tagName)) {
628
+ self.options.ownerDocument.execCommand('formatBlock', false, 'p');
629
+ }
630
+ }
631
+ if (tagName === 'a') {
632
+ self.options.ownerDocument.execCommand('unlink', false, null);
633
+ }
634
+ }
635
+ }
636
+ });
637
+ return this;
638
+ },
639
+
640
+ isListItemChild: function (node) {
641
+ var parentNode = node.parentNode,
642
+ tagName = parentNode.tagName.toLowerCase();
643
+ while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
644
+ if (tagName === 'li') {
645
+ return true;
646
+ }
647
+ parentNode = parentNode.parentNode;
648
+ if (parentNode && parentNode.tagName) {
649
+ tagName = parentNode.tagName.toLowerCase();
650
+ } else {
651
+ return false;
652
+ }
653
+ }
654
+ return false;
655
+ },
656
+
657
+ bindReturn: function (index) {
658
+ var self = this;
659
+ this.on(this.elements[index], 'keypress', function (e) {
660
+ if (e.which === 13) {
661
+ if (self.options.disableReturn || this.getAttribute('data-disable-return')) {
662
+ e.preventDefault();
663
+ } else if (self.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
664
+ var node = getSelectionStart.call(self);
665
+ if (node && node.textContent === '\n') {
666
+ e.preventDefault();
667
+ }
668
+ }
669
+ }
670
+ });
671
+ return this;
672
+ },
673
+
674
+ bindKeydown: function (index) {
675
+ var self = this;
676
+ this.on(this.elements[index], 'keydown', function (e) {
677
+
678
+ if (e.which === 9) {
679
+ // Override tab only for pre nodes
680
+ var tag = getSelectionStart.call(self).tagName.toLowerCase();
681
+ if (tag === 'pre') {
682
+ e.preventDefault();
683
+ self.options.ownerDocument.execCommand('insertHtml', null, ' ');
684
+ }
685
+
686
+ // Tab to indent list structures!
687
+ if (tag === 'li') {
688
+ e.preventDefault();
689
+
690
+ // If Shift is down, outdent, otherwise indent
691
+ if (e.shiftKey) {
692
+ self.options.ownerDocument.execCommand('outdent', e);
693
+ } else {
694
+ self.options.ownerDocument.execCommand('indent', e);
695
+ }
696
+ }
697
+ } else if (e.which === 8 || e.which === 46 || e.which === 13) {
698
+
699
+ // Bind keys which can create or destroy a block element: backspace, delete, return
700
+ self.onBlockModifier(e);
701
+
702
+ }
703
+ });
704
+ return this;
705
+ },
706
+
707
+ onBlockModifier: function (e) {
708
+
709
+ var range, sel, p, node = getSelectionStart.call(this),
710
+ tagName = node.tagName.toLowerCase(),
711
+ isEmpty = /^(\s+|<br\/?>)?$/i,
712
+ isHeader = /h\d/i;
713
+
714
+ // backspace or return
715
+ if ((e.which === 8 || e.which === 13)
716
+ && node.previousElementSibling
717
+ // in a header
718
+ && isHeader.test(tagName)
719
+ // at the very end of the block
720
+ && getCaretOffsets(node).left === 0) {
721
+ if (e.which === 8 && isEmpty.test(node.previousElementSibling.innerHTML)) {
722
+ // backspacing the begining of a header into an empty previous element will
723
+ // change the tagName of the current node to prevent one
724
+ // instead delete previous node and cancel the event.
725
+ node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
726
+ e.preventDefault();
727
+ } else if (e.which === 13) {
728
+ // hitting return in the begining of a header will create empty header elements before the current one
729
+ // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
730
+ p = this.options.ownerDocument.createElement('p');
731
+ p.innerHTML = '<br>';
732
+ node.previousElementSibling.parentNode.insertBefore(p, node);
733
+ e.preventDefault();
734
+ }
735
+
736
+ // delete
737
+ } else if (e.which === 46
738
+ && node.nextElementSibling
739
+ && node.previousElementSibling
740
+ // not in a header
741
+ && !isHeader.test(tagName)
742
+ // in an empty tag
743
+ && isEmpty.test(node.innerHTML)
744
+ // when the next tag *is* a header
745
+ && isHeader.test(node.nextElementSibling.tagName)) {
746
+ // hitting delete in an empty element preceding a header, ex:
747
+ // <p>[CURSOR]</p><h1>Header</h1>
748
+ // Will cause the h1 to become a paragraph.
749
+ // Instead, delete the paragraph node and move the cursor to the begining of the h1
750
+
751
+ // remove node and move cursor to start of header
752
+ range = document.createRange();
753
+ sel = window.getSelection();
754
+
755
+ range.setStart(node.nextElementSibling, 0);
756
+ range.collapse(true);
757
+
758
+ sel.removeAllRanges();
759
+ sel.addRange(range);
760
+
761
+ node.previousElementSibling.parentNode.removeChild(node);
762
+
763
+ e.preventDefault();
764
+ }
765
+
766
+ },
767
+
768
+ buttonTemplate: function (btnType) {
769
+ var buttonLabels = this.getButtonLabels(this.options.buttonLabels),
770
+ buttonTemplates = {
771
+ 'bold': '<button class="medium-editor-action medium-editor-action-bold" data-action="bold" data-element="b" aria-label="bold">' + buttonLabels.bold + '</button>',
772
+ 'italic': '<button class="medium-editor-action medium-editor-action-italic" data-action="italic" data-element="i" aria-label="italic">' + buttonLabels.italic + '</button>',
773
+ 'underline': '<button class="medium-editor-action medium-editor-action-underline" data-action="underline" data-element="u" aria-label="underline">' + buttonLabels.underline + '</button>',
774
+ 'strikethrough': '<button class="medium-editor-action medium-editor-action-strikethrough" data-action="strikethrough" data-element="strike" aria-label="strike through">' + buttonLabels.strikethrough + '</button>',
775
+ 'superscript': '<button class="medium-editor-action medium-editor-action-superscript" data-action="superscript" data-element="sup" aria-label="superscript">' + buttonLabels.superscript + '</button>',
776
+ 'subscript': '<button class="medium-editor-action medium-editor-action-subscript" data-action="subscript" data-element="sub" aria-label="subscript">' + buttonLabels.subscript + '</button>',
777
+ 'anchor': '<button class="medium-editor-action medium-editor-action-anchor" data-action="anchor" data-element="a" aria-label="link">' + buttonLabels.anchor + '</button>',
778
+ 'image': '<button class="medium-editor-action medium-editor-action-image" data-action="image" data-element="img" aria-label="image">' + buttonLabels.image + '</button>',
779
+ 'header1': '<button class="medium-editor-action medium-editor-action-header1" data-action="append-' + this.options.firstHeader + '" data-element="' + this.options.firstHeader + '" aria-label="h1">' + buttonLabels.header1 + '</button>',
780
+ 'header2': '<button class="medium-editor-action medium-editor-action-header2" data-action="append-' + this.options.secondHeader + '" data-element="' + this.options.secondHeader + ' "aria-label="h2">' + buttonLabels.header2 + '</button>',
781
+ 'quote': '<button class="medium-editor-action medium-editor-action-quote" data-action="append-blockquote" data-element="blockquote" aria-label="blockquote">' + buttonLabels.quote + '</button>',
782
+ 'orderedlist': '<button class="medium-editor-action medium-editor-action-orderedlist" data-action="insertorderedlist" data-element="ol" aria-label="ordered list">' + buttonLabels.orderedlist + '</button>',
783
+ 'unorderedlist': '<button class="medium-editor-action medium-editor-action-unorderedlist" data-action="insertunorderedlist" data-element="ul" aria-label="unordered list">' + buttonLabels.unorderedlist + '</button>',
784
+ 'pre': '<button class="medium-editor-action medium-editor-action-pre" data-action="append-pre" data-element="pre" aria-label="preformatted text">' + buttonLabels.pre + '</button>',
785
+ 'indent': '<button class="medium-editor-action medium-editor-action-indent" data-action="indent" data-element="ul" aria-label="indent">' + buttonLabels.indent + '</button>',
786
+ 'outdent': '<button class="medium-editor-action medium-editor-action-outdent" data-action="outdent" data-element="ul" aria-label="outdent">' + buttonLabels.outdent + '</button>',
787
+ 'justifyCenter': '<button class="medium-editor-action medium-editor-action-justifyCenter" data-action="justifyCenter" data-element="" aria-label="center justify">' + buttonLabels.justifyCenter + '</button>',
788
+ 'justifyFull': '<button class="medium-editor-action medium-editor-action-justifyFull" data-action="justifyFull" data-element="" aria-label="full justify">' + buttonLabels.justifyFull + '</button>',
789
+ 'justifyLeft': '<button class="medium-editor-action medium-editor-action-justifyLeft" data-action="justifyLeft" data-element="" aria-label="left justify">' + buttonLabels.justifyLeft + '</button>',
790
+ 'justifyRight': '<button class="medium-editor-action medium-editor-action-justifyRight" data-action="justifyRight" data-element="" aria-label="right justify">' + buttonLabels.justifyRight + '</button>'
791
+ };
792
+ return buttonTemplates[btnType] || false;
793
+ },
794
+
795
+ // TODO: break method
796
+ getButtonLabels: function (buttonLabelType) {
797
+ var customButtonLabels,
798
+ attrname,
799
+ buttonLabels = {
800
+ 'bold': '<b>B</b>',
801
+ 'italic': '<b><i>I</i></b>',
802
+ 'underline': '<b><u>U</u></b>',
803
+ 'strikethrough': '<s>A</s>',
804
+ 'superscript': '<b>x<sup>1</sup></b>',
805
+ 'subscript': '<b>x<sub>1</sub></b>',
806
+ 'anchor': '<b>#</b>',
807
+ 'image': '<b>image</b>',
808
+ 'header1': '<b>H1</b>',
809
+ 'header2': '<b>H2</b>',
810
+ 'quote': '<b>&ldquo;</b>',
811
+ 'orderedlist': '<b>1.</b>',
812
+ 'unorderedlist': '<b>&bull;</b>',
813
+ 'pre': '<b>0101</b>',
814
+ 'indent': '<b>&rarr;</b>',
815
+ 'outdent': '<b>&larr;</b>',
816
+ 'justifyCenter': '<b>C</b>',
817
+ 'justifyFull': '<b>J</b>',
818
+ 'justifyLeft': '<b>L</b>',
819
+ 'justifyRight': '<b>R</b>'
820
+ };
821
+ if (buttonLabelType === 'fontawesome') {
822
+ customButtonLabels = {
823
+ 'bold': '<i class="fa fa-bold"></i>',
824
+ 'italic': '<i class="fa fa-italic"></i>',
825
+ 'underline': '<i class="fa fa-underline"></i>',
826
+ 'strikethrough': '<i class="fa fa-strikethrough"></i>',
827
+ 'superscript': '<i class="fa fa-superscript"></i>',
828
+ 'subscript': '<i class="fa fa-subscript"></i>',
829
+ 'anchor': '<i class="fa fa-link"></i>',
830
+ 'image': '<i class="fa fa-picture-o"></i>',
831
+ 'quote': '<i class="fa fa-quote-right"></i>',
832
+ 'orderedlist': '<i class="fa fa-list-ol"></i>',
833
+ 'unorderedlist': '<i class="fa fa-list-ul"></i>',
834
+ 'pre': '<i class="fa fa-code fa-lg"></i>',
835
+ 'indent': '<i class="fa fa-indent"></i>',
836
+ 'outdent': '<i class="fa fa-outdent"></i>',
837
+ 'justifyCenter': '<i class="fa fa-align-center"></i>',
838
+ 'justifyFull': '<i class="fa fa-align-justify"></i>',
839
+ 'justifyLeft': '<i class="fa fa-align-left"></i>',
840
+ 'justifyRight': '<i class="fa fa-align-right"></i>'
841
+ };
842
+ } else if (typeof buttonLabelType === 'object') {
843
+ customButtonLabels = buttonLabelType;
844
+ }
845
+ if (typeof customButtonLabels === 'object') {
846
+ for (attrname in customButtonLabels) {
847
+ if (customButtonLabels.hasOwnProperty(attrname)) {
848
+ buttonLabels[attrname] = customButtonLabels[attrname];
849
+ }
850
+ }
851
+ }
852
+ return buttonLabels;
853
+ },
854
+
855
+ initToolbar: function () {
856
+ if (this.toolbar) {
857
+ return this;
858
+ }
859
+ this.toolbar = this.createToolbar();
860
+ this.addExtensionForms();
861
+ this.keepToolbarAlive = false;
862
+ this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions');
863
+ this.anchorPreview = this.createAnchorPreview();
864
+
865
+ if (!this.options.disableAnchorForm) {
866
+ this.anchorForm = this.toolbar.querySelector('.medium-editor-toolbar-form');
867
+ this.anchorInput = this.anchorForm.querySelector('input.medium-editor-toolbar-input');
868
+ this.anchorTarget = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-target');
869
+ this.anchorButton = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-button');
870
+ }
871
+ return this;
872
+ },
873
+
874
+ createToolbar: function () {
875
+ var toolbar = this.options.ownerDocument.createElement('div');
876
+ toolbar.id = 'medium-editor-toolbar-' + this.id;
877
+ toolbar.className = 'medium-editor-toolbar';
878
+
879
+ if (this.options.staticToolbar) {
880
+ toolbar.className += " static-toolbar";
881
+ } else {
882
+ toolbar.className += " stalker-toolbar";
883
+ }
884
+
885
+ toolbar.appendChild(this.toolbarButtons());
886
+ if (!this.options.disableAnchorForm) {
887
+ toolbar.appendChild(this.toolbarFormAnchor());
888
+ }
889
+ this.options.elementsContainer.appendChild(toolbar);
890
+ return toolbar;
891
+ },
892
+
893
+ //TODO: actionTemplate
894
+ toolbarButtons: function () {
895
+ var btns = this.options.buttons,
896
+ ul = this.options.ownerDocument.createElement('ul'),
897
+ li,
898
+ i,
899
+ btn,
900
+ ext;
901
+
902
+ ul.id = 'medium-editor-toolbar-actions' + this.id;
903
+ ul.className = 'medium-editor-toolbar-actions clearfix';
904
+
905
+ for (i = 0; i < btns.length; i += 1) {
906
+ if (this.options.extensions.hasOwnProperty(btns[i])) {
907
+ ext = this.options.extensions[btns[i]];
908
+ btn = ext.getButton !== undefined ? ext.getButton(this) : null;
909
+ if (ext.hasForm) {
910
+ btn.setAttribute('data-form', 'medium-editor-toolbar-form-' + btns[i] + '-' + this.id);
911
+ }
912
+ } else {
913
+ btn = this.buttonTemplate(btns[i]);
914
+ }
915
+
916
+ if (btn) {
917
+ li = this.options.ownerDocument.createElement('li');
918
+ if (isElement(btn)) {
919
+ li.appendChild(btn);
920
+ } else {
921
+ li.innerHTML = btn;
922
+ }
923
+ ul.appendChild(li);
924
+ }
925
+ }
926
+
927
+ return ul;
928
+ },
929
+
930
+ addExtensionForms: function () {
931
+ var extensions = this.options.extensions,
932
+ ext,
933
+ name,
934
+ form,
935
+ id;
936
+
937
+ for (name in extensions) {
938
+ if (extensions.hasOwnProperty(name)) {
939
+ ext = extensions[name];
940
+ if (ext.hasForm) {
941
+ form = ext.getForm !== undefined ? ext.getForm() : null;
942
+ }
943
+ if (form) {
944
+ id = 'medium-editor-toolbar-form-' + name + '-' + this.id;
945
+ form.className = 'medium-editor-toolbar-form';
946
+ form.id = id;
947
+ ext.getForm().id = id;
948
+ this.toolbar.appendChild(form);
949
+ }
950
+ }
951
+ }
952
+ },
953
+
954
+ toolbarFormAnchor: function () {
955
+ var anchor = this.options.ownerDocument.createElement('div'),
956
+ input = this.options.ownerDocument.createElement('input'),
957
+ target_label = this.options.ownerDocument.createElement('label'),
958
+ target = this.options.ownerDocument.createElement('input'),
959
+ button_label = this.options.ownerDocument.createElement('label'),
960
+ button = this.options.ownerDocument.createElement('input'),
961
+ close = this.options.ownerDocument.createElement('a'),
962
+ save = this.options.ownerDocument.createElement('a');
963
+
964
+ close.setAttribute('href', '#');
965
+ close.className = 'medium-editor-toobar-close';
966
+ close.innerHTML = '&times;';
967
+
968
+ save.setAttribute('href', '#');
969
+ save.className = 'medium-editor-toobar-save';
970
+ save.innerHTML = '&#10003;';
971
+
972
+ input.setAttribute('type', 'text');
973
+ input.className = 'medium-editor-toolbar-input';
974
+ input.setAttribute('placeholder', this.options.anchorInputPlaceholder);
975
+
976
+
977
+ target.setAttribute('type', 'checkbox');
978
+ target.className = 'medium-editor-toolbar-anchor-target';
979
+ target_label.innerHTML = this.options.anchorInputCheckboxLabel;
980
+ target_label.insertBefore(target, target_label.firstChild);
981
+
982
+ button.setAttribute('type', 'checkbox');
983
+ button.className = 'medium-editor-toolbar-anchor-button';
984
+ button_label.innerHTML = "Button";
985
+ button_label.insertBefore(button, button_label.firstChild);
986
+
987
+
988
+ anchor.className = 'medium-editor-toolbar-form';
989
+ anchor.id = 'medium-editor-toolbar-form-anchor-' + this.id;
990
+ anchor.appendChild(input);
991
+
992
+ anchor.appendChild(save);
993
+ anchor.appendChild(close);
994
+
995
+ if (this.options.anchorTarget) {
996
+ anchor.appendChild(target_label);
997
+ }
998
+
999
+ if (this.options.anchorButton) {
1000
+ anchor.appendChild(button_label);
1001
+ }
1002
+
1003
+ return anchor;
1004
+ },
1005
+
1006
+ bindSelect: function () {
1007
+ var self = this,
1008
+ i;
1009
+
1010
+ this.checkSelectionWrapper = function (e) {
1011
+ // Do not close the toolbar when bluring the editable area and clicking into the anchor form
1012
+ if (!self.options.disableAnchorForm && e && self.clickingIntoArchorForm(e)) {
1013
+ return false;
1014
+ }
1015
+
1016
+ self.checkSelection();
1017
+ };
1018
+
1019
+ this.on(this.options.ownerDocument.documentElement, 'mouseup', this.checkSelectionWrapper);
1020
+
1021
+ for (i = 0; i < this.elements.length; i += 1) {
1022
+ this.on(this.elements[i], 'keyup', this.checkSelectionWrapper);
1023
+ this.on(this.elements[i], 'blur', this.checkSelectionWrapper);
1024
+ this.on(this.elements[i], 'click', this.checkSelectionWrapper);
1025
+ }
1026
+ return this;
1027
+ },
1028
+
1029
+ stopSelectionUpdates: function () {
1030
+ this.preventSelectionUpdates = true;
1031
+ },
1032
+
1033
+ startSelectionUpdates: function () {
1034
+ this.preventSelectionUpdates = false;
1035
+ },
1036
+
1037
+ checkSelection: function () {
1038
+ var newSelection,
1039
+ selectionElement;
1040
+
1041
+ if (!this.preventSelectionUpdates &&
1042
+ this.keepToolbarAlive !== true &&
1043
+ !this.options.disableToolbar) {
1044
+
1045
+ newSelection = this.options.contentWindow.getSelection();
1046
+ if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
1047
+ (this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs()) ||
1048
+ this.selectionInContentEditableFalse()) {
1049
+
1050
+ if (!this.options.staticToolbar) {
1051
+ this.hideToolbarActions();
1052
+ } else if (this.anchorForm && this.anchorForm.style.display === 'block') {
1053
+ this.setToolbarButtonStates();
1054
+ this.showToolbarActions();
1055
+ }
1056
+
1057
+ } else {
1058
+ selectionElement = this.getSelectionElement();
1059
+ if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
1060
+ if (!this.options.staticToolbar) {
1061
+ this.hideToolbarActions();
1062
+ }
1063
+ } else {
1064
+ this.checkSelectionElement(newSelection, selectionElement);
1065
+ }
1066
+ }
1067
+ }
1068
+ return this;
1069
+ },
1070
+
1071
+ clickingIntoArchorForm: function (e) {
1072
+ var self = this;
1073
+
1074
+ if (e.type && e.type.toLowerCase() === 'blur' && e.relatedTarget && e.relatedTarget === self.anchorInput) {
1075
+ return true;
1076
+ }
1077
+
1078
+ return false;
1079
+ },
1080
+
1081
+ hasMultiParagraphs: function () {
1082
+ var selectionHtml = getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
1083
+ hasMultiParagraphs = selectionHtml.match(/<(p|h[0-6]|blockquote)>([\s\S]*?)<\/(p|h[0-6]|blockquote)>/g);
1084
+
1085
+ return (hasMultiParagraphs ? hasMultiParagraphs.length : 0);
1086
+ },
1087
+
1088
+ checkSelectionElement: function (newSelection, selectionElement) {
1089
+ var i,
1090
+ adjacentNode,
1091
+ offset = 0,
1092
+ newRange;
1093
+ this.selection = newSelection;
1094
+ this.selectionRange = this.selection.getRangeAt(0);
1095
+
1096
+ /*
1097
+ * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
1098
+ * will be at the very end of an element. In other browsers, the selectionRange start
1099
+ * would instead be at the very beginning of an element that actually has content.
1100
+ * example:
1101
+ * <span>foo</span><span>bar</span>
1102
+ *
1103
+ * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
1104
+ * of the 'bar' span. However, there are cases where firefox will have the selectionRange start
1105
+ * at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any
1106
+ * properties on the 'bar' span, they won't be reflected accurately in the toolbar
1107
+ * (ie 'Bold' button wouldn't be active)
1108
+ *
1109
+ * So, for cases where the selectionRange start is at the end of an element/node, find the next
1110
+ * adjacent text node that actually has content in it, and move the selectionRange start there.
1111
+ */
1112
+ if (this.options.standardizeSelectionStart &&
1113
+ this.selectionRange.startContainer.nodeValue &&
1114
+ (this.selectionRange.startOffset === this.selectionRange.startContainer.nodeValue.length)) {
1115
+ adjacentNode = findAdjacentTextNodeWithContent(this.getSelectionElement(), this.selectionRange.startContainer, this.options.ownerDocument);
1116
+ if (adjacentNode) {
1117
+ offset = 0;
1118
+ while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
1119
+ offset = offset + 1;
1120
+ }
1121
+ newRange = this.options.ownerDocument.createRange();
1122
+ newRange.setStart(adjacentNode, offset);
1123
+ newRange.setEnd(this.selectionRange.endContainer, this.selectionRange.endOffset);
1124
+ this.selection.removeAllRanges();
1125
+ this.selection.addRange(newRange);
1126
+ this.selectionRange = newRange;
1127
+ }
1128
+ }
1129
+
1130
+ for (i = 0; i < this.elements.length; i += 1) {
1131
+ if (this.elements[i] === selectionElement) {
1132
+ this.setToolbarButtonStates()
1133
+ .setToolbarPosition()
1134
+ .showToolbarActions();
1135
+ return;
1136
+ }
1137
+ }
1138
+
1139
+ if (!this.options.staticToolbar) {
1140
+ this.hideToolbarActions();
1141
+ }
1142
+ },
1143
+
1144
+ findMatchingSelectionParent: function (testElementFunction) {
1145
+ var selection = this.options.contentWindow.getSelection(), range, current;
1146
+
1147
+ if (selection.rangeCount === 0) {
1148
+ return false;
1149
+ }
1150
+
1151
+ range = selection.getRangeAt(0);
1152
+ current = range.commonAncestorContainer;
1153
+
1154
+ do {
1155
+ if (current.nodeType === 1) {
1156
+ if (testElementFunction(current)) {
1157
+ return current;
1158
+ }
1159
+ // do not traverse upwards past the nearest containing editor
1160
+ if (current.getAttribute('data-medium-element')) {
1161
+ return false;
1162
+ }
1163
+ }
1164
+
1165
+ current = current.parentNode;
1166
+ } while (current);
1167
+
1168
+ return false;
1169
+ },
1170
+
1171
+ getSelectionElement: function () {
1172
+ return this.findMatchingSelectionParent(function (el) {
1173
+ return el.getAttribute('data-medium-element');
1174
+ });
1175
+ },
1176
+
1177
+ selectionInContentEditableFalse: function () {
1178
+ return this.findMatchingSelectionParent(function (el) {
1179
+ return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
1180
+ });
1181
+ },
1182
+
1183
+ setToolbarPosition: function () {
1184
+ // document.documentElement for IE 9
1185
+ var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
1186
+ container = this.elements[0],
1187
+ containerRect = container.getBoundingClientRect(),
1188
+ containerTop = containerRect.top + scrollTop,
1189
+ buttonHeight = 50,
1190
+ selection = this.options.contentWindow.getSelection(),
1191
+ range,
1192
+ boundary,
1193
+ middleBoundary,
1194
+ defaultLeft = (this.options.diffLeft) - (this.toolbar.offsetWidth / 2),
1195
+ halfOffsetWidth = this.toolbar.offsetWidth / 2,
1196
+ containerCenter = (containerRect.left + (containerRect.width / 2));
1197
+
1198
+ if (selection.focusNode === null) {
1199
+ return this;
1200
+ }
1201
+
1202
+ this.showToolbar();
1203
+
1204
+ if (this.options.staticToolbar) {
1205
+
1206
+ if (this.options.stickyToolbar) {
1207
+
1208
+ // If it's beyond the height of the editor, position it at the bottom of the editor
1209
+ if (scrollTop > (containerTop + this.elements[0].offsetHeight - this.toolbar.offsetHeight)) {
1210
+ this.toolbar.style.top = (containerTop + this.elements[0].offsetHeight) + 'px';
1211
+
1212
+ // Stick the toolbar to the top of the window
1213
+ } else if (scrollTop > (containerTop - this.toolbar.offsetHeight)) {
1214
+ this.toolbar.classList.add('sticky-toolbar');
1215
+ this.toolbar.style.top = "0px";
1216
+ // Normal static toolbar position
1217
+ } else {
1218
+ this.toolbar.classList.remove('sticky-toolbar');
1219
+ this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
1220
+ }
1221
+
1222
+ } else {
1223
+ this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
1224
+ }
1225
+
1226
+ if (this.options.toolbarAlign) {
1227
+ if (this.options.toolbarAlign === 'left') {
1228
+ this.toolbar.style.left = containerRect.left + "px";
1229
+ } else if (this.options.toolbarAlign === 'center') {
1230
+ this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "px";
1231
+ } else {
1232
+ this.toolbar.style.left = (containerRect.right - this.toolbar.offsetWidth) + "px";
1233
+ }
1234
+ } else {
1235
+ this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "px";
1236
+ }
1237
+
1238
+ } else if (!selection.isCollapsed) {
1239
+ range = selection.getRangeAt(0);
1240
+ boundary = range.getBoundingClientRect();
1241
+ middleBoundary = (boundary.left + boundary.right) / 2;
1242
+
1243
+ if (boundary.top < buttonHeight) {
1244
+ this.toolbar.classList.add('medium-toolbar-arrow-over');
1245
+ this.toolbar.classList.remove('medium-toolbar-arrow-under');
1246
+ this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
1247
+ } else {
1248
+ this.toolbar.classList.add('medium-toolbar-arrow-under');
1249
+ this.toolbar.classList.remove('medium-toolbar-arrow-over');
1250
+ this.toolbar.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
1251
+ }
1252
+ if (middleBoundary < halfOffsetWidth) {
1253
+ this.toolbar.style.left = defaultLeft + halfOffsetWidth + 'px';
1254
+ } else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
1255
+ this.toolbar.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
1256
+ } else {
1257
+ this.toolbar.style.left = defaultLeft + middleBoundary + 'px';
1258
+ }
1259
+ }
1260
+
1261
+ this.hideAnchorPreview();
1262
+
1263
+ return this;
1264
+ },
1265
+
1266
+ setToolbarButtonStates: function () {
1267
+ var buttons = this.toolbarActions.querySelectorAll('button'),
1268
+ i;
1269
+ for (i = 0; i < buttons.length; i += 1) {
1270
+ buttons[i].classList.remove(this.options.activeButtonClass);
1271
+ }
1272
+ this.checkActiveButtons();
1273
+ return this;
1274
+ },
1275
+
1276
+ checkActiveButtons: function () {
1277
+ var elements = Array.prototype.slice.call(this.elements),
1278
+ parentNode = this.getSelectedParentElement();
1279
+ while (parentNode.tagName !== undefined && this.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
1280
+ this.activateButton(parentNode.tagName.toLowerCase());
1281
+ this.callExtensions('checkState', parentNode);
1282
+
1283
+ // we can abort the search upwards if we leave the contentEditable element
1284
+ if (elements.indexOf(parentNode) !== -1) {
1285
+ break;
1286
+ }
1287
+ parentNode = parentNode.parentNode;
1288
+ }
1289
+ },
1290
+
1291
+ activateButton: function (tag) {
1292
+ var el = this.toolbar.querySelector('[data-element="' + tag + '"]');
1293
+ if (el !== null && el.className.indexOf(this.options.activeButtonClass) === -1) {
1294
+ el.className += ' ' + this.options.activeButtonClass;
1295
+ }
1296
+ },
1297
+
1298
+ bindButtons: function () {
1299
+ var buttons = this.toolbar.querySelectorAll('button'),
1300
+ i,
1301
+ self = this,
1302
+ triggerAction = function (e) {
1303
+ e.preventDefault();
1304
+ e.stopPropagation();
1305
+ if (self.selection === undefined) {
1306
+ self.checkSelection();
1307
+ }
1308
+ if (this.className.indexOf(self.options.activeButtonClass) > -1) {
1309
+ this.classList.remove(self.options.activeButtonClass);
1310
+ } else {
1311
+ this.className += ' ' + self.options.activeButtonClass;
1312
+ }
1313
+ if (this.hasAttribute('data-action')) {
1314
+ self.execAction(this.getAttribute('data-action'), e);
1315
+ }
1316
+ // Allows extension buttons to show a form
1317
+ // TO DO: Improve this
1318
+ if (this.hasAttribute('data-form')) {
1319
+ self.showForm(this.getAttribute('data-form'), e);
1320
+ }
1321
+ };
1322
+ for (i = 0; i < buttons.length; i += 1) {
1323
+ this.on(buttons[i], 'click', triggerAction);
1324
+ }
1325
+ this.setFirstAndLastItems(buttons);
1326
+ return this;
1327
+ },
1328
+
1329
+ setFirstAndLastItems: function (buttons) {
1330
+ if (buttons.length > 0) {
1331
+ buttons[0].className += ' ' + this.options.firstButtonClass;
1332
+ buttons[buttons.length - 1].className += ' ' + this.options.lastButtonClass;
1333
+ }
1334
+ return this;
1335
+ },
1336
+
1337
+ execAction: function (action, e) {
1338
+ if (action.indexOf('append-') > -1) {
1339
+ this.execFormatBlock(action.replace('append-', ''));
1340
+ this.setToolbarPosition();
1341
+ this.setToolbarButtonStates();
1342
+ } else if (action === 'anchor') {
1343
+ if (!this.options.disableAnchorForm) {
1344
+ this.triggerAnchorAction(e);
1345
+ }
1346
+ } else if (action === 'image') {
1347
+ this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
1348
+ } else {
1349
+ this.options.ownerDocument.execCommand(action, false, null);
1350
+ this.setToolbarPosition();
1351
+ }
1352
+ },
1353
+
1354
+ // Method to show an extension's form
1355
+ // TO DO: Improve this
1356
+ showForm: function (formId, e) {
1357
+ this.toolbarActions.style.display = 'none';
1358
+ this.saveSelection();
1359
+ var form = document.getElementById(formId);
1360
+ form.style.display = 'block';
1361
+ this.setToolbarPosition();
1362
+ this.keepToolbarAlive = true;
1363
+ },
1364
+
1365
+ // Method to show an extension's form
1366
+ // TO DO: Improve this
1367
+ hideForm: function (form, e) {
1368
+ var el = document.getElementById(form.id);
1369
+ el.style.display = 'none';
1370
+ this.showToolbarActions();
1371
+ this.setToolbarPosition();
1372
+ restoreSelection.call(this, this.savedSelection);
1373
+ },
1374
+
1375
+ // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
1376
+ rangeSelectsSingleNode: function (range) {
1377
+ var startNode = range.startContainer;
1378
+ return startNode === range.endContainer &&
1379
+ startNode.hasChildNodes() &&
1380
+ range.endOffset === range.startOffset + 1;
1381
+ },
1382
+
1383
+ getSelectedParentElement: function () {
1384
+ var selectedParentElement = null,
1385
+ range = this.selectionRange;
1386
+ if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
1387
+ selectedParentElement = range.startContainer.childNodes[range.startOffset];
1388
+ } else if (range.startContainer.nodeType === 3) {
1389
+ selectedParentElement = range.startContainer.parentNode;
1390
+ } else {
1391
+ selectedParentElement = range.startContainer;
1392
+ }
1393
+ return selectedParentElement;
1394
+ },
1395
+
1396
+ triggerAnchorAction: function () {
1397
+ var selectedParentElement = this.getSelectedParentElement();
1398
+ if (selectedParentElement.tagName &&
1399
+ selectedParentElement.tagName.toLowerCase() === 'a') {
1400
+ this.options.ownerDocument.execCommand('unlink', false, null);
1401
+ } else if (this.anchorForm) {
1402
+ if (this.anchorForm.style.display === 'block') {
1403
+ this.showToolbarActions();
1404
+ } else {
1405
+ this.showAnchorForm();
1406
+ }
1407
+ }
1408
+ return this;
1409
+ },
1410
+
1411
+ execFormatBlock: function (el) {
1412
+ var selectionData = this.getSelectionData(this.selection.anchorNode);
1413
+ // FF handles blockquote differently on formatBlock
1414
+ // allowing nesting, we need to use outdent
1415
+ // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
1416
+ if (el === 'blockquote' && selectionData.el &&
1417
+ selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
1418
+ return this.options.ownerDocument.execCommand('outdent', false, null);
1419
+ }
1420
+ if (selectionData.tagName === el) {
1421
+ el = 'p';
1422
+ }
1423
+ // When IE we need to add <> to heading elements and
1424
+ // blockquote needs to be called as indent
1425
+ // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
1426
+ // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
1427
+ if (this.isIE) {
1428
+ if (el === 'blockquote') {
1429
+ return this.options.ownerDocument.execCommand('indent', false, el);
1430
+ }
1431
+ el = '<' + el + '>';
1432
+ }
1433
+ return this.options.ownerDocument.execCommand('formatBlock', false, el);
1434
+ },
1435
+
1436
+ getSelectionData: function (el) {
1437
+ var tagName;
1438
+
1439
+ if (el && el.tagName) {
1440
+ tagName = el.tagName.toLowerCase();
1441
+ }
1442
+
1443
+ while (el && this.parentElements.indexOf(tagName) === -1) {
1444
+ el = el.parentNode;
1445
+ if (el && el.tagName) {
1446
+ tagName = el.tagName.toLowerCase();
1447
+ }
1448
+ }
1449
+
1450
+ return {
1451
+ el: el,
1452
+ tagName: tagName
1453
+ };
1454
+ },
1455
+
1456
+ getFirstChild: function (el) {
1457
+ var firstChild = el.firstChild;
1458
+ while (firstChild !== null && firstChild.nodeType !== 1) {
1459
+ firstChild = firstChild.nextSibling;
1460
+ }
1461
+ return firstChild;
1462
+ },
1463
+
1464
+ isToolbarShown: function () {
1465
+ return this.toolbar && this.toolbar.classList.contains('medium-editor-toolbar-active');
1466
+ },
1467
+
1468
+ showToolbar: function () {
1469
+ if (this.toolbar && !this.isToolbarShown()) {
1470
+ this.toolbar.classList.add('medium-editor-toolbar-active');
1471
+ if (this.onShowToolbar) {
1472
+ this.onShowToolbar();
1473
+ }
1474
+ }
1475
+ },
1476
+
1477
+ hideToolbar: function () {
1478
+ if (this.isToolbarShown()) {
1479
+ this.toolbar.classList.remove('medium-editor-toolbar-active');
1480
+ if (this.onHideToolbar) {
1481
+ this.onHideToolbar();
1482
+ }
1483
+ }
1484
+ },
1485
+
1486
+ hideToolbarActions: function () {
1487
+ this.keepToolbarAlive = false;
1488
+ this.hideToolbar();
1489
+ },
1490
+
1491
+ showToolbarActions: function () {
1492
+ var self = this;
1493
+ if (this.anchorForm) {
1494
+ this.anchorForm.style.display = 'none';
1495
+ }
1496
+ this.toolbarActions.style.display = 'block';
1497
+ this.keepToolbarAlive = false;
1498
+ // Using setTimeout + options.delay because:
1499
+ // We will actually be displaying the toolbar, which should be controlled by options.delay
1500
+ this.delay(function () {
1501
+ self.showToolbar();
1502
+ });
1503
+ },
1504
+
1505
+ saveSelection: function () {
1506
+ this.savedSelection = saveSelection.call(this);
1507
+ },
1508
+
1509
+ restoreSelection: function () {
1510
+ restoreSelection.call(this, this.savedSelection);
1511
+ },
1512
+
1513
+ showAnchorForm: function (link_value) {
1514
+ if (!this.anchorForm) {
1515
+ return;
1516
+ }
1517
+
1518
+ this.toolbarActions.style.display = 'none';
1519
+ this.saveSelection();
1520
+ this.anchorForm.style.display = 'block';
1521
+ this.setToolbarPosition();
1522
+ this.keepToolbarAlive = true;
1523
+ this.anchorInput.focus();
1524
+ this.anchorInput.value = link_value || '';
1525
+ },
1526
+
1527
+ bindAnchorForm: function () {
1528
+ if (!this.anchorForm) {
1529
+ return this;
1530
+ }
1531
+
1532
+ var linkCancel = this.anchorForm.querySelector('a.medium-editor-toobar-close'),
1533
+ linkSave = this.anchorForm.querySelector('a.medium-editor-toobar-save'),
1534
+ self = this;
1535
+
1536
+ this.on(this.anchorForm, 'click', function (e) {
1537
+ e.stopPropagation();
1538
+ self.keepToolbarAlive = true;
1539
+ });
1540
+
1541
+ this.on(this.anchorInput, 'keyup', function (e) {
1542
+ var button = null,
1543
+ target;
1544
+
1545
+ if (e.keyCode === 13) {
1546
+ e.preventDefault();
1547
+ if (self.options.anchorTarget && self.anchorTarget.checked) {
1548
+ target = "_blank";
1549
+ } else {
1550
+ target = "_self";
1551
+ }
1552
+
1553
+ if (self.options.anchorButton && self.anchorButton.checked) {
1554
+ button = self.options.anchorButtonClass;
1555
+ }
1556
+
1557
+ self.createLink(this, target, button);
1558
+ } else if (e.keyCode === 27) {
1559
+ e.preventDefault();
1560
+ self.showToolbarActions();
1561
+ restoreSelection.call(self, self.savedSelection);
1562
+ }
1563
+ });
1564
+
1565
+ this.on(linkSave, 'click', function (e) {
1566
+ var button = null,
1567
+ target;
1568
+ e.preventDefault();
1569
+ if (self.options.anchorTarget && self.anchorTarget.checked) {
1570
+ target = "_blank";
1571
+ } else {
1572
+ target = "_self";
1573
+ }
1574
+
1575
+ if (self.options.anchorButton && self.anchorButton.checked) {
1576
+ button = self.options.anchorButtonClass;
1577
+ }
1578
+
1579
+ self.createLink(self.anchorInput, target, button);
1580
+ }, true);
1581
+
1582
+ this.on(this.anchorInput, 'click', function (e) {
1583
+ // make sure not to hide form when cliking into the input
1584
+ e.stopPropagation();
1585
+ self.keepToolbarAlive = true;
1586
+ });
1587
+
1588
+ // Hide the anchor form when focusing outside of it.
1589
+ this.on(this.options.ownerDocument.body, 'click', function (e) {
1590
+ if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
1591
+ self.keepToolbarAlive = false;
1592
+ self.checkSelection();
1593
+ }
1594
+ }, true);
1595
+ this.on(this.options.ownerDocument.body, 'focus', function (e) {
1596
+ if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
1597
+ self.keepToolbarAlive = false;
1598
+ self.checkSelection();
1599
+ }
1600
+ }, true);
1601
+
1602
+ this.on(linkCancel, 'click', function (e) {
1603
+ e.preventDefault();
1604
+ self.showToolbarActions();
1605
+ restoreSelection.call(self, self.savedSelection);
1606
+ });
1607
+ return this;
1608
+ },
1609
+
1610
+ hideAnchorPreview: function () {
1611
+ this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
1612
+ },
1613
+
1614
+ // TODO: break method
1615
+ showAnchorPreview: function (anchorEl) {
1616
+ if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')
1617
+ || anchorEl.getAttribute('data-disable-preview')) {
1618
+ return true;
1619
+ }
1620
+
1621
+ var self = this,
1622
+ buttonHeight = 40,
1623
+ boundary = anchorEl.getBoundingClientRect(),
1624
+ middleBoundary = (boundary.left + boundary.right) / 2,
1625
+ halfOffsetWidth,
1626
+ defaultLeft;
1627
+
1628
+ self.anchorPreview.querySelector('i').textContent = anchorEl.href;
1629
+ halfOffsetWidth = self.anchorPreview.offsetWidth / 2;
1630
+ defaultLeft = self.options.diffLeft - halfOffsetWidth;
1631
+
1632
+ self.observeAnchorPreview(anchorEl);
1633
+
1634
+ self.anchorPreview.classList.add('medium-toolbar-arrow-over');
1635
+ self.anchorPreview.classList.remove('medium-toolbar-arrow-under');
1636
+ self.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - self.options.diffTop + this.options.contentWindow.pageYOffset - self.anchorPreview.offsetHeight) + 'px';
1637
+ if (middleBoundary < halfOffsetWidth) {
1638
+ self.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
1639
+ } else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
1640
+ self.anchorPreview.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
1641
+ } else {
1642
+ self.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
1643
+ }
1644
+
1645
+ if (this.anchorPreview && !this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
1646
+ this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
1647
+ }
1648
+
1649
+ return this;
1650
+ },
1651
+
1652
+ // TODO: break method
1653
+ observeAnchorPreview: function (anchorEl) {
1654
+ var self = this,
1655
+ lastOver = (new Date()).getTime(),
1656
+ over = true,
1657
+ stamp = function () {
1658
+ lastOver = (new Date()).getTime();
1659
+ over = true;
1660
+ },
1661
+ unstamp = function (e) {
1662
+ if (!e.relatedTarget || !/anchor-preview/.test(e.relatedTarget.className)) {
1663
+ over = false;
1664
+ }
1665
+ },
1666
+ interval_timer = setInterval(function () {
1667
+ if (over) {
1668
+ return true;
1669
+ }
1670
+ var durr = (new Date()).getTime() - lastOver;
1671
+ if (durr > self.options.anchorPreviewHideDelay) {
1672
+ // hide the preview 1/2 second after mouse leaves the link
1673
+ self.hideAnchorPreview();
1674
+
1675
+ // cleanup
1676
+ clearInterval(interval_timer);
1677
+ self.off(self.anchorPreview, 'mouseover', stamp);
1678
+ self.off(self.anchorPreview, 'mouseout', unstamp);
1679
+ self.off(anchorEl, 'mouseover', stamp);
1680
+ self.off(anchorEl, 'mouseout', unstamp);
1681
+
1682
+ }
1683
+ }, 200);
1684
+
1685
+ this.on(self.anchorPreview, 'mouseover', stamp);
1686
+ this.on(self.anchorPreview, 'mouseout', unstamp);
1687
+ this.on(anchorEl, 'mouseover', stamp);
1688
+ this.on(anchorEl, 'mouseout', unstamp);
1689
+ },
1690
+
1691
+ createAnchorPreview: function () {
1692
+ var self = this,
1693
+ anchorPreview = this.options.ownerDocument.createElement('div');
1694
+
1695
+ anchorPreview.id = 'medium-editor-anchor-preview-' + this.id;
1696
+ anchorPreview.className = 'medium-editor-anchor-preview';
1697
+ anchorPreview.innerHTML = this.anchorPreviewTemplate();
1698
+ this.options.elementsContainer.appendChild(anchorPreview);
1699
+
1700
+ this.on(anchorPreview, 'click', function () {
1701
+ self.anchorPreviewClickHandler();
1702
+ });
1703
+
1704
+ return anchorPreview;
1705
+ },
1706
+
1707
+ anchorPreviewTemplate: function () {
1708
+ return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
1709
+ ' <i class="medium-editor-toolbar-anchor-preview-inner"></i>' +
1710
+ '</div>';
1711
+ },
1712
+
1713
+ anchorPreviewClickHandler: function (e) {
1714
+ if (!this.options.disableAnchorForm && this.activeAnchor) {
1715
+
1716
+ var self = this,
1717
+ range = this.options.ownerDocument.createRange(),
1718
+ sel = this.options.contentWindow.getSelection();
1719
+
1720
+ range.selectNodeContents(self.activeAnchor);
1721
+ sel.removeAllRanges();
1722
+ sel.addRange(range);
1723
+ // Using setTimeout + options.delay because:
1724
+ // We may actually be displaying the anchor preview, which should be controlled by options.delay
1725
+ this.delay(function () {
1726
+ if (self.activeAnchor) {
1727
+ self.showAnchorForm(self.activeAnchor.href);
1728
+ }
1729
+ self.keepToolbarAlive = false;
1730
+ });
1731
+
1732
+ }
1733
+
1734
+ this.hideAnchorPreview();
1735
+ },
1736
+
1737
+ editorAnchorObserver: function (e) {
1738
+ var self = this,
1739
+ overAnchor = true,
1740
+ leaveAnchor = function () {
1741
+ // mark the anchor as no longer hovered, and stop listening
1742
+ overAnchor = false;
1743
+ self.off(self.activeAnchor, 'mouseout', leaveAnchor);
1744
+ };
1745
+
1746
+ if (e.target && e.target.tagName.toLowerCase() === 'a') {
1747
+
1748
+ // Detect empty href attributes
1749
+ // The browser will make href="" or href="#top"
1750
+ // into absolute urls when accessed as e.targed.href, so check the html
1751
+ if (!/href=["']\S+["']/.test(e.target.outerHTML) || /href=["']#\S+["']/.test(e.target.outerHTML)) {
1752
+ return true;
1753
+ }
1754
+
1755
+ // only show when hovering on anchors
1756
+ if (this.isToolbarShown()) {
1757
+ // only show when toolbar is not present
1758
+ return true;
1759
+ }
1760
+ this.activeAnchor = e.target;
1761
+ this.on(this.activeAnchor, 'mouseout', leaveAnchor);
1762
+ // Using setTimeout + options.delay because:
1763
+ // - We're going to show the anchor preview according to the configured delay
1764
+ // if the mouse has not left the anchor tag in that time
1765
+ this.delay(function () {
1766
+ if (overAnchor) {
1767
+ self.showAnchorPreview(e.target);
1768
+ }
1769
+ });
1770
+ }
1771
+ },
1772
+
1773
+ bindAnchorPreview: function (index) {
1774
+ var i, self = this;
1775
+ this.editorAnchorObserverWrapper = function (e) {
1776
+ self.editorAnchorObserver(e);
1777
+ };
1778
+ for (i = 0; i < this.elements.length; i += 1) {
1779
+ this.on(this.elements[i], 'mouseover', this.editorAnchorObserverWrapper);
1780
+ }
1781
+ return this;
1782
+ },
1783
+
1784
+ checkLinkFormat: function (value) {
1785
+ var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
1786
+ return (re.test(value) ? '' : 'http://') + value;
1787
+ },
1788
+
1789
+ setTargetBlank: function (el) {
1790
+ var i;
1791
+ el = el || getSelectionStart.call(this);
1792
+ if (el.tagName.toLowerCase() === 'a') {
1793
+ el.target = '_blank';
1794
+ } else {
1795
+ el = el.getElementsByTagName('a');
1796
+
1797
+ for (i = 0; i < el.length; i += 1) {
1798
+ el[i].target = '_blank';
1799
+ }
1800
+ }
1801
+ },
1802
+
1803
+ setButtonClass: function (buttonClass) {
1804
+ var el = getSelectionStart.call(this),
1805
+ classes = buttonClass.split(' '),
1806
+ i,
1807
+ j;
1808
+ if (el.tagName.toLowerCase() === 'a') {
1809
+ for (j = 0; j < classes.length; j += 1) {
1810
+ el.classList.add(classes[j]);
1811
+ }
1812
+ } else {
1813
+ el = el.getElementsByTagName('a');
1814
+ for (i = 0; i < el.length; i += 1) {
1815
+ for (j = 0; j < classes.length; j += 1) {
1816
+ el[i].classList.add(classes[j]);
1817
+ }
1818
+ }
1819
+ }
1820
+ },
1821
+
1822
+ createLink: function (input, target, buttonClass) {
1823
+ var i, event;
1824
+
1825
+ if (input.value.trim().length === 0) {
1826
+ this.hideToolbarActions();
1827
+ return;
1828
+ }
1829
+
1830
+ restoreSelection.call(this, this.savedSelection);
1831
+
1832
+ if (this.options.checkLinkFormat) {
1833
+ input.value = this.checkLinkFormat(input.value);
1834
+ }
1835
+
1836
+ this.options.ownerDocument.execCommand('createLink', false, input.value);
1837
+
1838
+ if (this.options.targetBlank || target === "_blank") {
1839
+ this.setTargetBlank();
1840
+ }
1841
+
1842
+ if (buttonClass) {
1843
+ this.setButtonClass(buttonClass);
1844
+ }
1845
+
1846
+ if (this.options.targetBlank || target === "_blank" || buttonClass) {
1847
+ event = this.options.ownerDocument.createEvent("HTMLEvents");
1848
+ event.initEvent("input", true, true, this.options.contentWindow);
1849
+ for (i = 0; i < this.elements.length; i += 1) {
1850
+ this.elements[i].dispatchEvent(event);
1851
+ }
1852
+ }
1853
+
1854
+ this.checkSelection();
1855
+ this.showToolbarActions();
1856
+ input.value = '';
1857
+ },
1858
+
1859
+ positionToolbarIfShown: function () {
1860
+ if (this.isToolbarShown()) {
1861
+ this.setToolbarPosition();
1862
+ }
1863
+ },
1864
+
1865
+ bindWindowActions: function () {
1866
+ var self = this;
1867
+
1868
+ // Add a scroll event for sticky toolbar
1869
+ if (this.options.staticToolbar && this.options.stickyToolbar) {
1870
+ // On scroll, re-position the toolbar
1871
+ this.on(this.options.contentWindow, 'scroll', function () {
1872
+ self.positionToolbarIfShown();
1873
+ }, true);
1874
+ }
1875
+
1876
+ this.on(this.options.contentWindow, 'resize', function () {
1877
+ self.handleResize();
1878
+ });
1879
+ return this;
1880
+ },
1881
+
1882
+ activate: function () {
1883
+ if (this.isActive) {
1884
+ return;
1885
+ }
1886
+
1887
+ this.setup();
1888
+ },
1889
+
1890
+ // TODO: break method
1891
+ deactivate: function () {
1892
+ var i;
1893
+ if (!this.isActive) {
1894
+ return;
1895
+ }
1896
+ this.isActive = false;
1897
+
1898
+ if (this.toolbar !== undefined) {
1899
+ this.options.elementsContainer.removeChild(this.anchorPreview);
1900
+ this.options.elementsContainer.removeChild(this.toolbar);
1901
+ delete this.toolbar;
1902
+ delete this.anchorPreview;
1903
+ }
1904
+
1905
+ for (i = 0; i < this.elements.length; i += 1) {
1906
+ this.elements[i].removeAttribute('contentEditable');
1907
+ this.elements[i].removeAttribute('data-medium-element');
1908
+ }
1909
+
1910
+ this.removeAllEvents();
1911
+ },
1912
+
1913
+ htmlEntities: function (str) {
1914
+ // converts special characters (like <) into their escaped/encoded values (like &lt;).
1915
+ // This allows you to show to display the string without the browser reading it as HTML.
1916
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1917
+ },
1918
+
1919
+ bindPaste: function () {
1920
+ var i, self = this;
1921
+ this.pasteWrapper = function (e) {
1922
+ var paragraphs,
1923
+ html = '',
1924
+ p,
1925
+ dataFormatHTML = 'text/html',
1926
+ dataFormatPlain = 'text/plain';
1927
+
1928
+ this.classList.remove('medium-editor-placeholder');
1929
+ if (!self.options.forcePlainText && !self.options.cleanPastedHTML) {
1930
+ return this;
1931
+ }
1932
+
1933
+ if (self.options.contentWindow.clipboardData && e.clipboardData === undefined) {
1934
+ e.clipboardData = self.options.contentWindow.clipboardData;
1935
+ // If window.clipboardData exists, but e.clipboardData doesn't exist,
1936
+ // we're probably in IE. IE only has two possibilities for clipboard
1937
+ // data format: 'Text' and 'URL'.
1938
+ //
1939
+ // Of the two, we want 'Text':
1940
+ dataFormatHTML = 'Text';
1941
+ dataFormatPlain = 'Text';
1942
+ }
1943
+
1944
+ if (e.clipboardData && e.clipboardData.getData && !e.defaultPrevented) {
1945
+ e.preventDefault();
1946
+
1947
+ if (self.options.cleanPastedHTML && e.clipboardData.getData(dataFormatHTML)) {
1948
+ return self.cleanPaste(e.clipboardData.getData(dataFormatHTML));
1949
+ }
1950
+ if (!(self.options.disableReturn || this.getAttribute('data-disable-return'))) {
1951
+ paragraphs = e.clipboardData.getData(dataFormatPlain).split(/[\r\n]/g);
1952
+ for (p = 0; p < paragraphs.length; p += 1) {
1953
+ if (paragraphs[p] !== '') {
1954
+ if (navigator.userAgent.match(/firefox/i) && p === 0) {
1955
+ html += self.htmlEntities(paragraphs[p]);
1956
+ } else {
1957
+ html += '<p>' + self.htmlEntities(paragraphs[p]) + '</p>';
1958
+ }
1959
+ }
1960
+ }
1961
+ insertHTMLCommand(self.options.ownerDocument, html);
1962
+ } else {
1963
+ html = self.htmlEntities(e.clipboardData.getData(dataFormatPlain));
1964
+ insertHTMLCommand(self.options.ownerDocument, html);
1965
+ }
1966
+ }
1967
+ };
1968
+ for (i = 0; i < this.elements.length; i += 1) {
1969
+ this.on(this.elements[i], 'paste', this.pasteWrapper);
1970
+ }
1971
+ return this;
1972
+ },
1973
+
1974
+ setPlaceholders: function () {
1975
+ if (!this.options.disablePlaceholders && this.elements && this.elements.length) {
1976
+ this.elements.forEach(function (el) {
1977
+ this.activatePlaceholder(el);
1978
+ this.on(el, 'blur', this.placeholderWrapper.bind(this));
1979
+ this.on(el, 'keypress', this.placeholderWrapper.bind(this));
1980
+ }.bind(this));
1981
+ }
1982
+
1983
+ return this;
1984
+ },
1985
+
1986
+ cleanPaste: function (text) {
1987
+
1988
+ /*jslint regexp: true*/
1989
+ /*
1990
+ jslint does not allow character negation, because the negation
1991
+ will not match any unicode characters. In the regexes in this
1992
+ block, negation is used specifically to match the end of an html
1993
+ tag, and in fact unicode characters *should* be allowed.
1994
+ */
1995
+ var i, elList, workEl,
1996
+ el = this.getSelectionElement(),
1997
+ multiline = /<p|<br|<div/.test(text),
1998
+ replacements = [
1999
+
2000
+ // replace two bogus tags that begin pastes from google docs
2001
+ [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ""],
2002
+ [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ""],
2003
+
2004
+ // un-html spaces and newlines inserted by OS X
2005
+ [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
2006
+ [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
2007
+
2008
+ // replace google docs italics+bold with a span to be replaced once the html is inserted
2009
+ [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
2010
+
2011
+ // replace google docs italics with a span to be replaced once the html is inserted
2012
+ [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
2013
+
2014
+ //[replace google docs bolds with a span to be replaced once the html is inserted
2015
+ [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
2016
+
2017
+ // replace manually entered b/i/a tags with real ones
2018
+ [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
2019
+
2020
+ // replace manually a tags with real ones, converting smart-quotes from google docs
2021
+ [new RegExp(/&lt;a\s+href=(&quot;|&rdquo;|&ldquo;|“|”)([^&]+)(&quot;|&rdquo;|&ldquo;|“|”)&gt;/gi), '<a href="$2">']
2022
+
2023
+ ];
2024
+ /*jslint regexp: false*/
2025
+
2026
+ for (i = 0; i < replacements.length; i += 1) {
2027
+ text = text.replace(replacements[i][0], replacements[i][1]);
2028
+ }
2029
+
2030
+ if (multiline) {
2031
+
2032
+ // double br's aren't converted to p tags, but we want paragraphs.
2033
+ elList = text.split('<br><br>');
2034
+
2035
+ this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>');
2036
+ this.options.ownerDocument.execCommand('insertText', false, "\n");
2037
+
2038
+ // block element cleanup
2039
+ elList = el.querySelectorAll('a,p,div,br');
2040
+ for (i = 0; i < elList.length; i += 1) {
2041
+
2042
+ workEl = elList[i];
2043
+
2044
+ switch (workEl.tagName.toLowerCase()) {
2045
+ case 'a':
2046
+ if (this.options.targetBlank) {
2047
+ this.setTargetBlank(workEl);
2048
+ }
2049
+ break;
2050
+ case 'p':
2051
+ case 'div':
2052
+ this.filterCommonBlocks(workEl);
2053
+ break;
2054
+ case 'br':
2055
+ this.filterLineBreak(workEl);
2056
+ break;
2057
+ }
2058
+
2059
+ }
2060
+
2061
+
2062
+ } else {
2063
+
2064
+ this.pasteHTML(text);
2065
+
2066
+ }
2067
+
2068
+ },
2069
+
2070
+ pasteHTML: function (html) {
2071
+ var elList, workEl, i, fragmentBody, pasteBlock = this.options.ownerDocument.createDocumentFragment();
2072
+
2073
+ pasteBlock.appendChild(this.options.ownerDocument.createElement('body'));
2074
+
2075
+ fragmentBody = pasteBlock.querySelector('body');
2076
+ fragmentBody.innerHTML = html;
2077
+
2078
+ this.cleanupSpans(fragmentBody);
2079
+
2080
+ elList = fragmentBody.querySelectorAll('*');
2081
+ for (i = 0; i < elList.length; i += 1) {
2082
+
2083
+ workEl = elList[i];
2084
+
2085
+ // delete ugly attributes
2086
+ workEl.removeAttribute('class');
2087
+ workEl.removeAttribute('style');
2088
+ workEl.removeAttribute('dir');
2089
+
2090
+ if (workEl.tagName.toLowerCase() === 'meta') {
2091
+ workEl.parentNode.removeChild(workEl);
2092
+ }
2093
+
2094
+ }
2095
+ this.options.ownerDocument.execCommand('insertHTML', false, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
2096
+ },
2097
+ isCommonBlock: function (el) {
2098
+ return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
2099
+ },
2100
+ filterCommonBlocks: function (el) {
2101
+ if (/^\s*$/.test(el.textContent)) {
2102
+ el.parentNode.removeChild(el);
2103
+ }
2104
+ },
2105
+ filterLineBreak: function (el) {
2106
+ if (this.isCommonBlock(el.previousElementSibling)) {
2107
+
2108
+ // remove stray br's following common block elements
2109
+ el.parentNode.removeChild(el);
2110
+
2111
+ } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
2112
+
2113
+ // remove br's just inside open or close tags of a div/p
2114
+ el.parentNode.removeChild(el);
2115
+
2116
+ } else if (el.parentNode.childElementCount === 1) {
2117
+
2118
+ // and br's that are the only child of a div/p
2119
+ this.removeWithParent(el);
2120
+
2121
+ }
2122
+
2123
+ },
2124
+
2125
+ // remove an element, including its parent, if it is the only element within its parent
2126
+ removeWithParent: function (el) {
2127
+ if (el && el.parentNode) {
2128
+ if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
2129
+ el.parentNode.parentNode.removeChild(el.parentNode);
2130
+ } else {
2131
+ el.parentNode.removeChild(el.parentNode);
2132
+ }
2133
+ }
2134
+ },
2135
+
2136
+ cleanupSpans: function (container_el) {
2137
+
2138
+ var i,
2139
+ el,
2140
+ new_el,
2141
+ spans = container_el.querySelectorAll('.replace-with');
2142
+
2143
+ for (i = 0; i < spans.length; i += 1) {
2144
+
2145
+ el = spans[i];
2146
+ new_el = this.options.ownerDocument.createElement(el.classList.contains('bold') ? 'b' : 'i');
2147
+
2148
+ if (el.classList.contains('bold') && el.classList.contains('italic')) {
2149
+
2150
+ // add an i tag as well if this has both italics and bold
2151
+ new_el.innerHTML = '<i>' + el.innerHTML + '</i>';
2152
+
2153
+ } else {
2154
+
2155
+ new_el.innerHTML = el.innerHTML;
2156
+
2157
+ }
2158
+ el.parentNode.replaceChild(new_el, el);
2159
+
2160
+ }
2161
+
2162
+ spans = container_el.querySelectorAll('span');
2163
+ for (i = 0; i < spans.length; i += 1) {
2164
+
2165
+ el = spans[i];
2166
+
2167
+ // remove empty spans, replace others with their contents
2168
+ if (/^\s*$/.test()) {
2169
+ el.parentNode.removeChild(el);
2170
+ } else {
2171
+ el.parentNode.replaceChild(this.options.ownerDocument.createTextNode(el.textContent), el);
2172
+ }
2173
+
2174
+ }
2175
+
2176
+ }
2177
+
2178
+ };
2179
+
2180
+ }(window, document));