tenon 1.0.52 → 1.0.53

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