tenon 1.0.52 → 1.0.53

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,1414 @@
1
+ // forked from medium v. 0.9.3
2
+ function MediumEditor(elements, options) {
3
+ 'use strict';
4
+ return this.init(elements, options);
5
+ }
6
+
7
+ if (typeof module === 'object') {
8
+ module.exports = MediumEditor;
9
+ }
10
+
11
+ (function (window, document) {
12
+ 'use strict';
13
+
14
+ function extend(b, a) {
15
+ var prop;
16
+ if (b === undefined) {
17
+ return a;
18
+ }
19
+ for (prop in a) {
20
+ if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
21
+ b[prop] = a[prop];
22
+ }
23
+ }
24
+ return b;
25
+ }
26
+
27
+ // http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element
28
+ // by Tim Down
29
+ function saveSelection() {
30
+ var i,
31
+ len,
32
+ ranges,
33
+ sel = window.getSelection();
34
+ if (sel.getRangeAt && sel.rangeCount) {
35
+ ranges = [];
36
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
37
+ ranges.push(sel.getRangeAt(i));
38
+ }
39
+ return ranges;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function restoreSelection(savedSel) {
45
+ var i,
46
+ len,
47
+ sel = window.getSelection();
48
+
49
+ if (savedSel) {
50
+ sel.removeAllRanges();
51
+ for (i = 0, len = savedSel.length; i < len; i += 1) {
52
+ sel.addRange(savedSel[i]);
53
+ }
54
+ }
55
+ }
56
+
57
+ // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
58
+ // by You
59
+ function getSelectionStart() {
60
+ var node = document.getSelection().anchorNode,
61
+ startNode = (node && node.nodeType === 3 ? node.parentNode : node);
62
+ return startNode;
63
+ }
64
+
65
+ // http://stackoverflow.com/questions/4176923/html-of-selected-text
66
+ // by Tim Down
67
+ function getSelectionHtml() {
68
+ var i,
69
+ html = '',
70
+ sel,
71
+ len,
72
+ container;
73
+ if (window.getSelection !== undefined) {
74
+ sel = window.getSelection();
75
+ if (sel.rangeCount) {
76
+ container = document.createElement('div');
77
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
78
+ container.appendChild(sel.getRangeAt(i).cloneContents());
79
+ }
80
+ html = container.innerHTML;
81
+ }
82
+ } else if (document.selection !== undefined) {
83
+ if (document.selection.type === 'Text') {
84
+ html = document.selection.createRange().htmlText;
85
+ }
86
+ }
87
+ return html;
88
+ }
89
+
90
+ // https://github.com/jashkenas/underscore
91
+ function isElement(obj) {
92
+ return !!(obj && obj.nodeType === 1);
93
+ }
94
+
95
+ MediumEditor.prototype = {
96
+ defaults: {
97
+ allowMultiParagraphSelection: true,
98
+ anchorInputPlaceholder: 'Paste or type a link',
99
+ anchorPreviewHideDelay: 500,
100
+ buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
101
+ buttonLabels: false,
102
+ checkLinkFormat: false,
103
+ cleanPastedHTML: false,
104
+ delay: 0,
105
+ diffLeft: 0,
106
+ diffTop: -10,
107
+ disableReturn: false,
108
+ disableDoubleReturn: false,
109
+ disableToolbar: false,
110
+ disableEditing: false,
111
+ elementsContainer: false,
112
+ firstHeader: 'h3',
113
+ forcePlainText: true,
114
+ placeholder: 'Type your text',
115
+ secondHeader: 'h4',
116
+ targetBlank: false,
117
+ extensions: {}
118
+ },
119
+
120
+ // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
121
+ // by rg89
122
+ isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
123
+
124
+ init: function (elements, options) {
125
+ this.setElementSelection(elements);
126
+ if (this.elements.length === 0) {
127
+ return;
128
+ }
129
+ this.parentElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'];
130
+ this.id = document.querySelectorAll('.medium-editor-toolbar').length + 1;
131
+ this.options = extend(options, this.defaults);
132
+ return this.setup();
133
+ },
134
+
135
+ setup: function () {
136
+ this.isActive = true;
137
+ this.initElements()
138
+ .bindSelect()
139
+ .bindPaste()
140
+ .setPlaceholders()
141
+ .bindWindowActions();
142
+ },
143
+
144
+ initElements: function () {
145
+ this.updateElementList();
146
+ var i,
147
+ addToolbar = false;
148
+ for (i = 0; i < this.elements.length; i += 1) {
149
+ if (!this.options.disableEditing && !this.elements[i].getAttribute('data-disable-editing')) {
150
+ this.elements[i].setAttribute('contentEditable', true);
151
+ }
152
+ if (!this.elements[i].getAttribute('data-placeholder')) {
153
+ this.elements[i].setAttribute('data-placeholder', this.options.placeholder);
154
+ }
155
+ this.elements[i].setAttribute('data-medium-element', true);
156
+ this.bindParagraphCreation(i).bindReturn(i).bindTab(i);
157
+ if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) {
158
+ addToolbar = true;
159
+ }
160
+ }
161
+ // Init toolbar
162
+ if (addToolbar) {
163
+ if (!this.options.elementsContainer) {
164
+ this.options.elementsContainer = document.body;
165
+ }
166
+ this.initToolbar()
167
+ .bindButtons()
168
+ .bindAnchorForm()
169
+ .bindAnchorPreview();
170
+ }
171
+ return this;
172
+ },
173
+
174
+ setElementSelection: function (selector) {
175
+ this.elementSelection = selector;
176
+ this.updateElementList();
177
+ },
178
+
179
+ updateElementList: function () {
180
+ this.elements = typeof this.elementSelection === 'string' ? document.querySelectorAll(this.elementSelection) : this.elementSelection;
181
+ if (this.elements.nodeType === 1) {
182
+ this.elements = [this.elements];
183
+ }
184
+ },
185
+
186
+ serialize: function () {
187
+ var i,
188
+ elementid,
189
+ content = {};
190
+ for (i = 0; i < this.elements.length; i += 1) {
191
+ elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
192
+ content[elementid] = {
193
+ value: this.elements[i].innerHTML.trim()
194
+ };
195
+ }
196
+ return content;
197
+ },
198
+
199
+ /**
200
+ * Helper function to call a method with a number of parameters on all registered extensions.
201
+ * The function assures that the function exists before calling.
202
+ *
203
+ * @param {string} funcName name of the function to call
204
+ * @param [args] arguments passed into funcName
205
+ */
206
+ callExtensions: function (funcName) {
207
+ if (arguments.length < 1) {
208
+ return;
209
+ }
210
+
211
+ var args = Array.prototype.slice.call(arguments, 1),
212
+ ext,
213
+ name;
214
+
215
+ for (name in this.options.extensions) {
216
+ if (this.options.extensions.hasOwnProperty(name)) {
217
+ ext = this.options.extensions[name];
218
+ if (ext[funcName] !== undefined) {
219
+ ext[funcName].apply(ext, args);
220
+ }
221
+ }
222
+ }
223
+ },
224
+
225
+ bindParagraphCreation: function (index) {
226
+ var self = this;
227
+ this.elements[index].addEventListener('keypress', function (e) {
228
+ var node = getSelectionStart(),
229
+ tagName;
230
+ if (e.which === 32) {
231
+ tagName = node.tagName.toLowerCase();
232
+ if (tagName === 'a') {
233
+ document.execCommand('unlink', false, null);
234
+ }
235
+ }
236
+ self.triggerChange();
237
+ });
238
+
239
+ this.elements[index].addEventListener('keyup', function (e) {
240
+ var node = getSelectionStart(),
241
+ tagName;
242
+ if (node && node.getAttribute('data-medium-element') && node.children.length === 0 && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
243
+ document.execCommand('formatBlock', false, 'p');
244
+ }
245
+ if (e.which === 13) {
246
+ node = getSelectionStart();
247
+ tagName = node.tagName.toLowerCase();
248
+ if (!(self.options.disableReturn || this.getAttribute('data-disable-return')) &&
249
+ tagName !== 'li' && !self.isListItemChild(node)) {
250
+ if (!e.shiftKey) {
251
+ document.execCommand('formatBlock', false, 'p');
252
+ }
253
+ if (tagName === 'a') {
254
+ document.execCommand('unlink', false, null);
255
+ }
256
+ }
257
+ }
258
+ self.triggerChange();
259
+ });
260
+
261
+ return this;
262
+ },
263
+
264
+ isListItemChild: function (node) {
265
+ var parentNode = node.parentNode,
266
+ tagName = parentNode.tagName.toLowerCase();
267
+ while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
268
+ if (tagName === 'li') {
269
+ return true;
270
+ }
271
+ parentNode = parentNode.parentNode;
272
+ if (parentNode && parentNode.tagName) {
273
+ tagName = parentNode.tagName.toLowerCase();
274
+ } else {
275
+ return false;
276
+ }
277
+ }
278
+ return false;
279
+ },
280
+
281
+ bindReturn: function (index) {
282
+ var self = this;
283
+ this.elements[index].addEventListener('keypress', function (e) {
284
+ if (e.which === 13) {
285
+ if (self.options.disableReturn || this.getAttribute('data-disable-return')) {
286
+ e.preventDefault();
287
+ } else if (self.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
288
+ var node = getSelectionStart();
289
+ if (node && node.innerText === '\n') {
290
+ e.preventDefault();
291
+ }
292
+ }
293
+ }
294
+ });
295
+ return this;
296
+ },
297
+
298
+ bindTab: function (index) {
299
+ this.elements[index].addEventListener('keydown', function (e) {
300
+ if (e.which === 9) {
301
+ // Override tab only for pre nodes
302
+ var tag = getSelectionStart().tagName.toLowerCase();
303
+ if (tag === 'pre') {
304
+ e.preventDefault();
305
+ document.execCommand('insertHtml', null, ' ');
306
+ this.triggerChange();
307
+ }
308
+ }
309
+ });
310
+ return this;
311
+ },
312
+
313
+ buttonTemplate: function (btnType) {
314
+ var buttonLabels = this.getButtonLabels(this.options.buttonLabels),
315
+ buttonTemplates = {
316
+ 'bold': '<button class="medium-editor-action medium-editor-action-bold" data-action="bold" data-element="b">' + buttonLabels.bold + '</button>',
317
+ 'italic': '<button class="medium-editor-action medium-editor-action-italic" data-action="italic" data-element="i">' + buttonLabels.italic + '</button>',
318
+ 'underline': '<button class="medium-editor-action medium-editor-action-underline" data-action="underline" data-element="u">' + buttonLabels.underline + '</button>',
319
+ 'strikethrough': '<button class="medium-editor-action medium-editor-action-strikethrough" data-action="strikethrough" data-element="strike"><strike>A</strike></button>',
320
+ 'superscript': '<button class="medium-editor-action medium-editor-action-superscript" data-action="superscript" data-element="sup">' + buttonLabels.superscript + '</button>',
321
+ 'subscript': '<button class="medium-editor-action medium-editor-action-subscript" data-action="subscript" data-element="sub">' + buttonLabels.subscript + '</button>',
322
+ 'anchor': '<button class="medium-editor-action medium-editor-action-anchor" data-action="anchor" data-element="a">' + buttonLabels.anchor + '</button>',
323
+ 'image': '<button class="medium-editor-action medium-editor-action-image" data-action="image" data-element="img">' + buttonLabels.image + '</button>',
324
+ 'header1': '<button class="medium-editor-action medium-editor-action-header1" data-action="append-' + this.options.firstHeader + '" data-element="' + this.options.firstHeader + '">' + buttonLabels.header1 + '</button>',
325
+ 'header2': '<button class="medium-editor-action medium-editor-action-header2" data-action="append-' + this.options.secondHeader + '" data-element="' + this.options.secondHeader + '">' + buttonLabels.header2 + '</button>',
326
+ 'quote': '<button class="medium-editor-action medium-editor-action-quote" data-action="append-blockquote" data-element="blockquote">' + buttonLabels.quote + '</button>',
327
+ 'orderedlist': '<button class="medium-editor-action medium-editor-action-orderedlist" data-action="insertorderedlist" data-element="ol">' + buttonLabels.orderedlist + '</button>',
328
+ 'unorderedlist': '<button class="medium-editor-action medium-editor-action-unorderedlist" data-action="insertunorderedlist" data-element="ul">' + buttonLabels.unorderedlist + '</button>',
329
+ 'pre': '<button class="medium-editor-action medium-editor-action-pre" data-action="append-pre" data-element="pre">' + buttonLabels.pre + '</button>',
330
+ 'indent': '<button class="medium-editor-action medium-editor-action-indent" data-action="indent" data-element="ul">' + buttonLabels.indent + '</button>',
331
+ 'outdent': '<button class="medium-editor-action medium-editor-action-outdent" data-action="outdent" data-element="ul">' + buttonLabels.outdent + '</button>'
332
+ };
333
+ return buttonTemplates[btnType] || false;
334
+ },
335
+
336
+ // TODO: break method
337
+ getButtonLabels: function (buttonLabelType) {
338
+ var customButtonLabels,
339
+ attrname,
340
+ buttonLabels = {
341
+ 'bold': '<b>B</b>',
342
+ 'italic': '<b><i>I</i></b>',
343
+ 'underline': '<b><u>U</u></b>',
344
+ 'superscript': '<b>x<sup>1</sup></b>',
345
+ 'subscript': '<b>x<sub>1</sub></b>',
346
+ 'anchor': '<b>#</b>',
347
+ 'image': '<b>image</b>',
348
+ 'header1': '<b>H1</b>',
349
+ 'header2': '<b>H2</b>',
350
+ 'quote': '<b>&ldquo;</b>',
351
+ 'orderedlist': '<b>1.</b>',
352
+ 'unorderedlist': '<b>&bull;</b>',
353
+ 'pre': '<b>0101</b>',
354
+ 'indent': '<b>&rarr;</b>',
355
+ 'outdent': '<b>&larr;</b>'
356
+ };
357
+ if (buttonLabelType === 'fontawesome') {
358
+ customButtonLabels = {
359
+ 'bold': '<i class="fa fa-bold"></i>',
360
+ 'italic': '<i class="fa fa-italic"></i>',
361
+ 'underline': '<i class="fa fa-underline"></i>',
362
+ 'superscript': '<i class="fa fa-superscript"></i>',
363
+ 'subscript': '<i class="fa fa-subscript"></i>',
364
+ 'anchor': '<i class="fa fa-link"></i>',
365
+ 'image': '<i class="fa fa-picture-o"></i>',
366
+ 'quote': '<i class="fa fa-quote-right"></i>',
367
+ 'orderedlist': '<i class="fa fa-list-ol"></i>',
368
+ 'unorderedlist': '<i class="fa fa-list-ul"></i>',
369
+ 'pre': '<i class="fa fa-code fa-lg"></i>',
370
+ 'indent': '<i class="fa fa-indent"></i>',
371
+ 'outdent': '<i class="fa fa-outdent"></i>'
372
+ };
373
+ } else if (typeof buttonLabelType === 'object') {
374
+ customButtonLabels = buttonLabelType;
375
+ }
376
+ if (typeof customButtonLabels === 'object') {
377
+ for (attrname in customButtonLabels) {
378
+ if (customButtonLabels.hasOwnProperty(attrname)) {
379
+ buttonLabels[attrname] = customButtonLabels[attrname];
380
+ }
381
+ }
382
+ }
383
+ return buttonLabels;
384
+ },
385
+
386
+ initToolbar: function () {
387
+ if (this.toolbar) {
388
+ return this;
389
+ }
390
+ this.toolbar = this.createToolbar();
391
+ this.keepToolbarAlive = false;
392
+ this.anchorForm = this.toolbar.querySelector('.medium-editor-toolbar-form-anchor');
393
+ this.anchorInput = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-input');
394
+ this.anchorTarget = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-target');
395
+ this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions');
396
+ this.anchorPreview = this.createAnchorPreview();
397
+
398
+ return this;
399
+ },
400
+
401
+ createToolbar: function () {
402
+ var toolbar = document.createElement('div');
403
+ toolbar.id = 'medium-editor-toolbar-' + this.id;
404
+ toolbar.className = 'medium-editor-toolbar';
405
+ toolbar.appendChild(this.toolbarButtons());
406
+ toolbar.appendChild(this.toolbarFormAnchor());
407
+ this.options.elementsContainer.appendChild(toolbar);
408
+ return toolbar;
409
+ },
410
+
411
+ //TODO: actionTemplate
412
+ toolbarButtons: function () {
413
+ var btns = this.options.buttons,
414
+ ul = document.createElement('ul'),
415
+ li,
416
+ i,
417
+ btn,
418
+ ext;
419
+
420
+ ul.id = 'medium-editor-toolbar-actions';
421
+ ul.className = 'medium-editor-toolbar-actions clearfix';
422
+
423
+ for (i = 0; i < btns.length; i += 1) {
424
+ if (this.options.extensions.hasOwnProperty(btns[i])) {
425
+ ext = this.options.extensions[btns[i]];
426
+ btn = ext.getButton !== undefined ? ext.getButton() : null;
427
+ } else {
428
+ btn = this.buttonTemplate(btns[i]);
429
+ }
430
+
431
+ if (btn) {
432
+ li = document.createElement('li');
433
+ if (isElement(btn)) {
434
+ li.appendChild(btn);
435
+ } else {
436
+ li.innerHTML = btn;
437
+ }
438
+ ul.appendChild(li);
439
+ }
440
+ }
441
+
442
+ return ul;
443
+ },
444
+
445
+ toolbarFormAnchor: function () {
446
+ var anchor = document.createElement('div'),
447
+ input = document.createElement('input'),
448
+ cancel = document.createElement('a'),
449
+ asset_button = document.createElement('a'),
450
+ target_wrap = document.createElement('div'),
451
+ target_label = document.createElement('label'),
452
+ target = document.createElement('input'),
453
+ icon = document.createElement('i');
454
+
455
+ cancel.setAttribute('href', '#');
456
+ cancel.className = 'medium-editor-cancel';
457
+
458
+ asset_button.className = 'medium-editor-link-to-asset';
459
+ asset_button.setAttribute('href', '/tenon/item_assets/new?hide_upload=true');
460
+ asset_button.setAttribute('data-modal-remote', 'true');
461
+ asset_button.setAttribute('data-tooltip', 'true');
462
+ asset_button.setAttribute('title', 'Link to an Asset');
463
+ asset_button.setAttribute('data-modal-title', 'Link to Asset');
464
+ asset_button.setAttribute('data-modal-handler', 'Tenon.features.tenonContent.AssetLink');
465
+
466
+ target.className = 'medium-editor-toolbar-anchor-target';
467
+ target.setAttribute('type', 'checkbox')
468
+ target.setAttribute('title', 'Open in New Window?')
469
+ target.setAttribute('data-tooltip', 'true');
470
+ target_label.className = 'medium-editor-toolbar-anchor-target-label';
471
+ target_wrap.className = 'medium-editor-toolbar-anchor-target-wrap';
472
+ target_wrap.insertBefore(target_label, target_wrap.firstChild);
473
+ target_wrap.insertBefore(target, target_wrap.firstChild);
474
+
475
+ input.className = 'medium-editor-toolbar-anchor-input';
476
+ input.setAttribute('type', 'text');
477
+ input.setAttribute('placeholder', this.options.anchorInputPlaceholder);
478
+
479
+ anchor.className = 'medium-editor-toolbar-form-anchor';
480
+ anchor.id = 'medium-editor-toolbar-form-anchor';
481
+ if (!this.isIE) { anchor.appendChild(asset_button); }
482
+ anchor.appendChild(input);
483
+ anchor.appendChild(target_wrap);
484
+ anchor.appendChild(cancel);
485
+
486
+ return anchor;
487
+ },
488
+
489
+ bindSelect: function () {
490
+ var self = this,
491
+ timer = '',
492
+ i;
493
+
494
+ this.checkSelectionWrapper = function (e) {
495
+
496
+ // Do not close the toolbar when bluring the editable area and clicking into the anchor form
497
+ if (e && self.clickingIntoArchorForm(e)) {
498
+ return false;
499
+ }
500
+
501
+ clearTimeout(timer);
502
+ timer = setTimeout(function () {
503
+ self.checkSelection();
504
+ }, self.options.delay);
505
+ };
506
+
507
+ document.documentElement.addEventListener('mouseup', this.checkSelectionWrapper);
508
+
509
+ for (i = 0; i < this.elements.length; i += 1) {
510
+ this.elements[i].addEventListener('keyup', this.checkSelectionWrapper);
511
+ this.elements[i].addEventListener('blur', this.checkSelectionWrapper);
512
+ }
513
+ return this;
514
+ },
515
+
516
+ checkSelection: function () {
517
+ var newSelection,
518
+ selectionElement;
519
+
520
+ if (this.keepToolbarAlive !== true && !this.options.disableToolbar) {
521
+ newSelection = window.getSelection();
522
+ if (newSelection.toString().trim() === '' ||
523
+ (this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs())) {
524
+ this.hideToolbarActions();
525
+ } else {
526
+ selectionElement = this.getSelectionElement();
527
+ if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
528
+ this.hideToolbarActions();
529
+ } else {
530
+ this.checkSelectionElement(newSelection, selectionElement);
531
+ }
532
+ }
533
+ }
534
+ return this;
535
+ },
536
+
537
+ clickingIntoArchorForm: function (e) {
538
+ var self = this;
539
+ if (e.type && e.type.toLowerCase() === 'blur' && e.relatedTarget && e.relatedTarget === self.anchorInput) {
540
+ return true;
541
+ }
542
+ return false;
543
+ },
544
+
545
+ hasMultiParagraphs: function () {
546
+ var selectionHtml = getSelectionHtml().replace(/<[\S]+><\/[\S]+>/gim, ''),
547
+ hasMultiParagraphs = selectionHtml.match(/<(p|h[0-6]|blockquote)>([\s\S]*?)<\/(p|h[0-6]|blockquote)>/g);
548
+
549
+ return (hasMultiParagraphs ? hasMultiParagraphs.length : 0);
550
+ },
551
+
552
+ checkSelectionElement: function (newSelection, selectionElement) {
553
+ var i;
554
+ this.selection = newSelection;
555
+ this.selectionRange = this.selection.getRangeAt(0);
556
+ for (i = 0; i < this.elements.length; i += 1) {
557
+ if (this.elements[i] === selectionElement) {
558
+ this.setToolbarButtonStates()
559
+ .setToolbarPosition()
560
+ .showToolbarActions();
561
+ return;
562
+ }
563
+ }
564
+ this.hideToolbarActions();
565
+ },
566
+
567
+ getSelectionElement: function () {
568
+ var selection = window.getSelection(),
569
+ range, current, parent,
570
+ result,
571
+ getMediumElement = function (e) {
572
+ var localParent = e;
573
+ try {
574
+ while (!localParent.getAttribute('data-medium-element')) {
575
+ localParent = localParent.parentNode;
576
+ }
577
+ } catch (errb) {
578
+ return false;
579
+ }
580
+ return localParent;
581
+ };
582
+ // First try on current node
583
+ try {
584
+ range = selection.getRangeAt(0);
585
+ current = range.commonAncestorContainer;
586
+ parent = current.parentNode;
587
+
588
+ if (current.getAttribute('data-medium-element')) {
589
+ result = current;
590
+ } else {
591
+ result = getMediumElement(parent);
592
+ }
593
+ // If not search in the parent nodes.
594
+ } catch (err) {
595
+ result = getMediumElement(parent);
596
+ }
597
+ return result;
598
+ },
599
+
600
+ setToolbarPosition: function () {
601
+ var buttonHeight = 50,
602
+ selection = window.getSelection(),
603
+ range = selection.getRangeAt(0),
604
+ boundary = range.getBoundingClientRect(),
605
+ defaultLeft = (this.options.diffLeft) - (this.toolbar.offsetWidth / 2),
606
+ middleBoundary = (boundary.left + boundary.right) / 2,
607
+ halfOffsetWidth = this.toolbar.offsetWidth / 2;
608
+ if (boundary.top < buttonHeight) {
609
+ this.toolbar.classList.add('medium-toolbar-arrow-over');
610
+ this.toolbar.classList.remove('medium-toolbar-arrow-under');
611
+ this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + window.pageYOffset - this.toolbar.offsetHeight + 'px';
612
+ } else {
613
+ this.toolbar.classList.add('medium-toolbar-arrow-under');
614
+ this.toolbar.classList.remove('medium-toolbar-arrow-over');
615
+ this.toolbar.style.top = boundary.top + this.options.diffTop + window.pageYOffset - this.toolbar.offsetHeight + 'px';
616
+ }
617
+ if (middleBoundary < halfOffsetWidth) {
618
+ this.toolbar.style.left = defaultLeft + halfOffsetWidth + 'px';
619
+ } else if ((window.innerWidth - middleBoundary) < halfOffsetWidth) {
620
+ this.toolbar.style.left = window.innerWidth + defaultLeft - halfOffsetWidth + 'px';
621
+ } else {
622
+ this.toolbar.style.left = defaultLeft + middleBoundary + 'px';
623
+ }
624
+
625
+ this.hideAnchorPreview();
626
+
627
+ return this;
628
+ },
629
+
630
+ setToolbarButtonStates: function () {
631
+ var buttons = this.toolbarActions.querySelectorAll('button'),
632
+ i;
633
+ for (i = 0; i < buttons.length; i += 1) {
634
+ buttons[i].classList.remove('medium-editor-button-active');
635
+ }
636
+ this.checkActiveButtons();
637
+ return this;
638
+ },
639
+
640
+ checkActiveButtons: function () {
641
+ var elements = Array.prototype.slice.call(this.elements),
642
+ parentNode = this.getSelectedParentElement();
643
+ while (parentNode.tagName !== undefined && this.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
644
+ this.activateButton(parentNode.tagName.toLowerCase());
645
+ this.callExtensions('checkState', parentNode);
646
+
647
+ // we can abort the search upwards if we leave the contentEditable element
648
+ if (elements.indexOf(parentNode) !== -1) {
649
+ break;
650
+ }
651
+ parentNode = parentNode.parentNode;
652
+ }
653
+ },
654
+
655
+ activateButton: function (tag) {
656
+ var el = this.toolbar.querySelector('[data-element="' + tag + '"]');
657
+ if (el !== null && el.className.indexOf('medium-editor-button-active') === -1) {
658
+ el.className += ' medium-editor-button-active';
659
+ }
660
+ },
661
+
662
+ bindButtons: function () {
663
+ var buttons = this.toolbar.querySelectorAll('button'),
664
+ i,
665
+ self = this,
666
+ triggerAction = function (e) {
667
+ e.preventDefault();
668
+ e.stopPropagation();
669
+ if (self.selection === undefined) {
670
+ self.checkSelection();
671
+ }
672
+ if (this.className.indexOf('medium-editor-button-active') > -1) {
673
+ this.classList.remove('medium-editor-button-active');
674
+ } else {
675
+ this.className += ' medium-editor-button-active';
676
+ }
677
+ if (this.hasAttribute('data-action')) {
678
+ self.execAction(this.getAttribute('data-action'), e);
679
+ }
680
+ };
681
+ for (i = 0; i < buttons.length; i += 1) {
682
+ buttons[i].addEventListener('click', triggerAction);
683
+ }
684
+ this.setFirstAndLastItems(buttons);
685
+ return this;
686
+ },
687
+
688
+ setFirstAndLastItems: function (buttons) {
689
+ if (buttons.length > 0) {
690
+ buttons[0].className += ' medium-editor-button-first';
691
+ buttons[buttons.length - 1].className += ' medium-editor-button-last';
692
+ }
693
+ return this;
694
+ },
695
+
696
+ execAction: function (action, e) {
697
+ if (action.indexOf('append-') > -1) {
698
+ this.execFormatBlock(action.replace('append-', ''));
699
+ this.setToolbarPosition();
700
+ // this.setToolbarButtonStates();
701
+ } else if (action === 'anchor') {
702
+ this.triggerAnchorAction(e);
703
+ } else if (action === 'image') {
704
+ document.execCommand('insertImage', false, window.getSelection());
705
+ } else {
706
+ document.execCommand(action, false, null);
707
+ this.setToolbarPosition();
708
+ }
709
+ this.triggerChange();
710
+ },
711
+
712
+ // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
713
+ rangeSelectsSingleNode: function (range) {
714
+ var startNode = range.startContainer;
715
+ return startNode === range.endContainer &&
716
+ startNode.hasChildNodes() &&
717
+ range.endOffset === range.startOffset + 1;
718
+ },
719
+
720
+ getSelectedParentElement: function () {
721
+ var selectedParentElement = null,
722
+ range = this.selectionRange;
723
+ if (this.rangeSelectsSingleNode(range)) {
724
+ selectedParentElement = range.startContainer.childNodes[range.startOffset];
725
+ } else if (range.startContainer.nodeType === 3) {
726
+ selectedParentElement = range.startContainer.parentNode;
727
+ } else {
728
+ selectedParentElement = range.startContainer;
729
+ }
730
+ return selectedParentElement;
731
+ },
732
+
733
+ triggerAnchorAction: function () {
734
+ var selectedParentElement = this.getSelectedParentElement();
735
+ if (selectedParentElement.tagName &&
736
+ selectedParentElement.tagName.toLowerCase() === 'a') {
737
+ document.execCommand('unlink', false, null);
738
+ this.triggerChange();
739
+ } else {
740
+ if (this.anchorForm.style.display === 'block') {
741
+ this.showToolbarActions();
742
+ } else {
743
+ this.showAnchorForm();
744
+ }
745
+ }
746
+ return this;
747
+ },
748
+
749
+ execFormatBlock: function (el) {
750
+ var selectionData = this.getSelectionData(this.selection.anchorNode);
751
+ // FF handles blockquote differently on formatBlock
752
+ // allowing nesting, we need to use outdent
753
+ // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
754
+ if (el === 'blockquote' && selectionData.el &&
755
+ selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
756
+ return document.execCommand('outdent', false, null);
757
+ }
758
+ if (selectionData.tagName === el) {
759
+ el = 'p';
760
+ }
761
+ // When IE we need to add <> to heading elements and
762
+ // blockquote needs to be called as indent
763
+ // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
764
+ // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
765
+ if (this.isIE) {
766
+ if (el === 'blockquote') {
767
+ return document.execCommand('indent', false, el);
768
+ }
769
+ el = '<' + el + '>';
770
+ }
771
+ return document.execCommand('formatBlock', false, el);
772
+ },
773
+
774
+ getSelectionData: function (el) {
775
+ var tagName;
776
+
777
+ if (el && el.tagName) {
778
+ tagName = el.tagName.toLowerCase();
779
+ }
780
+
781
+ while (el && this.parentElements.indexOf(tagName) === -1) {
782
+ el = el.parentNode;
783
+ if (el && el.tagName) {
784
+ tagName = el.tagName.toLowerCase();
785
+ }
786
+ }
787
+
788
+ this.triggerChange();
789
+
790
+ return {
791
+ el: el,
792
+ tagName: tagName
793
+ };
794
+ },
795
+
796
+ getFirstChild: function (el) {
797
+ var firstChild = el.firstChild;
798
+ while (firstChild !== null && firstChild.nodeType !== 1) {
799
+ firstChild = firstChild.nextSibling;
800
+ }
801
+ return firstChild;
802
+ },
803
+
804
+ hideToolbarActions: function () {
805
+ this.keepToolbarAlive = false;
806
+ if (this.toolbar !== undefined) {
807
+ this.toolbar.classList.remove('medium-editor-toolbar-active');
808
+ }
809
+ },
810
+
811
+ showToolbarActions: function () {
812
+ var self = this,
813
+ timer;
814
+ this.anchorForm.style.display = 'none';
815
+ this.toolbarActions.style.display = 'block';
816
+ this.keepToolbarAlive = false;
817
+ clearTimeout(timer);
818
+ timer = setTimeout(function () {
819
+ if (self.toolbar && !self.toolbar.classList.contains('medium-editor-toolbar-active')) {
820
+ self.toolbar.classList.add('medium-editor-toolbar-active');
821
+ }
822
+ }, 100);
823
+ },
824
+
825
+ showAnchorForm: function (link_value) {
826
+ this.toolbarActions.style.display = 'none';
827
+ this.savedSelection = saveSelection();
828
+ this.anchorForm.style.display = 'block';
829
+ this.keepToolbarAlive = true;
830
+ this.anchorInput.focus();
831
+ this.anchorInput.value = link_value || '';
832
+ },
833
+
834
+ bindAnchorForm: function () {
835
+ var linkCancel = this.anchorForm.querySelector('a.medium-editor-cancel'),
836
+ self = this;
837
+
838
+ this.anchorInput.addEventListener('keyup', function (e) {
839
+ if (e.keyCode === 13) {
840
+ e.preventDefault();
841
+
842
+ // set target for link and pass to createLink()
843
+ var target = "_self";
844
+ if (self.anchorTarget.checked) {
845
+ target = "_blank";
846
+ // reset target state to false:
847
+ self.anchorTarget.checked = false;
848
+ }
849
+
850
+ self.createLink(this, target);
851
+ }
852
+ });
853
+ this.anchorInput.addEventListener('click', function (e) {
854
+ // make sure not to hide form when cliking into the input
855
+ e.stopPropagation();
856
+ self.keepToolbarAlive = true;
857
+ });
858
+ this.anchorInput.addEventListener('blur', function () {
859
+ // self.keepToolbarAlive = false;
860
+ self.checkSelection();
861
+ });
862
+
863
+ linkCancel.addEventListener('click', function (e) {
864
+ e.preventDefault();
865
+ self.showToolbarActions();
866
+ restoreSelection(self.savedSelection);
867
+ });
868
+ return this;
869
+ },
870
+
871
+
872
+ hideAnchorPreview: function () {
873
+ this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
874
+ },
875
+
876
+ // TODO: break method
877
+ showAnchorPreview: function (anchorEl) {
878
+ if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
879
+ return true;
880
+ }
881
+
882
+ var self = this,
883
+ buttonHeight = 40,
884
+ boundary = anchorEl.getBoundingClientRect(),
885
+ middleBoundary = (boundary.left + boundary.right) / 2,
886
+ halfOffsetWidth,
887
+ defaultLeft,
888
+ timer;
889
+
890
+ self.anchorPreview.querySelector('i').textContent = anchorEl.href;
891
+ halfOffsetWidth = self.anchorPreview.offsetWidth / 2;
892
+ defaultLeft = self.options.diffLeft - halfOffsetWidth;
893
+
894
+ clearTimeout(timer);
895
+ timer = setTimeout(function () {
896
+ if (self.anchorPreview && !self.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
897
+ self.anchorPreview.classList.add('medium-editor-anchor-preview-active');
898
+ }
899
+ }, 100);
900
+
901
+ self.observeAnchorPreview(anchorEl);
902
+
903
+ self.anchorPreview.classList.add('medium-toolbar-arrow-over');
904
+ self.anchorPreview.classList.remove('medium-toolbar-arrow-under');
905
+ self.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - self.options.diffTop + window.pageYOffset - self.anchorPreview.offsetHeight) + 'px';
906
+ if (middleBoundary < halfOffsetWidth) {
907
+ self.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
908
+ } else if ((window.innerWidth - middleBoundary) < halfOffsetWidth) {
909
+ self.anchorPreview.style.left = window.innerWidth + defaultLeft - halfOffsetWidth + 'px';
910
+ } else {
911
+ self.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
912
+ }
913
+
914
+ return this;
915
+ },
916
+
917
+ // TODO: break method
918
+ observeAnchorPreview: function (anchorEl) {
919
+ var self = this,
920
+ lastOver = (new Date()).getTime(),
921
+ over = true,
922
+ stamp = function () {
923
+ lastOver = (new Date()).getTime();
924
+ over = true;
925
+ },
926
+ unstamp = function (e) {
927
+ if (!e.relatedTarget || !/anchor-preview/.test(e.relatedTarget.className)) {
928
+ over = false;
929
+ }
930
+ },
931
+ interval_timer = setInterval(function () {
932
+ if (over) {
933
+ return true;
934
+ }
935
+ var durr = (new Date()).getTime() - lastOver;
936
+ if (durr > self.options.anchorPreviewHideDelay) {
937
+ // hide the preview 1/2 second after mouse leaves the link
938
+ self.hideAnchorPreview();
939
+
940
+ // cleanup
941
+ clearInterval(interval_timer);
942
+ self.anchorPreview.removeEventListener('mouseover', stamp);
943
+ self.anchorPreview.removeEventListener('mouseout', unstamp);
944
+ anchorEl.removeEventListener('mouseover', stamp);
945
+ anchorEl.removeEventListener('mouseout', unstamp);
946
+
947
+ }
948
+ }, 200);
949
+
950
+ self.anchorPreview.addEventListener('mouseover', stamp);
951
+ self.anchorPreview.addEventListener('mouseout', unstamp);
952
+ anchorEl.addEventListener('mouseover', stamp);
953
+ anchorEl.addEventListener('mouseout', unstamp);
954
+ },
955
+
956
+ createAnchorPreview: function () {
957
+ var self = this,
958
+ anchorPreview = document.createElement('div');
959
+
960
+ anchorPreview.id = 'medium-editor-anchor-preview-' + this.id;
961
+ anchorPreview.className = 'medium-editor-anchor-preview';
962
+ anchorPreview.innerHTML = this.anchorPreviewTemplate();
963
+ this.options.elementsContainer.appendChild(anchorPreview);
964
+
965
+ anchorPreview.addEventListener('click', function () {
966
+ self.anchorPreviewClickHandler();
967
+ });
968
+
969
+ return anchorPreview;
970
+ },
971
+
972
+ anchorPreviewTemplate: function () {
973
+ return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
974
+ ' <i class="medium-editor-toolbar-anchor-preview-inner"></i>' +
975
+ '</div>';
976
+ },
977
+
978
+ anchorPreviewClickHandler: function (e) {
979
+ if (this.activeAnchor) {
980
+
981
+ var self = this,
982
+ range = document.createRange(),
983
+ sel = window.getSelection();
984
+
985
+ range.selectNodeContents(self.activeAnchor);
986
+ sel.removeAllRanges();
987
+ sel.addRange(range);
988
+ setTimeout(function () {
989
+ if (self.activeAnchor) {
990
+ self.showAnchorForm(self.activeAnchor.href);
991
+ }
992
+ self.keepToolbarAlive = false;
993
+ }, 100 + self.options.delay);
994
+
995
+ }
996
+
997
+ this.hideAnchorPreview();
998
+ },
999
+
1000
+ editorAnchorObserver: function (e) {
1001
+ var self = this,
1002
+ overAnchor = true,
1003
+ leaveAnchor = function () {
1004
+ // mark the anchor as no longer hovered, and stop listening
1005
+ overAnchor = false;
1006
+ self.activeAnchor.removeEventListener('mouseout', leaveAnchor);
1007
+ };
1008
+
1009
+ if (e.target && e.target.tagName.toLowerCase() === 'a') {
1010
+
1011
+ // Detect empty href attributes
1012
+ // The browser will make href="" or href="#top"
1013
+ // into absolute urls when accessed as e.targed.href, so check the html
1014
+ if (!/href=["']\S+["']/.test(e.target.outerHTML) || /href=["']#\S+["']/.test(e.target.outerHTML)) {
1015
+ return true;
1016
+ }
1017
+
1018
+ // only show when hovering on anchors
1019
+ if (this.toolbar.classList.contains('medium-editor-toolbar-active')) {
1020
+ // only show when toolbar is not present
1021
+ return true;
1022
+ }
1023
+ this.activeAnchor = e.target;
1024
+ this.activeAnchor.addEventListener('mouseout', leaveAnchor);
1025
+ // show the anchor preview according to the configured delay
1026
+ // if the mouse has not left the anchor tag in that time
1027
+ setTimeout(function () {
1028
+ if (overAnchor) {
1029
+ self.showAnchorPreview(e.target);
1030
+ }
1031
+ }, self.options.delay);
1032
+
1033
+
1034
+ }
1035
+ },
1036
+
1037
+ bindAnchorPreview: function (index) {
1038
+ var i, self = this;
1039
+ this.editorAnchorObserverWrapper = function (e) {
1040
+ self.editorAnchorObserver(e);
1041
+ };
1042
+ for (i = 0; i < this.elements.length; i += 1) {
1043
+ this.elements[i].addEventListener('mouseover', this.editorAnchorObserverWrapper);
1044
+ }
1045
+ return this;
1046
+ },
1047
+
1048
+ checkLinkFormat: function (value) {
1049
+ var re = /^https?:\/\//;
1050
+ if (value.match(re)) {
1051
+ return value;
1052
+ }
1053
+ return "http://" + value;
1054
+ },
1055
+
1056
+ setTargetBlank: function (href) {
1057
+ var el = getSelectionStart(),
1058
+ i;
1059
+ if (el.tagName.toLowerCase() === 'a') {
1060
+ el.target = '_blank';
1061
+ } else {
1062
+ el = el.getElementsByTagName('a');
1063
+ for (i = 0; i < el.length; i += 1) {
1064
+ if ( el[i].getAttribute('href') === href) {
1065
+ el[i].target = '_blank';
1066
+ }
1067
+ }
1068
+ }
1069
+ },
1070
+
1071
+ createLink: function (input, target) {
1072
+ restoreSelection(this.savedSelection);
1073
+ if (this.options.checkLinkFormat) {
1074
+ input.value = this.checkLinkFormat(input.value);
1075
+ }
1076
+
1077
+ document.execCommand('createLink', false, input.value);
1078
+
1079
+ if (target === "_blank") {
1080
+ this.setTargetBlank(input.value);
1081
+ }
1082
+
1083
+ this.triggerChange();
1084
+
1085
+ this.checkSelection();
1086
+ this.showToolbarActions();
1087
+ input.value = '';
1088
+ },
1089
+
1090
+ bindWindowActions: function () {
1091
+ var timerResize,
1092
+ self = this;
1093
+ this.windowResizeHandler = function () {
1094
+ clearTimeout(timerResize);
1095
+ timerResize = setTimeout(function () {
1096
+ if (self.toolbar && self.toolbar.classList.contains('medium-editor-toolbar-active')) {
1097
+ self.setToolbarPosition();
1098
+ }
1099
+ }, 100);
1100
+ };
1101
+ window.addEventListener('resize', this.windowResizeHandler);
1102
+ return this;
1103
+ },
1104
+
1105
+ activate: function () {
1106
+ if (this.isActive) {
1107
+ return;
1108
+ }
1109
+
1110
+ this.setup();
1111
+ },
1112
+
1113
+ // TODO: break method
1114
+ deactivate: function () {
1115
+ var i;
1116
+ if (!this.isActive) {
1117
+ return;
1118
+ }
1119
+ this.isActive = false;
1120
+
1121
+ if (this.toolbar !== undefined) {
1122
+ this.options.elementsContainer.removeChild(this.anchorPreview);
1123
+ this.options.elementsContainer.removeChild(this.toolbar);
1124
+ delete this.toolbar;
1125
+ delete this.anchorPreview;
1126
+ }
1127
+
1128
+ document.documentElement.removeEventListener('mouseup', this.checkSelectionWrapper);
1129
+ window.removeEventListener('resize', this.windowResizeHandler);
1130
+
1131
+ for (i = 0; i < this.elements.length; i += 1) {
1132
+ this.elements[i].removeEventListener('mouseover', this.editorAnchorObserverWrapper);
1133
+ this.elements[i].removeEventListener('keyup', this.checkSelectionWrapper);
1134
+ this.elements[i].removeEventListener('blur', this.checkSelectionWrapper);
1135
+ this.elements[i].removeEventListener('paste', this.pasteWrapper);
1136
+ this.elements[i].removeAttribute('contentEditable');
1137
+ this.elements[i].removeAttribute('data-medium-element');
1138
+ }
1139
+
1140
+ },
1141
+
1142
+ htmlEntities: function (str) {
1143
+ // converts special characters (like <) into their escaped/encoded values (like &lt;).
1144
+ // This allows you to show to display the string without the browser reading it as HTML.
1145
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1146
+ },
1147
+
1148
+ bindPaste: function () {
1149
+ var i, self = this;
1150
+ this.pasteWrapper = function (e) {
1151
+ var paragraphs,
1152
+ html = '',
1153
+ p;
1154
+
1155
+ this.classList.remove('medium-editor-placeholder');
1156
+ if (!self.options.forcePlainText && !self.options.cleanPastedHTML) {
1157
+ return this;
1158
+ }
1159
+
1160
+ if (e.clipboardData && e.clipboardData.getData && !e.defaultPrevented) {
1161
+ e.preventDefault();
1162
+
1163
+ if (self.options.cleanPastedHTML && e.clipboardData.getData('text/html')) {
1164
+ return self.cleanPaste(e.clipboardData.getData('text/html'));
1165
+ }
1166
+ if (!(self.options.disableReturn || this.getAttribute('data-disable-return'))) {
1167
+ paragraphs = e.clipboardData.getData('text/plain').split(/[\r\n]/g);
1168
+ for (p = 0; p < paragraphs.length; p += 1) {
1169
+ if (paragraphs[p] !== '') {
1170
+ if (navigator.userAgent.match(/firefox/i) && p === 0) {
1171
+ html += self.htmlEntities(paragraphs[p]);
1172
+ } else {
1173
+ html += '<p>' + self.htmlEntities(paragraphs[p]) + '</p>';
1174
+ }
1175
+ }
1176
+ }
1177
+ document.execCommand('insertHTML', false, html);
1178
+ this.triggerChange();
1179
+ } else {
1180
+ document.execCommand('insertHTML', false, e.clipboardData.getData('text/plain'));
1181
+ this.triggerChange();
1182
+ }
1183
+ }
1184
+ };
1185
+ for (i = 0; i < this.elements.length; i += 1) {
1186
+ this.elements[i].addEventListener('paste', this.pasteWrapper);
1187
+ }
1188
+
1189
+ this.triggerChange();
1190
+
1191
+ return this;
1192
+ },
1193
+
1194
+ setPlaceholders: function () {
1195
+ var i,
1196
+ activatePlaceholder = function (el) {
1197
+ if (!(el.querySelector('img')) &&
1198
+ el.textContent.replace(/^\s+|\s+$/g, '') === '') {
1199
+ el.classList.add('medium-editor-placeholder');
1200
+ }
1201
+ },
1202
+ placeholderWrapper = function (e) {
1203
+ this.classList.remove('medium-editor-placeholder');
1204
+ if (e.type !== 'keypress') {
1205
+ activatePlaceholder(this);
1206
+ }
1207
+ };
1208
+ for (i = 0; i < this.elements.length; i += 1) {
1209
+ activatePlaceholder(this.elements[i]);
1210
+ this.elements[i].addEventListener('blur', placeholderWrapper);
1211
+ this.elements[i].addEventListener('keypress', placeholderWrapper);
1212
+ }
1213
+ return this;
1214
+ },
1215
+
1216
+ cleanPaste: function (text) {
1217
+
1218
+ /*jslint regexp: true*/
1219
+ /*
1220
+ jslint does not allow character negation, because the negation
1221
+ will not match any unicode characters. In the regexes in this
1222
+ block, negation is used specifically to match the end of an html
1223
+ tag, and in fact unicode characters *should* be allowed.
1224
+ */
1225
+ var i, elList, workEl,
1226
+ el = this.getSelectionElement(),
1227
+ multiline = /<p|<br|<div/.test(text),
1228
+ replacements = [
1229
+
1230
+ // replace two bogus tags that begin pastes from google docs
1231
+ [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ""],
1232
+ [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ""],
1233
+
1234
+ // un-html spaces and newlines inserted by OS X
1235
+ [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
1236
+ [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
1237
+
1238
+ // replace google docs italics+bold with a span to be replaced once the html is inserted
1239
+ [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
1240
+
1241
+ // replace google docs italics with a span to be replaced once the html is inserted
1242
+ [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
1243
+
1244
+ //[replace google docs bolds with a span to be replaced once the html is inserted
1245
+ [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
1246
+
1247
+ // replace manually entered b/i/a tags with real ones
1248
+ [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
1249
+
1250
+ // replace manually a tags with real ones, converting smart-quotes from google docs
1251
+ [new RegExp(/&lt;a\s+href=(&quot;|&rdquo;|&ldquo;|“|”)([^&]+)(&quot;|&rdquo;|&ldquo;|“|”)&gt;/gi), '<a href="$2">']
1252
+
1253
+ ];
1254
+ /*jslint regexp: false*/
1255
+
1256
+ for (i = 0; i < replacements.length; i += 1) {
1257
+ text = text.replace(replacements[i][0], replacements[i][1]);
1258
+ }
1259
+
1260
+ if (multiline) {
1261
+
1262
+ // double br's aren't converted to p tags, but we want paragraphs.
1263
+ elList = text.split('<br><br>');
1264
+
1265
+ this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>');
1266
+ document.execCommand('insertText', false, "\n");
1267
+ this.triggerChange();
1268
+
1269
+ // block element cleanup
1270
+ elList = el.querySelectorAll('p,div,br');
1271
+ for (i = 0; i < elList.length; i += 1) {
1272
+
1273
+ workEl = elList[i];
1274
+
1275
+ switch (workEl.tagName.toLowerCase()) {
1276
+ case 'p':
1277
+ case 'div':
1278
+ this.filterCommonBlocks(workEl);
1279
+ break;
1280
+ case 'br':
1281
+ this.filterLineBreak(workEl);
1282
+ break;
1283
+ }
1284
+
1285
+ }
1286
+
1287
+
1288
+ } else {
1289
+
1290
+ this.pasteHTML(text);
1291
+
1292
+ }
1293
+
1294
+ },
1295
+
1296
+ pasteHTML: function (html) {
1297
+ var elList, workEl, i, fragmentBody, pasteBlock = document.createDocumentFragment();
1298
+
1299
+ pasteBlock.appendChild(document.createElement('body'));
1300
+
1301
+ fragmentBody = pasteBlock.querySelector('body');
1302
+ fragmentBody.innerHTML = html;
1303
+
1304
+ this.cleanupSpans(fragmentBody);
1305
+
1306
+ elList = fragmentBody.querySelectorAll('*');
1307
+ for (i = 0; i < elList.length; i += 1) {
1308
+
1309
+ workEl = elList[i];
1310
+
1311
+ // delete ugly attributes
1312
+ workEl.removeAttribute('class');
1313
+ workEl.removeAttribute('style');
1314
+ workEl.removeAttribute('dir');
1315
+
1316
+ if (workEl.tagName.toLowerCase() === 'meta') {
1317
+ workEl.parentNode.removeChild(workEl);
1318
+ }
1319
+
1320
+ }
1321
+ document.execCommand('insertHTML', false, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
1322
+ this.triggerChange();
1323
+ },
1324
+ isCommonBlock: function (el) {
1325
+ return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
1326
+ },
1327
+ filterCommonBlocks: function (el) {
1328
+ if (/^\s*$/.test(el.innerText)) {
1329
+ el.parentNode.removeChild(el);
1330
+ }
1331
+ },
1332
+ filterLineBreak: function (el) {
1333
+ if (this.isCommonBlock(el.previousElementSibling)) {
1334
+
1335
+ // remove stray br's following common block elements
1336
+ el.parentNode.removeChild(el);
1337
+
1338
+ } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
1339
+
1340
+ // remove br's just inside open or close tags of a div/p
1341
+ el.parentNode.removeChild(el);
1342
+
1343
+ } else if (el.parentNode.childElementCount === 1) {
1344
+
1345
+ // and br's that are the only child of a div/p
1346
+ this.removeWithParent(el);
1347
+
1348
+ }
1349
+
1350
+ },
1351
+
1352
+ // remove an element, including its parent, if it is the only element within its parent
1353
+ removeWithParent: function (el) {
1354
+ if (el && el.parentNode) {
1355
+ if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
1356
+ el.parentNode.parentNode.removeChild(el.parentNode);
1357
+ } else {
1358
+ el.parentNode.removeChild(el.parentNode);
1359
+ }
1360
+ }
1361
+ },
1362
+
1363
+ cleanupSpans: function (container_el) {
1364
+
1365
+ var i,
1366
+ el,
1367
+ new_el,
1368
+ spans = container_el.querySelectorAll('.replace-with');
1369
+
1370
+ for (i = 0; i < spans.length; i += 1) {
1371
+
1372
+ el = spans[i];
1373
+ new_el = document.createElement(el.classList.contains('bold') ? 'b' : 'i');
1374
+
1375
+ if (el.classList.contains('bold') && el.classList.contains('italic')) {
1376
+
1377
+ // add an i tag as well if this has both italics and bold
1378
+ new_el.innerHTML = '<i>' + el.innerHTML + '</i>';
1379
+
1380
+ } else {
1381
+
1382
+ new_el.innerHTML = el.innerHTML;
1383
+
1384
+ }
1385
+ el.parentNode.replaceChild(new_el, el);
1386
+
1387
+ }
1388
+
1389
+ spans = container_el.querySelectorAll('span');
1390
+ for (i = 0; i < spans.length; i += 1) {
1391
+
1392
+ el = spans[i];
1393
+
1394
+ // remove empty spans, replace others with their contents
1395
+ if (/^\s*$/.test()) {
1396
+ el.parentNode.removeChild(el);
1397
+ } else {
1398
+ el.parentNode.replaceChild(document.createTextNode(el.innerText), el);
1399
+ }
1400
+
1401
+ }
1402
+
1403
+ },
1404
+
1405
+ triggerChange: function () {
1406
+ // in IE all medium changes need to be triggered manually:
1407
+ // http://stackoverflow.com/a/23930764
1408
+ // Note: this will sometimes be triggered more than once per change
1409
+ jQuery(document).find('.editable-text').trigger('change');
1410
+ }
1411
+
1412
+ };
1413
+
1414
+ }(window, document));