scrivito-medium-editor 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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));