godmin-medium 0.1.0

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,4445 @@
1
+ /*global self, document, DOMException */
2
+
3
+ /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
4
+
5
+ // Full polyfill for browsers with no classList support
6
+ if (!("classList" in document.createElement("_"))) {
7
+ (function (view) {
8
+
9
+ "use strict";
10
+
11
+ if (!('Element' in view)) return;
12
+
13
+ var
14
+ classListProp = "classList"
15
+ , protoProp = "prototype"
16
+ , elemCtrProto = view.Element[protoProp]
17
+ , objCtr = Object
18
+ , strTrim = String[protoProp].trim || function () {
19
+ return this.replace(/^\s+|\s+$/g, "");
20
+ }
21
+ , arrIndexOf = Array[protoProp].indexOf || function (item) {
22
+ var
23
+ i = 0
24
+ , len = this.length
25
+ ;
26
+ for (; i < len; i++) {
27
+ if (i in this && this[i] === item) {
28
+ return i;
29
+ }
30
+ }
31
+ return -1;
32
+ }
33
+ // Vendors: please allow content code to instantiate DOMExceptions
34
+ , DOMEx = function (type, message) {
35
+ this.name = type;
36
+ this.code = DOMException[type];
37
+ this.message = message;
38
+ }
39
+ , checkTokenAndGetIndex = function (classList, token) {
40
+ if (token === "") {
41
+ throw new DOMEx(
42
+ "SYNTAX_ERR"
43
+ , "An invalid or illegal string was specified"
44
+ );
45
+ }
46
+ if (/\s/.test(token)) {
47
+ throw new DOMEx(
48
+ "INVALID_CHARACTER_ERR"
49
+ , "String contains an invalid character"
50
+ );
51
+ }
52
+ return arrIndexOf.call(classList, token);
53
+ }
54
+ , ClassList = function (elem) {
55
+ var
56
+ trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
57
+ , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
58
+ , i = 0
59
+ , len = classes.length
60
+ ;
61
+ for (; i < len; i++) {
62
+ this.push(classes[i]);
63
+ }
64
+ this._updateClassName = function () {
65
+ elem.setAttribute("class", this.toString());
66
+ };
67
+ }
68
+ , classListProto = ClassList[protoProp] = []
69
+ , classListGetter = function () {
70
+ return new ClassList(this);
71
+ }
72
+ ;
73
+ // Most DOMException implementations don't allow calling DOMException's toString()
74
+ // on non-DOMExceptions. Error's toString() is sufficient here.
75
+ DOMEx[protoProp] = Error[protoProp];
76
+ classListProto.item = function (i) {
77
+ return this[i] || null;
78
+ };
79
+ classListProto.contains = function (token) {
80
+ token += "";
81
+ return checkTokenAndGetIndex(this, token) !== -1;
82
+ };
83
+ classListProto.add = function () {
84
+ var
85
+ tokens = arguments
86
+ , i = 0
87
+ , l = tokens.length
88
+ , token
89
+ , updated = false
90
+ ;
91
+ do {
92
+ token = tokens[i] + "";
93
+ if (checkTokenAndGetIndex(this, token) === -1) {
94
+ this.push(token);
95
+ updated = true;
96
+ }
97
+ }
98
+ while (++i < l);
99
+
100
+ if (updated) {
101
+ this._updateClassName();
102
+ }
103
+ };
104
+ classListProto.remove = function () {
105
+ var
106
+ tokens = arguments
107
+ , i = 0
108
+ , l = tokens.length
109
+ , token
110
+ , updated = false
111
+ , index
112
+ ;
113
+ do {
114
+ token = tokens[i] + "";
115
+ index = checkTokenAndGetIndex(this, token);
116
+ while (index !== -1) {
117
+ this.splice(index, 1);
118
+ updated = true;
119
+ index = checkTokenAndGetIndex(this, token);
120
+ }
121
+ }
122
+ while (++i < l);
123
+
124
+ if (updated) {
125
+ this._updateClassName();
126
+ }
127
+ };
128
+ classListProto.toggle = function (token, force) {
129
+ token += "";
130
+
131
+ var
132
+ result = this.contains(token)
133
+ , method = result ?
134
+ force !== true && "remove"
135
+ :
136
+ force !== false && "add"
137
+ ;
138
+
139
+ if (method) {
140
+ this[method](token);
141
+ }
142
+
143
+ if (force === true || force === false) {
144
+ return force;
145
+ } else {
146
+ return !result;
147
+ }
148
+ };
149
+ classListProto.toString = function () {
150
+ return this.join(" ");
151
+ };
152
+
153
+ if (objCtr.defineProperty) {
154
+ var classListPropDesc = {
155
+ get: classListGetter
156
+ , enumerable: true
157
+ , configurable: true
158
+ };
159
+ try {
160
+ objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
161
+ } catch (ex) { // IE 8 doesn't support enumerable:true
162
+ if (ex.number === -0x7FF5EC54) {
163
+ classListPropDesc.enumerable = false;
164
+ objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
165
+ }
166
+ }
167
+ } else if (objCtr[protoProp].__defineGetter__) {
168
+ elemCtrProto.__defineGetter__(classListProp, classListGetter);
169
+ }
170
+
171
+ }(self));
172
+ }
173
+
174
+ /* Blob.js
175
+ * A Blob implementation.
176
+ * 2014-07-24
177
+ *
178
+ * By Eli Grey, http://eligrey.com
179
+ * By Devin Samarin, https://github.com/dsamarin
180
+ * License: X11/MIT
181
+ * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
182
+ */
183
+
184
+ /*global self, unescape */
185
+ /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
186
+ plusplus: true */
187
+
188
+ /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
189
+
190
+ (function (view) {
191
+ "use strict";
192
+
193
+ view.URL = view.URL || view.webkitURL;
194
+
195
+ if (view.Blob && view.URL) {
196
+ try {
197
+ new Blob;
198
+ return;
199
+ } catch (e) {}
200
+ }
201
+
202
+ // Internally we use a BlobBuilder implementation to base Blob off of
203
+ // in order to support older browsers that only have BlobBuilder
204
+ var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
205
+ var
206
+ get_class = function(object) {
207
+ return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
208
+ }
209
+ , FakeBlobBuilder = function BlobBuilder() {
210
+ this.data = [];
211
+ }
212
+ , FakeBlob = function Blob(data, type, encoding) {
213
+ this.data = data;
214
+ this.size = data.length;
215
+ this.type = type;
216
+ this.encoding = encoding;
217
+ }
218
+ , FBB_proto = FakeBlobBuilder.prototype
219
+ , FB_proto = FakeBlob.prototype
220
+ , FileReaderSync = view.FileReaderSync
221
+ , FileException = function(type) {
222
+ this.code = this[this.name = type];
223
+ }
224
+ , file_ex_codes = (
225
+ "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
226
+ + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
227
+ ).split(" ")
228
+ , file_ex_code = file_ex_codes.length
229
+ , real_URL = view.URL || view.webkitURL || view
230
+ , real_create_object_URL = real_URL.createObjectURL
231
+ , real_revoke_object_URL = real_URL.revokeObjectURL
232
+ , URL = real_URL
233
+ , btoa = view.btoa
234
+ , atob = view.atob
235
+
236
+ , ArrayBuffer = view.ArrayBuffer
237
+ , Uint8Array = view.Uint8Array
238
+
239
+ , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
240
+ ;
241
+ FakeBlob.fake = FB_proto.fake = true;
242
+ while (file_ex_code--) {
243
+ FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
244
+ }
245
+ // Polyfill URL
246
+ if (!real_URL.createObjectURL) {
247
+ URL = view.URL = function(uri) {
248
+ var
249
+ uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
250
+ , uri_origin
251
+ ;
252
+ uri_info.href = uri;
253
+ if (!("origin" in uri_info)) {
254
+ if (uri_info.protocol.toLowerCase() === "data:") {
255
+ uri_info.origin = null;
256
+ } else {
257
+ uri_origin = uri.match(origin);
258
+ uri_info.origin = uri_origin && uri_origin[1];
259
+ }
260
+ }
261
+ return uri_info;
262
+ };
263
+ }
264
+ URL.createObjectURL = function(blob) {
265
+ var
266
+ type = blob.type
267
+ , data_URI_header
268
+ ;
269
+ if (type === null) {
270
+ type = "application/octet-stream";
271
+ }
272
+ if (blob instanceof FakeBlob) {
273
+ data_URI_header = "data:" + type;
274
+ if (blob.encoding === "base64") {
275
+ return data_URI_header + ";base64," + blob.data;
276
+ } else if (blob.encoding === "URI") {
277
+ return data_URI_header + "," + decodeURIComponent(blob.data);
278
+ } if (btoa) {
279
+ return data_URI_header + ";base64," + btoa(blob.data);
280
+ } else {
281
+ return data_URI_header + "," + encodeURIComponent(blob.data);
282
+ }
283
+ } else if (real_create_object_URL) {
284
+ return real_create_object_URL.call(real_URL, blob);
285
+ }
286
+ };
287
+ URL.revokeObjectURL = function(object_URL) {
288
+ if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
289
+ real_revoke_object_URL.call(real_URL, object_URL);
290
+ }
291
+ };
292
+ FBB_proto.append = function(data/*, endings*/) {
293
+ var bb = this.data;
294
+ // decode data to a binary string
295
+ if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
296
+ var
297
+ str = ""
298
+ , buf = new Uint8Array(data)
299
+ , i = 0
300
+ , buf_len = buf.length
301
+ ;
302
+ for (; i < buf_len; i++) {
303
+ str += String.fromCharCode(buf[i]);
304
+ }
305
+ bb.push(str);
306
+ } else if (get_class(data) === "Blob" || get_class(data) === "File") {
307
+ if (FileReaderSync) {
308
+ var fr = new FileReaderSync;
309
+ bb.push(fr.readAsBinaryString(data));
310
+ } else {
311
+ // async FileReader won't work as BlobBuilder is sync
312
+ throw new FileException("NOT_READABLE_ERR");
313
+ }
314
+ } else if (data instanceof FakeBlob) {
315
+ if (data.encoding === "base64" && atob) {
316
+ bb.push(atob(data.data));
317
+ } else if (data.encoding === "URI") {
318
+ bb.push(decodeURIComponent(data.data));
319
+ } else if (data.encoding === "raw") {
320
+ bb.push(data.data);
321
+ }
322
+ } else {
323
+ if (typeof data !== "string") {
324
+ data += ""; // convert unsupported types to strings
325
+ }
326
+ // decode UTF-16 to binary string
327
+ bb.push(unescape(encodeURIComponent(data)));
328
+ }
329
+ };
330
+ FBB_proto.getBlob = function(type) {
331
+ if (!arguments.length) {
332
+ type = null;
333
+ }
334
+ return new FakeBlob(this.data.join(""), type, "raw");
335
+ };
336
+ FBB_proto.toString = function() {
337
+ return "[object BlobBuilder]";
338
+ };
339
+ FB_proto.slice = function(start, end, type) {
340
+ var args = arguments.length;
341
+ if (args < 3) {
342
+ type = null;
343
+ }
344
+ return new FakeBlob(
345
+ this.data.slice(start, args > 1 ? end : this.data.length)
346
+ , type
347
+ , this.encoding
348
+ );
349
+ };
350
+ FB_proto.toString = function() {
351
+ return "[object Blob]";
352
+ };
353
+ FB_proto.close = function() {
354
+ this.size = 0;
355
+ delete this.data;
356
+ };
357
+ return FakeBlobBuilder;
358
+ }(view));
359
+
360
+ view.Blob = function(blobParts, options) {
361
+ var type = options ? (options.type || "") : "";
362
+ var builder = new BlobBuilder();
363
+ if (blobParts) {
364
+ for (var i = 0, len = blobParts.length; i < len; i++) {
365
+ if (Uint8Array && blobParts[i] instanceof Uint8Array) {
366
+ builder.append(blobParts[i].buffer);
367
+ }
368
+ else {
369
+ builder.append(blobParts[i]);
370
+ }
371
+ }
372
+ }
373
+ var blob = builder.getBlob(type);
374
+ if (!blob.slice && blob.webkitSlice) {
375
+ blob.slice = blob.webkitSlice;
376
+ }
377
+ return blob;
378
+ };
379
+
380
+ var getPrototypeOf = Object.getPrototypeOf || function(object) {
381
+ return object.__proto__;
382
+ };
383
+ view.Blob.prototype = getPrototypeOf(new view.Blob());
384
+ }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
385
+
386
+ (function (root, factory) {
387
+ 'use strict';
388
+ if (typeof module === 'object') {
389
+ module.exports = factory;
390
+ } else if (typeof define === 'function' && define.amd) {
391
+ define(function () {
392
+ return factory;
393
+ });
394
+ } else {
395
+ root.MediumEditor = factory;
396
+ }
397
+ }(this, function () {
398
+
399
+ 'use strict';
400
+
401
+ var Util;
402
+
403
+ (function (window) {
404
+ 'use strict';
405
+
406
+ // Params: Array, Boolean, Object
407
+ function getProp(parts, create, context) {
408
+ if (!context) {
409
+ context = window;
410
+ }
411
+
412
+ try {
413
+ for (var i = 0; i < parts.length; i++) {
414
+ var p = parts[i];
415
+ if (!(p in context)) {
416
+ if (create) {
417
+ context[p] = {};
418
+ } else {
419
+ return;
420
+ }
421
+ }
422
+ context = context[p];
423
+ }
424
+ return context;
425
+ } catch (e) {
426
+ // "p in context" throws an exception when context is a number, boolean, etc. rather than an object,
427
+ // so in that corner case just return undefined (by having no return statement)
428
+ }
429
+ }
430
+
431
+ function copyInto(overwrite, dest) {
432
+ var prop,
433
+ sources = Array.prototype.slice.call(arguments, 2);
434
+ dest = dest || {};
435
+ for (var i = 0; i < sources.length; i++) {
436
+ var source = sources[i];
437
+ if (source) {
438
+ for (prop in source) {
439
+ if (source.hasOwnProperty(prop) &&
440
+ typeof source[prop] !== 'undefined' &&
441
+ (overwrite || dest.hasOwnProperty(prop) === false)) {
442
+ dest[prop] = source[prop];
443
+ }
444
+ }
445
+ }
446
+ }
447
+ return dest;
448
+ }
449
+
450
+ Util = {
451
+
452
+ // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
453
+ // by rg89
454
+ isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
455
+
456
+ // https://github.com/jashkenas/underscore
457
+ keyCode: {
458
+ BACKSPACE: 8,
459
+ TAB: 9,
460
+ ENTER: 13,
461
+ ESCAPE: 27,
462
+ SPACE: 32,
463
+ DELETE: 46
464
+ },
465
+
466
+ parentElements: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'],
467
+
468
+ extend: function extend(/* dest, source1, source2, ...*/) {
469
+ var args = [true].concat(Array.prototype.slice.call(arguments));
470
+ return copyInto.apply(this, args);
471
+ },
472
+
473
+ defaults: function defaults(/*dest, source1, source2, ...*/) {
474
+ var args = [false].concat(Array.prototype.slice.call(arguments));
475
+ return copyInto.apply(this, args);
476
+ },
477
+
478
+ derives: function derives(base, derived) {
479
+ var origPrototype = derived.prototype;
480
+ function Proto() { }
481
+ Proto.prototype = base.prototype;
482
+ derived.prototype = new Proto();
483
+ derived.prototype.constructor = base;
484
+ derived.prototype = copyInto(false, derived.prototype, origPrototype);
485
+ return derived;
486
+ },
487
+
488
+ // Find the next node in the DOM tree that represents any text that is being
489
+ // displayed directly next to the targetNode (passed as an argument)
490
+ // Text that appears directly next to the current node can be:
491
+ // - A sibling text node
492
+ // - A descendant of a sibling element
493
+ // - A sibling text node of an ancestor
494
+ // - A descendant of a sibling element of an ancestor
495
+ findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
496
+ var pastTarget = false,
497
+ nextNode,
498
+ nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
499
+
500
+ // Use a native NodeIterator to iterate over all the text nodes that are descendants
501
+ // of the rootNode. Once past the targetNode, choose the first non-empty text node
502
+ nextNode = nodeIterator.nextNode();
503
+ while (nextNode) {
504
+ if (nextNode === targetNode) {
505
+ pastTarget = true;
506
+ } else if (pastTarget) {
507
+ if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
508
+ break;
509
+ }
510
+ }
511
+ nextNode = nodeIterator.nextNode();
512
+ }
513
+
514
+ return nextNode;
515
+ },
516
+
517
+ isDescendant: function isDescendant(parent, child, checkEquality) {
518
+ if (!parent || !child) {
519
+ return false;
520
+ }
521
+ if (checkEquality && parent === child) {
522
+ return true;
523
+ }
524
+ var node = child.parentNode;
525
+ while (node !== null) {
526
+ if (node === parent) {
527
+ return true;
528
+ }
529
+ node = node.parentNode;
530
+ }
531
+ return false;
532
+ },
533
+
534
+ // https://github.com/jashkenas/underscore
535
+ isElement: function isElement(obj) {
536
+ return !!(obj && obj.nodeType === 1);
537
+ },
538
+
539
+ now: Date.now,
540
+
541
+ // https://github.com/jashkenas/underscore
542
+ throttle: function (func, wait) {
543
+ var THROTTLE_INTERVAL = 50,
544
+ context,
545
+ args,
546
+ result,
547
+ timeout = null,
548
+ previous = 0,
549
+ later = function () {
550
+ previous = Date.now();
551
+ timeout = null;
552
+ result = func.apply(context, args);
553
+ if (!timeout) {
554
+ context = args = null;
555
+ }
556
+ };
557
+
558
+ if (!wait && wait !== 0) {
559
+ wait = THROTTLE_INTERVAL;
560
+ }
561
+
562
+ return function () {
563
+ var now = Date.now(),
564
+ remaining = wait - (now - previous);
565
+
566
+ context = this;
567
+ args = arguments;
568
+ if (remaining <= 0 || remaining > wait) {
569
+ if (timeout) {
570
+ clearTimeout(timeout);
571
+ timeout = null;
572
+ }
573
+ previous = now;
574
+ result = func.apply(context, args);
575
+ if (!timeout) {
576
+ context = args = null;
577
+ }
578
+ } else if (!timeout) {
579
+ timeout = setTimeout(later, remaining);
580
+ }
581
+ return result;
582
+ };
583
+ },
584
+
585
+ traverseUp: function (current, testElementFunction) {
586
+ if (!current) {
587
+ return false;
588
+ }
589
+
590
+ do {
591
+ if (current.nodeType === 1) {
592
+ if (testElementFunction(current)) {
593
+ return current;
594
+ }
595
+ // do not traverse upwards past the nearest containing editor
596
+ if (current.getAttribute('data-medium-element')) {
597
+ return false;
598
+ }
599
+ }
600
+
601
+ current = current.parentNode;
602
+ } while (current);
603
+
604
+ return false;
605
+ },
606
+
607
+ htmlEntities: function (str) {
608
+ // converts special characters (like <) into their escaped/encoded values (like &lt;).
609
+ // This allows you to show to display the string without the browser reading it as HTML.
610
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
611
+ },
612
+
613
+ // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
614
+ insertHTMLCommand: function (doc, html) {
615
+ var selection, range, el, fragment, node, lastNode, toReplace;
616
+
617
+ if (doc.queryCommandSupported('insertHTML')) {
618
+ try {
619
+ return doc.execCommand('insertHTML', false, html);
620
+ } catch (ignore) {}
621
+ }
622
+
623
+ selection = doc.defaultView.getSelection();
624
+ if (selection.getRangeAt && selection.rangeCount) {
625
+ range = selection.getRangeAt(0);
626
+ toReplace = range.commonAncestorContainer;
627
+ // Ensure range covers maximum amount of nodes as possible
628
+ // By moving up the DOM and selecting ancestors whose only child is the range
629
+ if ((toReplace.nodeType === 3 && toReplace.nodeValue === range.toString()) ||
630
+ (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {
631
+ while (toReplace.parentNode &&
632
+ toReplace.parentNode.childNodes.length === 1 &&
633
+ !toReplace.parentNode.getAttribute('data-medium-element')) {
634
+ toReplace = toReplace.parentNode;
635
+ }
636
+ range.selectNode(toReplace);
637
+ }
638
+ range.deleteContents();
639
+
640
+ el = doc.createElement("div");
641
+ el.innerHTML = html;
642
+ fragment = doc.createDocumentFragment();
643
+ while (el.firstChild) {
644
+ node = el.firstChild;
645
+ lastNode = fragment.appendChild(node);
646
+ }
647
+ range.insertNode(fragment);
648
+
649
+ // Preserve the selection:
650
+ if (lastNode) {
651
+ range = range.cloneRange();
652
+ range.setStartAfter(lastNode);
653
+ range.collapse(true);
654
+ selection.removeAllRanges();
655
+ selection.addRange(range);
656
+ }
657
+ }
658
+ },
659
+
660
+ getSelectionRange: function (ownerDocument) {
661
+ var selection = ownerDocument.getSelection();
662
+ if (selection.rangeCount === 0) {
663
+ return null;
664
+ }
665
+ return selection.getRangeAt(0);
666
+ },
667
+
668
+ // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
669
+ // by You
670
+ getSelectionStart: function (ownerDocument) {
671
+ var node = ownerDocument.getSelection().anchorNode,
672
+ startNode = (node && node.nodeType === 3 ? node.parentNode : node);
673
+ return startNode;
674
+ },
675
+
676
+ getSelectionData: function (el) {
677
+ var tagName;
678
+
679
+ if (el && el.tagName) {
680
+ tagName = el.tagName.toLowerCase();
681
+ }
682
+
683
+ while (el && this.parentElements.indexOf(tagName) === -1) {
684
+ el = el.parentNode;
685
+ if (el && el.tagName) {
686
+ tagName = el.tagName.toLowerCase();
687
+ }
688
+ }
689
+
690
+ return {
691
+ el: el,
692
+ tagName: tagName
693
+ };
694
+ },
695
+
696
+ execFormatBlock: function (doc, tagName) {
697
+ var selectionData = this.getSelectionData(this.getSelectionStart(doc));
698
+ // FF handles blockquote differently on formatBlock
699
+ // allowing nesting, we need to use outdent
700
+ // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
701
+ if (tagName === 'blockquote' && selectionData.el &&
702
+ selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
703
+ return doc.execCommand('outdent', false, null);
704
+ }
705
+ if (selectionData.tagName === tagName) {
706
+ tagName = 'p';
707
+ }
708
+ // When IE we need to add <> to heading elements and
709
+ // blockquote needs to be called as indent
710
+ // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
711
+ // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
712
+ if (this.isIE) {
713
+ if (tagName === 'blockquote') {
714
+ return doc.execCommand('indent', false, tagName);
715
+ }
716
+ tagName = '<' + tagName + '>';
717
+ }
718
+ return doc.execCommand('formatBlock', false, tagName);
719
+ },
720
+
721
+ // TODO: not sure if this should be here
722
+ setTargetBlank: function (el) {
723
+ var i;
724
+ if (el.tagName.toLowerCase() === 'a') {
725
+ el.target = '_blank';
726
+ } else {
727
+ el = el.getElementsByTagName('a');
728
+
729
+ for (i = 0; i < el.length; i += 1) {
730
+ el[i].target = '_blank';
731
+ }
732
+ }
733
+ },
734
+
735
+ addClassToAnchors: function (el, buttonClass) {
736
+ var classes = buttonClass.split(' '),
737
+ i,
738
+ j;
739
+ if (el.tagName.toLowerCase() === 'a') {
740
+ for (j = 0; j < classes.length; j += 1) {
741
+ el.classList.add(classes[j]);
742
+ }
743
+ } else {
744
+ el = el.getElementsByTagName('a');
745
+ for (i = 0; i < el.length; i += 1) {
746
+ for (j = 0; j < classes.length; j += 1) {
747
+ el[i].classList.add(classes[j]);
748
+ }
749
+ }
750
+ }
751
+ },
752
+
753
+ isListItem: function (node) {
754
+ if (!node) {
755
+ return false;
756
+ }
757
+ if (node.tagName.toLowerCase() === 'li') {
758
+ return true;
759
+ }
760
+
761
+ var parentNode = node.parentNode,
762
+ tagName = parentNode.tagName.toLowerCase();
763
+ while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
764
+ if (tagName === 'li') {
765
+ return true;
766
+ }
767
+ parentNode = parentNode.parentNode;
768
+ if (parentNode && parentNode.tagName) {
769
+ tagName = parentNode.tagName.toLowerCase();
770
+ } else {
771
+ return false;
772
+ }
773
+ }
774
+ return false;
775
+ },
776
+
777
+ cleanListDOM: function (element) {
778
+ if (element.tagName.toLowerCase() === 'li') {
779
+ var list = element.parentElement;
780
+ if (list.parentElement.tagName.toLowerCase() === 'p') { // yes we need to clean up
781
+ this.unwrapElement(list.parentElement);
782
+ }
783
+ }
784
+ },
785
+
786
+ unwrapElement: function (element) {
787
+ var parent = element.parentNode,
788
+ current = element.firstChild,
789
+ next;
790
+ do {
791
+ next = current.nextSibling;
792
+ parent.insertBefore(current, element);
793
+ current = next;
794
+ } while (current);
795
+ parent.removeChild(element);
796
+ },
797
+
798
+ warn: function(){
799
+ if(window.console !== undefined && typeof window.console.warn === 'function'){
800
+ window.console.warn.apply(console, arguments);
801
+ }
802
+ },
803
+
804
+ deprecated: function(oldName, newName, version){
805
+ // simple deprecation warning mechanism.
806
+ var m = oldName + " is deprecated, please use " + newName + " instead.";
807
+ if(version){
808
+ m += " Will be removed in " + version;
809
+ }
810
+ Util.warn(m);
811
+ },
812
+
813
+ deprecatedMethod: function (oldName, newName, args, version) {
814
+ // run the replacement and warn when someone calls a deprecated method
815
+ Util.deprecated(oldName, newName, version);
816
+ if (typeof this[newName] === 'function') {
817
+ this[newName].apply(this, args);
818
+ }
819
+ },
820
+
821
+ cleanupAttrs: function (el, attrs) {
822
+ attrs.forEach(function (attr) {
823
+ el.removeAttribute(attr);
824
+ });
825
+ },
826
+
827
+ cleanupTags: function (el, tags) {
828
+ tags.forEach(function (tag) {
829
+ if (el.tagName.toLowerCase() === tag) {
830
+ el.parentNode.removeChild(el);
831
+ }
832
+ }, this);
833
+ },
834
+
835
+ getClosestTag : function(el, tag) { // get the closest parent
836
+ return Util.traverseUp(el, function (element) {
837
+ return element.tagName.toLowerCase() === tag.toLowerCase();
838
+ });
839
+ },
840
+
841
+ unwrap: function (el, doc) {
842
+ var fragment = doc.createDocumentFragment();
843
+
844
+ for (var i = 0; i < el.childNodes.length; i++) {
845
+ fragment.appendChild(el.childNodes[i]);
846
+ }
847
+
848
+ if (fragment.childNodes.length) {
849
+ el.parentNode.replaceChild(fragment, el);
850
+ } else {
851
+ el.parentNode.removeChild(el);
852
+ }
853
+ },
854
+
855
+ setObject: function(name, value, context){
856
+ // summary:
857
+ // Set a property from a dot-separated string, such as "A.B.C"
858
+ var parts = name.split("."), p = parts.pop(), obj = getProp(parts, true, context);
859
+ return obj && p ? (obj[p] = value) : undefined; // Object
860
+ },
861
+
862
+ getObject: function(name, create, context){
863
+ // summary:
864
+ // Get a property from a dot-separated string, such as "A.B.C"
865
+ return getProp(name ? name.split(".") : [], create, context); // Object
866
+ }
867
+
868
+ };
869
+ }(window));
870
+
871
+ var ButtonsData;
872
+ (function(){
873
+ 'use strict';
874
+
875
+ ButtonsData = {
876
+ 'bold': {
877
+ name: 'bold',
878
+ action: 'bold',
879
+ aria: 'bold',
880
+ tagNames: ['b', 'strong'],
881
+ style: {
882
+ prop: 'font-weight',
883
+ value: '700|bold'
884
+ },
885
+ useQueryState: true,
886
+ contentDefault: '<b>B</b>',
887
+ contentFA: '<i class="fa fa-bold"></i>',
888
+ key: 'b'
889
+ },
890
+ 'italic': {
891
+ name: 'italic',
892
+ action: 'italic',
893
+ aria: 'italic',
894
+ tagNames: ['i', 'em'],
895
+ style: {
896
+ prop: 'font-style',
897
+ value: 'italic'
898
+ },
899
+ useQueryState: true,
900
+ contentDefault: '<b><i>I</i></b>',
901
+ contentFA: '<i class="fa fa-italic"></i>',
902
+ key: 'i'
903
+ },
904
+ 'underline': {
905
+ name: 'underline',
906
+ action: 'underline',
907
+ aria: 'underline',
908
+ tagNames: ['u'],
909
+ style: {
910
+ prop: 'text-decoration',
911
+ value: 'underline'
912
+ },
913
+ useQueryState: true,
914
+ contentDefault: '<b><u>U</u></b>',
915
+ contentFA: '<i class="fa fa-underline"></i>',
916
+ key: 'u'
917
+ },
918
+ 'strikethrough': {
919
+ name: 'strikethrough',
920
+ action: 'strikethrough',
921
+ aria: 'strike through',
922
+ tagNames: ['strike'],
923
+ style: {
924
+ prop: 'text-decoration',
925
+ value: 'line-through'
926
+ },
927
+ useQueryState: true,
928
+ contentDefault: '<s>A</s>',
929
+ contentFA: '<i class="fa fa-strikethrough"></i>'
930
+ },
931
+ 'superscript': {
932
+ name: 'superscript',
933
+ action: 'superscript',
934
+ aria: 'superscript',
935
+ tagNames: ['sup'],
936
+ /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript
937
+ https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
938
+ // useQueryState: true
939
+ contentDefault: '<b>x<sup>1</sup></b>',
940
+ contentFA: '<i class="fa fa-superscript"></i>'
941
+ },
942
+ 'subscript': {
943
+ name: 'subscript',
944
+ action: 'subscript',
945
+ aria: 'subscript',
946
+ tagNames: ['sub'],
947
+ /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript
948
+ https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
949
+ // useQueryState: true
950
+ contentDefault: '<b>x<sub>1</sub></b>',
951
+ contentFA: '<i class="fa fa-subscript"></i>'
952
+ },
953
+ 'image': {
954
+ name: 'image',
955
+ action: 'image',
956
+ aria: 'image',
957
+ tagNames: ['img'],
958
+ contentDefault: '<b>image</b>',
959
+ contentFA: '<i class="fa fa-picture-o"></i>'
960
+ },
961
+ 'quote': {
962
+ name: 'quote',
963
+ action: 'append-blockquote',
964
+ aria: 'blockquote',
965
+ tagNames: ['blockquote'],
966
+ contentDefault: '<b>&ldquo;</b>',
967
+ contentFA: '<i class="fa fa-quote-right"></i>'
968
+ },
969
+ 'orderedlist': {
970
+ name: 'orderedlist',
971
+ action: 'insertorderedlist',
972
+ aria: 'ordered list',
973
+ tagNames: ['ol'],
974
+ useQueryState: true,
975
+ contentDefault: '<b>1.</b>',
976
+ contentFA: '<i class="fa fa-list-ol"></i>'
977
+ },
978
+ 'unorderedlist': {
979
+ name: 'unorderedlist',
980
+ action: 'insertunorderedlist',
981
+ aria: 'unordered list',
982
+ tagNames: ['ul'],
983
+ useQueryState: true,
984
+ contentDefault: '<b>&bull;</b>',
985
+ contentFA: '<i class="fa fa-list-ul"></i>'
986
+ },
987
+ 'pre': {
988
+ name: 'pre',
989
+ action: 'append-pre',
990
+ aria: 'preformatted text',
991
+ tagNames: ['pre'],
992
+ contentDefault: '<b>0101</b>',
993
+ contentFA: '<i class="fa fa-code fa-lg"></i>'
994
+ },
995
+ 'indent': {
996
+ name: 'indent',
997
+ action: 'indent',
998
+ aria: 'indent',
999
+ tagNames: [],
1000
+ contentDefault: '<b>&rarr;</b>',
1001
+ contentFA: '<i class="fa fa-indent"></i>'
1002
+ },
1003
+ 'outdent': {
1004
+ name: 'outdent',
1005
+ action: 'outdent',
1006
+ aria: 'outdent',
1007
+ tagNames: [],
1008
+ contentDefault: '<b>&larr;</b>',
1009
+ contentFA: '<i class="fa fa-outdent"></i>'
1010
+ },
1011
+ 'justifyCenter': {
1012
+ name: 'justifyCenter',
1013
+ action: 'justifyCenter',
1014
+ aria: 'center justify',
1015
+ tagNames: [],
1016
+ style: {
1017
+ prop: 'text-align',
1018
+ value: 'center'
1019
+ },
1020
+ contentDefault: '<b>C</b>',
1021
+ contentFA: '<i class="fa fa-align-center"></i>'
1022
+ },
1023
+ 'justifyFull': {
1024
+ name: 'justifyFull',
1025
+ action: 'justifyFull',
1026
+ aria: 'full justify',
1027
+ tagNames: [],
1028
+ style: {
1029
+ prop: 'text-align',
1030
+ value: 'justify'
1031
+ },
1032
+ contentDefault: '<b>J</b>',
1033
+ contentFA: '<i class="fa fa-align-justify"></i>'
1034
+ },
1035
+ 'justifyLeft': {
1036
+ name: 'justifyLeft',
1037
+ action: 'justifyLeft',
1038
+ aria: 'left justify',
1039
+ tagNames: [],
1040
+ style: {
1041
+ prop: 'text-align',
1042
+ value: 'left'
1043
+ },
1044
+ contentDefault: '<b>L</b>',
1045
+ contentFA: '<i class="fa fa-align-left"></i>'
1046
+ },
1047
+ 'justifyRight': {
1048
+ name: 'justifyRight',
1049
+ action: 'justifyRight',
1050
+ aria: 'right justify',
1051
+ tagNames: [],
1052
+ style: {
1053
+ prop: 'text-align',
1054
+ value: 'right'
1055
+ },
1056
+ contentDefault: '<b>R</b>',
1057
+ contentFA: '<i class="fa fa-align-right"></i>'
1058
+ },
1059
+ 'header1': {
1060
+ name: 'header1',
1061
+ action: function (options) {
1062
+ return 'append-' + options.firstHeader;
1063
+ },
1064
+ aria: function (options) {
1065
+ return options.firstHeader;
1066
+ },
1067
+ tagNames: function (options) {
1068
+ return [options.firstHeader];
1069
+ },
1070
+ contentDefault: '<b>H1</b>'
1071
+ },
1072
+ 'header2': {
1073
+ name: 'header2',
1074
+ action: function (options) {
1075
+ return 'append-' + options.secondHeader;
1076
+ },
1077
+ aria: function (options) {
1078
+ return options.secondHeader;
1079
+ },
1080
+ tagNames: function (options) {
1081
+ return [options.secondHeader];
1082
+ },
1083
+ contentDefault: '<b>H2</b>'
1084
+ },
1085
+ // Known inline elements that are not removed, or not removed consistantly across browsers:
1086
+ // <span>, <label>, <br>
1087
+ 'removeFormat': {
1088
+ name: 'removeFormat',
1089
+ aria: 'remove formatting',
1090
+ action: 'removeFormat',
1091
+ contentDefault: '<b>X</b>',
1092
+ contentFA: '<i class="fa fa-eraser"></i>'
1093
+ }
1094
+ };
1095
+
1096
+ })();
1097
+ var editorDefaults;
1098
+ (function(){
1099
+
1100
+ // summary: The default options hash used by the Editor
1101
+
1102
+ editorDefaults = {
1103
+
1104
+ allowMultiParagraphSelection: true,
1105
+ anchorInputPlaceholder: 'Paste or type a link',
1106
+ anchorInputCheckboxLabel: 'Open in new window',
1107
+ anchorPreviewHideDelay: 500,
1108
+ buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
1109
+ buttonLabels: false,
1110
+ checkLinkFormat: false,
1111
+ delay: 0,
1112
+ diffLeft: 0,
1113
+ diffTop: -10,
1114
+ disableReturn: false,
1115
+ disableDoubleReturn: false,
1116
+ disableToolbar: false,
1117
+ disableAnchorPreview: false,
1118
+ disableEditing: false,
1119
+ disablePlaceholders: false,
1120
+ toolbarAlign: 'center',
1121
+ elementsContainer: false,
1122
+ imageDragging: true,
1123
+ standardizeSelectionStart: false,
1124
+ contentWindow: window,
1125
+ ownerDocument: document,
1126
+ firstHeader: 'h3',
1127
+ placeholder: 'Type your text',
1128
+ secondHeader: 'h4',
1129
+ targetBlank: false,
1130
+ anchorTarget: false,
1131
+ anchorButton: false,
1132
+ anchorButtonClass: 'btn',
1133
+ extensions: {},
1134
+ activeButtonClass: 'medium-editor-button-active',
1135
+ firstButtonClass: 'medium-editor-button-first',
1136
+ lastButtonClass: 'medium-editor-button-last',
1137
+ spellcheck: true,
1138
+
1139
+ paste: {
1140
+ forcePlainText: true,
1141
+ cleanPastedHTML: false,
1142
+ cleanAttrs: ['class', 'style', 'dir'],
1143
+ cleanTags: ['meta']
1144
+ }
1145
+
1146
+ };
1147
+
1148
+ })();
1149
+
1150
+ var Extension;
1151
+ (function(){
1152
+
1153
+ /* global Util */
1154
+
1155
+ Extension = function (options) {
1156
+ Util.extend(this, options);
1157
+ };
1158
+
1159
+ Extension.extend = function (protoProps) {
1160
+ // magic extender thinger. mostly borrowed from backbone/goog.inherits
1161
+ // place this function on some thing you want extend-able.
1162
+ //
1163
+ // example:
1164
+ //
1165
+ // function Thing(args){
1166
+ // this.options = args;
1167
+ // }
1168
+ //
1169
+ // Thing.prototype = { foo: "bar" };
1170
+ // Thing.extend = extenderify;
1171
+ //
1172
+ // var ThingTwo = Thing.extend({ foo: "baz" });
1173
+ //
1174
+ // var thingOne = new Thing(); // foo === bar
1175
+ // var thingTwo = new ThingTwo(); // foo == baz
1176
+ //
1177
+ // which seems like some simply shallow copy nonsense
1178
+ // at first, but a lot more is going on there.
1179
+ //
1180
+ // passing a `constructor` to the extend props
1181
+ // will cause the instance to instantiate through that
1182
+ // instead of the parent's constructor.
1183
+
1184
+ var parent = this, child;
1185
+
1186
+ // The constructor function for the new subclass is either defined by you
1187
+ // (the "constructor" property in your `extend` definition), or defaulted
1188
+ // by us to simply call the parent's constructor.
1189
+
1190
+ if (protoProps && protoProps.hasOwnProperty("constructor")) {
1191
+ child = protoProps.constructor;
1192
+ } else {
1193
+ child = function () { return parent.apply(this, arguments); };
1194
+ }
1195
+
1196
+ // das statics (.extend comes over, so your subclass can have subclasses too)
1197
+ Util.extend(child, parent);
1198
+
1199
+ // Set the prototype chain to inherit from `parent`, without calling
1200
+ // `parent`'s constructor function.
1201
+ var Surrogate = function(){ this.constructor = child; };
1202
+ Surrogate.prototype = parent.prototype;
1203
+ child.prototype = new Surrogate();
1204
+
1205
+ if (protoProps) { Util.extend(child.prototype, protoProps); }
1206
+
1207
+ // todo: $super?
1208
+
1209
+ return child;
1210
+ };
1211
+
1212
+ Extension.prototype = {
1213
+ init: function(/* instance */){
1214
+ // called when properly decorated and used.
1215
+ // has a .base value pointing to the editor
1216
+ // owning us. has been given a .name if no
1217
+ // name present
1218
+ },
1219
+
1220
+ /* parent: [boolean]
1221
+ *
1222
+ * setting this to true will set the .base property
1223
+ * of the extension to be a reference to the
1224
+ * medium-editor instance that is using the extension
1225
+ */
1226
+ parent: false,
1227
+
1228
+ /* base: [MediumEditor instance]
1229
+ *
1230
+ * If .parent is set to true, this will be set to the
1231
+ * current MediumEditor instance before init() is called
1232
+ */
1233
+ base: null,
1234
+
1235
+ /* name: [string]
1236
+ *
1237
+ * 'name' of the extension, used for retrieving the extension.
1238
+ * If not set, MediumEditor will set this to be the key
1239
+ * used when passing the extension into MediumEditor via the
1240
+ * 'extensions' option
1241
+ */
1242
+ name: null,
1243
+
1244
+ /* checkState: [function (node)]
1245
+ *
1246
+ * If implemented, this function will be called one or more times
1247
+ * the state of the editor & toolbar are updated.
1248
+ * When the state is updated, the editor does the following:
1249
+ *
1250
+ * 1) Find the parent node containing the current selection
1251
+ * 2) Call checkState on the extension, passing the node as an argument
1252
+ * 3) Get tha parent node of the previous node
1253
+ * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable
1254
+ */
1255
+ checkState: null,
1256
+
1257
+ /* getButton: [function ()]
1258
+ *
1259
+ * If implemented, this function will be called when
1260
+ * the toolbar is being created. The DOM Element returned
1261
+ * by this function will be appended to the toolbar along
1262
+ * with any other buttons.
1263
+ */
1264
+ getButton: null,
1265
+
1266
+ /* As alternatives to checkState, these functions provide a more structured
1267
+ * path to updating the state of an extension (usually a button) whenever
1268
+ * the state of the editor & toolbar are updated.
1269
+ */
1270
+
1271
+ /* queryCommandState: [function ()]
1272
+ *
1273
+ * If implemented, this function will be called once on each extension
1274
+ * when the state of the editor/toolbar is being updated.
1275
+ *
1276
+ * If this function returns a non-null value, the exntesion will
1277
+ * be ignored as the code climbs the dom tree.
1278
+ *
1279
+ * If this function returns true, and the setActive() function is defined
1280
+ * setActive() will be called
1281
+ */
1282
+ queryCommandState: null,
1283
+
1284
+ /* isActive: [function ()]
1285
+ *
1286
+ * If implemented, this function will be called when MediumEditor
1287
+ * has determined that this extension is 'active' for the current selection.
1288
+ * This may be called when the editor & toolbar are being updated,
1289
+ * but only if queryCommandState() or isAlreadyApplied() functions
1290
+ * are implemented, and when called, return true.
1291
+ */
1292
+ isActive: null,
1293
+
1294
+ /* isAlreadyApplied: [function (node)]
1295
+ *
1296
+ * If implemented, this function is similar to checkState() in
1297
+ * that it will be calle repeatedly as MediumEditor moves up
1298
+ * the DOM to update the editor & toolbar after a state change.
1299
+ *
1300
+ * NOTE: This function will NOT be called if checkState() has
1301
+ * been implemented. This function will NOT be called if
1302
+ * queryCommandState() is implemented and returns a non-null
1303
+ * value when called
1304
+ */
1305
+ isAlreadyApplied: null,
1306
+
1307
+ /* setActive: [function ()]
1308
+ *
1309
+ * If implemented, this function is called when MediumEditor knows
1310
+ * that this extension is currently enabled. Currently, this
1311
+ * function is called when updating the editor & toolbar, and
1312
+ * only if queryCommandState() or isAlreadyApplied(node) return
1313
+ * true when called
1314
+ */
1315
+ setActive: null,
1316
+
1317
+ /* setInactive: [function ()]
1318
+ *
1319
+ * If implemented, this function is called when MediumEditor knows
1320
+ * that this extension is currently disabled. Curently, this
1321
+ * is called at the beginning of each state change for
1322
+ * the editor & toolbar. After calling this, MediumEditor
1323
+ * will attempt to update the extension, either via checkState()
1324
+ * or the combination of queryCommandState(), isAlreadyApplied(node),
1325
+ * isActive(), and setActive()
1326
+ */
1327
+ setInactive: null,
1328
+
1329
+
1330
+ /* onHide: [function ()]
1331
+ *
1332
+ * If implemented, this function is called each time the
1333
+ * toolbar is hidden
1334
+ */
1335
+ onHide: null
1336
+ };
1337
+
1338
+ })();
1339
+ var Selection;
1340
+
1341
+ (function () {
1342
+ 'use strict';
1343
+
1344
+ Selection = {
1345
+ findMatchingSelectionParent: function (testElementFunction, contentWindow) {
1346
+ var selection = contentWindow.getSelection(), range, current;
1347
+
1348
+ if (selection.rangeCount === 0) {
1349
+ return false;
1350
+ }
1351
+
1352
+ range = selection.getRangeAt(0);
1353
+ current = range.commonAncestorContainer;
1354
+
1355
+ return Util.traverseUp(current, testElementFunction);
1356
+ },
1357
+
1358
+ getSelectionElement: function (contentWindow) {
1359
+ return this.findMatchingSelectionParent(function (el) {
1360
+ return el.getAttribute('data-medium-element');
1361
+ }, contentWindow);
1362
+ },
1363
+
1364
+ selectionInContentEditableFalse: function (contentWindow) {
1365
+ return this.findMatchingSelectionParent(function (el) {
1366
+ return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
1367
+ }, contentWindow);
1368
+ },
1369
+
1370
+ // http://stackoverflow.com/questions/4176923/html-of-selected-text
1371
+ // by Tim Down
1372
+ getSelectionHtml: function getSelectionHtml() {
1373
+ var i,
1374
+ html = '',
1375
+ sel = this.options.contentWindow.getSelection(),
1376
+ len,
1377
+ container;
1378
+ if (sel.rangeCount) {
1379
+ container = this.options.ownerDocument.createElement('div');
1380
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
1381
+ container.appendChild(sel.getRangeAt(i).cloneContents());
1382
+ }
1383
+ html = container.innerHTML;
1384
+ }
1385
+ return html;
1386
+ },
1387
+
1388
+ /**
1389
+ * Find the caret position within an element irrespective of any inline tags it may contain.
1390
+ *
1391
+ * @param {DOMElement} An element containing the cursor to find offsets relative to.
1392
+ * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
1393
+ * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
1394
+ */
1395
+ getCaretOffsets: function getCaretOffsets(element, range) {
1396
+ var preCaretRange, postCaretRange;
1397
+
1398
+ if (!range) {
1399
+ range = window.getSelection().getRangeAt(0);
1400
+ }
1401
+
1402
+ preCaretRange = range.cloneRange();
1403
+ postCaretRange = range.cloneRange();
1404
+
1405
+ preCaretRange.selectNodeContents(element);
1406
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
1407
+
1408
+ postCaretRange.selectNodeContents(element);
1409
+ postCaretRange.setStart(range.endContainer, range.endOffset);
1410
+
1411
+ return {
1412
+ left: preCaretRange.toString().length,
1413
+ right: postCaretRange.toString().length
1414
+ };
1415
+ },
1416
+
1417
+ // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
1418
+ rangeSelectsSingleNode: function (range) {
1419
+ var startNode = range.startContainer;
1420
+ return startNode === range.endContainer &&
1421
+ startNode.hasChildNodes() &&
1422
+ range.endOffset === range.startOffset + 1;
1423
+ },
1424
+
1425
+ getSelectedParentElement: function (range) {
1426
+ var selectedParentElement = null;
1427
+ if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
1428
+ selectedParentElement = range.startContainer.childNodes[range.startOffset];
1429
+ } else if (range.startContainer.nodeType === 3) {
1430
+ selectedParentElement = range.startContainer.parentNode;
1431
+ } else {
1432
+ selectedParentElement = range.startContainer;
1433
+ }
1434
+ return selectedParentElement;
1435
+ },
1436
+
1437
+ getSelectedElements: function (doc) {
1438
+ var selection = doc.getSelection(),
1439
+ range,
1440
+ toRet,
1441
+ currNode;
1442
+
1443
+ if (!selection.rangeCount ||
1444
+ !selection.getRangeAt(0).commonAncestorContainer) {
1445
+ return [];
1446
+ }
1447
+
1448
+ range = selection.getRangeAt(0);
1449
+
1450
+ if (range.commonAncestorContainer.nodeType === 3) {
1451
+ toRet = [];
1452
+ currNode = range.commonAncestorContainer;
1453
+ while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {
1454
+ toRet.push(currNode.parentNode);
1455
+ currNode = currNode.parentNode;
1456
+ }
1457
+
1458
+ return toRet;
1459
+ }
1460
+
1461
+ return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {
1462
+ return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true;
1463
+ });
1464
+ },
1465
+
1466
+ selectNode: function (node, doc) {
1467
+ var range = doc.createRange(),
1468
+ sel = doc.getSelection();
1469
+
1470
+ range.selectNodeContents(node);
1471
+ sel.removeAllRanges();
1472
+ sel.addRange(range);
1473
+ }
1474
+ };
1475
+ }());
1476
+
1477
+ var Events;
1478
+
1479
+ (function () {
1480
+ 'use strict';
1481
+
1482
+ Events = function (instance) {
1483
+ this.base = instance;
1484
+ this.options = this.base.options;
1485
+ this.events = [];
1486
+ this.customEvents = {};
1487
+ this.listeners = {};
1488
+ };
1489
+
1490
+ Events.prototype = {
1491
+
1492
+ // Helpers for event handling
1493
+
1494
+ attachDOMEvent: function (target, event, listener, useCapture) {
1495
+ target.addEventListener(event, listener, useCapture);
1496
+ this.events.push([target, event, listener, useCapture]);
1497
+ },
1498
+
1499
+ detachDOMEvent: function (target, event, listener, useCapture) {
1500
+ var index = this.indexOfListener(target, event, listener, useCapture),
1501
+ e;
1502
+ if (index !== -1) {
1503
+ e = this.events.splice(index, 1)[0];
1504
+ e[0].removeEventListener(e[1], e[2], e[3]);
1505
+ }
1506
+ },
1507
+
1508
+ indexOfListener: function (target, event, listener, useCapture) {
1509
+ var i, n, item;
1510
+ for (i = 0, n = this.events.length; i < n; i = i + 1) {
1511
+ item = this.events[i];
1512
+ if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
1513
+ return i;
1514
+ }
1515
+ }
1516
+ return -1;
1517
+ },
1518
+
1519
+ detachAllDOMEvents: function () {
1520
+ var e = this.events.pop();
1521
+ while (e) {
1522
+ e[0].removeEventListener(e[1], e[2], e[3]);
1523
+ e = this.events.pop();
1524
+ }
1525
+ },
1526
+
1527
+ // custom events
1528
+ attachCustomEvent: function (event, listener) {
1529
+ this.setupListener(event);
1530
+ // If we don't suppot this custom event, don't do anything
1531
+ if (this.listeners[event]) {
1532
+ if (!this.customEvents[event]) {
1533
+ this.customEvents[event] = [];
1534
+ }
1535
+ this.customEvents[event].push(listener);
1536
+ }
1537
+ },
1538
+
1539
+ detachCustomEvent: function (event, listener) {
1540
+ var index = this.indexOfCustomListener(event, listener);
1541
+ if (index !== -1) {
1542
+ this.customEvents[event].splice(index, 1);
1543
+ // TODO: If array is empty, should detach internal listeners via destoryListener()
1544
+ }
1545
+ },
1546
+
1547
+ indexOfCustomListener: function (event, listener) {
1548
+ if (!this.customEvents[event] || !this.customEvents[event].length) {
1549
+ return -1;
1550
+ }
1551
+
1552
+ return this.customEvents[event].indexOf(listener);
1553
+ },
1554
+
1555
+ detachAllCustomEvents: function () {
1556
+ this.customEvents = {};
1557
+ // TODO: Should detach internal listeners here via destroyListener()
1558
+ },
1559
+
1560
+ triggerCustomEvent: function (name, data, editable) {
1561
+ if (this.customEvents[name]) {
1562
+ this.customEvents[name].forEach(function (listener) {
1563
+ listener(data, editable);
1564
+ });
1565
+ }
1566
+ },
1567
+
1568
+ // Listening to browser events to emit events medium-editor cares about
1569
+
1570
+ setupListener: function (name) {
1571
+ if (this.listeners[name]) {
1572
+ return;
1573
+ }
1574
+
1575
+ switch (name) {
1576
+ case 'externalInteraction':
1577
+ // Detecting when user has interacted with elements outside of MediumEditor
1578
+ this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true);
1579
+ this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true);
1580
+ this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true);
1581
+ this.listeners[name] = true;
1582
+ break;
1583
+ case 'blur':
1584
+ // Detecting when focus is lost
1585
+ this.setupListener('externalInteraction');
1586
+ this.listeners[name] = true;
1587
+ break;
1588
+ case 'focus':
1589
+ // Detecting when focus moves into some part of MediumEditor
1590
+ this.setupListener('externalInteraction');
1591
+ this.listeners[name] = true;
1592
+ break;
1593
+ case 'editableClick':
1594
+ // Detecting click in the contenteditables
1595
+ this.base.elements.forEach(function (element) {
1596
+ this.attachDOMEvent(element, 'click', this.handleClick.bind(this));
1597
+ }.bind(this));
1598
+ this.listeners[name] = true;
1599
+ break;
1600
+ case 'editableBlur':
1601
+ // Detecting blur in the contenteditables
1602
+ this.base.elements.forEach(function (element) {
1603
+ this.attachDOMEvent(element, 'blur', this.handleBlur.bind(this));
1604
+ }.bind(this));
1605
+ this.listeners[name] = true;
1606
+ break;
1607
+ case 'editableKeypress':
1608
+ // Detecting keypress in the contenteditables
1609
+ this.base.elements.forEach(function (element) {
1610
+ this.attachDOMEvent(element, 'keypress', this.handleKeypress.bind(this));
1611
+ }.bind(this));
1612
+ this.listeners[name] = true;
1613
+ break;
1614
+ case 'editableKeyup':
1615
+ // Detecting keyup in the contenteditables
1616
+ this.base.elements.forEach(function (element) {
1617
+ this.attachDOMEvent(element, 'keyup', this.handleKeyup.bind(this));
1618
+ }.bind(this));
1619
+ this.listeners[name] = true;
1620
+ break;
1621
+ case 'editableKeydown':
1622
+ // Detecting keydown on the contenteditables
1623
+ this.base.elements.forEach(function (element) {
1624
+ this.attachDOMEvent(element, 'keydown', this.handleKeydown.bind(this));
1625
+ }.bind(this));
1626
+ this.listeners[name] = true;
1627
+ break;
1628
+ case 'editableKeydownEnter':
1629
+ // Detecting keydown for ENTER on the contenteditables
1630
+ this.setupListener('editableKeydown');
1631
+ this.listeners[name] = true;
1632
+ break;
1633
+ case 'editableKeydownTab':
1634
+ // Detecting keydown for TAB on the contenteditable
1635
+ this.setupListener('editableKeydown');
1636
+ this.listeners[name] = true;
1637
+ break;
1638
+ case 'editableKeydownDelete':
1639
+ // Detecting keydown for DELETE/BACKSPACE on the contenteditables
1640
+ this.setupListener('editableKeydown');
1641
+ this.listeners[name] = true;
1642
+ break;
1643
+ case 'editableMouseover':
1644
+ // Detecting mouseover on the contenteditables
1645
+ this.base.elements.forEach(function (element) {
1646
+ this.attachDOMEvent(element, 'mouseover', this.handleMouseover.bind(this));
1647
+ }, this);
1648
+ this.listeners[name] = true;
1649
+ break;
1650
+ case 'editableDrag':
1651
+ // Detecting dragover and dragleave on the contenteditables
1652
+ this.base.elements.forEach(function (element) {
1653
+ this.attachDOMEvent(element, 'dragover', this.handleDragging.bind(this));
1654
+ this.attachDOMEvent(element, 'dragleave', this.handleDragging.bind(this));
1655
+ }, this);
1656
+ this.listeners[name] = true;
1657
+ break;
1658
+ case 'editableDrop':
1659
+ // Detecting drop on the contenteditables
1660
+ this.base.elements.forEach(function (element) {
1661
+ this.attachDOMEvent(element, 'drop', this.handleDrop.bind(this));
1662
+ }, this);
1663
+ this.listeners[name] = true;
1664
+ break;
1665
+ case 'editablePaste':
1666
+ // Detecting paste on the contenteditables
1667
+ this.base.elements.forEach(function (element) {
1668
+ this.attachDOMEvent(element, 'paste', this.handlePaste.bind(this));
1669
+ }, this);
1670
+ this.listeners[name] = true;
1671
+ break;
1672
+ }
1673
+ },
1674
+
1675
+ focusElement: function (element) {
1676
+ element.focus();
1677
+ this.updateFocus(element, { target: element, type: 'focus' });
1678
+ },
1679
+
1680
+ updateFocus: function (target, eventObj) {
1681
+ var toolbarEl = this.base.toolbar ? this.base.toolbar.getToolbarElement() : null,
1682
+ anchorPreview = this.base.getExtensionByName('anchor-preview'),
1683
+ previewEl = (anchorPreview && anchorPreview.getPreviewElement) ? anchorPreview.getPreviewElement() : null,
1684
+ hadFocus,
1685
+ toFocus;
1686
+
1687
+ this.base.elements.some(function (element) {
1688
+ // Find the element that has focus
1689
+ if (!hadFocus && element.getAttribute('data-medium-focused')) {
1690
+ hadFocus = element;
1691
+ }
1692
+
1693
+ // bail if we found the element that had focus
1694
+ return !!hadFocus;
1695
+ }, this);
1696
+
1697
+ // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element.
1698
+ // If so, we don't want to focus another element
1699
+ if (hadFocus &&
1700
+ eventObj.type === 'click' &&
1701
+ this.lastMousedownTarget &&
1702
+ (Util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
1703
+ Util.isDescendant(toolbarEl, this.lastMousedownTarget, true) ||
1704
+ Util.isDescendant(previewEl, this.lastMousedownTarget, true))) {
1705
+ toFocus = hadFocus;
1706
+ }
1707
+
1708
+ if (!toFocus) {
1709
+ this.base.elements.some(function (element) {
1710
+ // If the target is part of an editor element, this is the element getting focus
1711
+ if (!toFocus && (Util.isDescendant(element, target, true))) {
1712
+ toFocus = element;
1713
+ }
1714
+
1715
+ // bail if we found an element that's getting focus
1716
+ return !!toFocus;
1717
+ }, this);
1718
+ }
1719
+
1720
+ // Check if the target is external (not part of the editor, toolbar, or anchorpreview)
1721
+ var externalEvent = !Util.isDescendant(hadFocus, target, true) &&
1722
+ !Util.isDescendant(toolbarEl, target, true) &&
1723
+ !Util.isDescendant(previewEl, target, true);
1724
+
1725
+ if (toFocus !== hadFocus) {
1726
+ // If element has focus, and focus is going outside of editor
1727
+ // Don't blur focused element if clicking on editor, toolbar, or anchorpreview
1728
+ if (hadFocus && externalEvent) {
1729
+ // Trigger blur on the editable that has lost focus
1730
+ hadFocus.removeAttribute('data-medium-focused');
1731
+ this.triggerCustomEvent('blur', eventObj, hadFocus);
1732
+ }
1733
+
1734
+ // If focus is going into an editor element
1735
+ if (toFocus) {
1736
+ // Trigger focus on the editable that now has focus
1737
+ toFocus.setAttribute('data-medium-focused', true);
1738
+ this.triggerCustomEvent('focus', eventObj, toFocus);
1739
+ }
1740
+ }
1741
+
1742
+ if (externalEvent) {
1743
+ this.triggerCustomEvent('externalInteraction', eventObj);
1744
+ }
1745
+ },
1746
+
1747
+ handleBodyClick: function (event) {
1748
+ this.updateFocus(event.target, event);
1749
+ },
1750
+
1751
+ handleBodyFocus: function (event) {
1752
+ this.updateFocus(event.target, event);
1753
+ },
1754
+
1755
+ handleBodyMousedown: function (event) {
1756
+ this.lastMousedownTarget = event.target;
1757
+ },
1758
+
1759
+ handleClick: function (event) {
1760
+ this.triggerCustomEvent('editableClick', event, event.currentTarget);
1761
+ },
1762
+
1763
+ handleBlur: function (event) {
1764
+ this.triggerCustomEvent('editableBlur', event, event.currentTarget);
1765
+ },
1766
+
1767
+ handleKeypress: function (event) {
1768
+ this.triggerCustomEvent('editableKeypress', event, event.currentTarget);
1769
+ },
1770
+
1771
+ handleKeyup: function (event) {
1772
+ this.triggerCustomEvent('editableKeyup', event, event.currentTarget);
1773
+ },
1774
+
1775
+ handleMouseover: function (event) {
1776
+ this.triggerCustomEvent('editableMouseover', event, event.currentTarget);
1777
+ },
1778
+
1779
+ handleDragging: function (event) {
1780
+ this.triggerCustomEvent('editableDrag', event, event.currentTarget);
1781
+ },
1782
+
1783
+ handleDrop: function (event) {
1784
+ this.triggerCustomEvent('editableDrop', event, event.currentTarget);
1785
+ },
1786
+
1787
+ handlePaste: function (event) {
1788
+ this.triggerCustomEvent('editablePaste', event, event.currentTarget);
1789
+ },
1790
+
1791
+ handleKeydown: function (event) {
1792
+ this.triggerCustomEvent('editableKeydown', event, event.currentTarget);
1793
+
1794
+ switch (event.which) {
1795
+ case Util.keyCode.ENTER:
1796
+ this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget);
1797
+ break;
1798
+ case Util.keyCode.TAB:
1799
+ this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget);
1800
+ break;
1801
+ case Util.keyCode.DELETE:
1802
+ case Util.keyCode.BACKSPACE:
1803
+ this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget);
1804
+ break;
1805
+ }
1806
+ }
1807
+ };
1808
+
1809
+ }());
1810
+
1811
+ var DefaultButton;
1812
+
1813
+ (function () {
1814
+ 'use strict';
1815
+
1816
+ DefaultButton = function (options, instance) {
1817
+ this.options = options;
1818
+ this.name = options.name;
1819
+ this.init(instance);
1820
+ };
1821
+
1822
+ DefaultButton.prototype = {
1823
+ init: function (instance) {
1824
+ this.base = instance;
1825
+
1826
+ this.button = this.createButton();
1827
+ this.base.on(this.button, 'click', this.handleClick.bind(this));
1828
+ if (this.options.key) {
1829
+ this.base.subscribe('editableKeydown', this.handleKeydown.bind(this));
1830
+ }
1831
+ },
1832
+ getButton: function () {
1833
+ return this.button;
1834
+ },
1835
+ getAction: function () {
1836
+ return (typeof this.options.action === 'function') ? this.options.action(this.base.options) : this.options.action;
1837
+ },
1838
+ getAria: function () {
1839
+ return (typeof this.options.aria === 'function') ? this.options.aria(this.base.options) : this.options.aria;
1840
+ },
1841
+ getTagNames: function () {
1842
+ return (typeof this.options.tagNames === 'function') ? this.options.tagNames(this.base.options) : this.options.tagNames;
1843
+ },
1844
+ createButton: function () {
1845
+ var button = this.base.options.ownerDocument.createElement('button'),
1846
+ content = this.options.contentDefault,
1847
+ ariaLabel = this.getAria();
1848
+ button.classList.add('medium-editor-action');
1849
+ button.classList.add('medium-editor-action-' + this.name);
1850
+ button.setAttribute('data-action', this.getAction());
1851
+ if (ariaLabel) {
1852
+ button.setAttribute('title', ariaLabel);
1853
+ button.setAttribute('aria-label', ariaLabel);
1854
+ }
1855
+ if (this.base.options.buttonLabels) {
1856
+ if (this.base.options.buttonLabels === 'fontawesome' && this.options.contentFA) {
1857
+ content = this.options.contentFA;
1858
+ } else if (typeof this.base.options.buttonLabels === 'object' && this.base.options.buttonLabels[this.name]) {
1859
+ content = this.base.options.buttonLabels[this.options.name];
1860
+ }
1861
+ }
1862
+ button.innerHTML = content;
1863
+ return button;
1864
+ },
1865
+ handleKeydown: function (evt) {
1866
+ var key, action;
1867
+
1868
+ if (evt.ctrlKey || evt.metaKey) {
1869
+ key = String.fromCharCode(evt.which || evt.keyCode).toLowerCase();
1870
+ if (this.options.key === key) {
1871
+ evt.preventDefault();
1872
+ evt.stopPropagation();
1873
+
1874
+ action = this.getAction();
1875
+ if (action) {
1876
+ this.base.execAction(action);
1877
+ }
1878
+ }
1879
+ }
1880
+ },
1881
+ handleClick: function (evt) {
1882
+ evt.preventDefault();
1883
+ evt.stopPropagation();
1884
+
1885
+ var action = this.getAction();
1886
+
1887
+ if (action) {
1888
+ this.base.execAction(action);
1889
+ }
1890
+ },
1891
+ isActive: function () {
1892
+ return this.button.classList.contains(this.base.options.activeButtonClass);
1893
+ },
1894
+ setInactive: function () {
1895
+ this.button.classList.remove(this.base.options.activeButtonClass);
1896
+ delete this.knownState;
1897
+ },
1898
+ setActive: function () {
1899
+ this.button.classList.add(this.base.options.activeButtonClass);
1900
+ delete this.knownState;
1901
+ },
1902
+ queryCommandState: function () {
1903
+ var queryState = null;
1904
+ if (this.options.useQueryState) {
1905
+ queryState = this.base.queryCommandState(this.getAction());
1906
+ }
1907
+ return queryState;
1908
+ },
1909
+ isAlreadyApplied: function (node) {
1910
+ var isMatch = false,
1911
+ tagNames = this.getTagNames(),
1912
+ styleVals,
1913
+ computedStyle;
1914
+
1915
+ if (this.knownState === false || this.knownState === true) {
1916
+ return this.knownState;
1917
+ }
1918
+
1919
+ if (tagNames && tagNames.length > 0 && node.tagName) {
1920
+ isMatch = tagNames.indexOf(node.tagName.toLowerCase()) !== -1;
1921
+ }
1922
+
1923
+ if (!isMatch && this.options.style) {
1924
+ styleVals = this.options.style.value.split('|');
1925
+ computedStyle = this.base.options.contentWindow.getComputedStyle(node, null).getPropertyValue(this.options.style.prop);
1926
+ styleVals.forEach(function (val) {
1927
+ if (!this.knownState) {
1928
+ isMatch = (computedStyle.indexOf(val) !== -1);
1929
+ // text-decoration is not inherited by default
1930
+ // so if the computed style for text-decoration doesn't match
1931
+ // don't write to knownState so we can fallback to other checks
1932
+ if (isMatch || this.options.style.prop !== 'text-decoration') {
1933
+ this.knownState = isMatch;
1934
+ }
1935
+ }
1936
+ }, this);
1937
+ }
1938
+
1939
+ return isMatch;
1940
+ }
1941
+ };
1942
+ }());
1943
+
1944
+ var PasteHandler;
1945
+
1946
+ (function () {
1947
+ 'use strict';
1948
+ /*jslint regexp: true*/
1949
+ /*
1950
+ jslint does not allow character negation, because the negation
1951
+ will not match any unicode characters. In the regexes in this
1952
+ block, negation is used specifically to match the end of an html
1953
+ tag, and in fact unicode characters *should* be allowed.
1954
+ */
1955
+ function createReplacements() {
1956
+ return [
1957
+
1958
+ // replace two bogus tags that begin pastes from google docs
1959
+ [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ""],
1960
+ [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ""],
1961
+
1962
+ // un-html spaces and newlines inserted by OS X
1963
+ [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
1964
+ [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
1965
+
1966
+ // replace google docs italics+bold with a span to be replaced once the html is inserted
1967
+ [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
1968
+
1969
+ // replace google docs italics with a span to be replaced once the html is inserted
1970
+ [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
1971
+
1972
+ //[replace google docs bolds with a span to be replaced once the html is inserted
1973
+ [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
1974
+
1975
+ // replace manually entered b/i/a tags with real ones
1976
+ [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
1977
+
1978
+ // replace manually a tags with real ones, converting smart-quotes from google docs
1979
+ [new RegExp(/&lt;a(?:(?!href).)+href=(?:&quot;|&rdquo;|&ldquo;|"|“|”)(((?!&quot;|&rdquo;|&ldquo;|"|“|”).)*)(?:&quot;|&rdquo;|&ldquo;|"|“|”)(?:(?!&gt;).)*&gt;/gi), '<a href="$1">'],
1980
+
1981
+ // Newlines between paragraphs in html have no syntactic value,
1982
+ // but then have a tendency to accidentally become additional paragraphs down the line
1983
+ [new RegExp(/<\/p>\n+/gi), '</p>'],
1984
+ [new RegExp(/\n+<p/gi), '<p'],
1985
+
1986
+ // Microsoft Word makes these odd tags, like <o:p></o:p>
1987
+ [new RegExp(/<\/?o:[a-z]*>/gi), '']
1988
+ ];
1989
+ }
1990
+ /*jslint regexp: false*/
1991
+
1992
+ PasteHandler = Extension.extend({
1993
+
1994
+ /* Paste Options */
1995
+
1996
+ /* forcePlainText: [boolean]
1997
+ * Forces pasting as plain text.
1998
+ */
1999
+ forcePlainText: true,
2000
+
2001
+ /* cleanPastedHTML: [boolean]
2002
+ * cleans pasted content from different sources, like google docs etc.
2003
+ */
2004
+ cleanPastedHTML: false,
2005
+
2006
+ /* cleanReplacements: [Array]
2007
+ * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when
2008
+ * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
2009
+ */
2010
+ cleanReplacements: [],
2011
+
2012
+ /* cleanAttrs:: [Array]
2013
+ * list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when
2014
+ * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
2015
+ */
2016
+ cleanAttrs: ['class', 'style', 'dir'],
2017
+
2018
+ /* cleanTags: [Array]
2019
+ * list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when
2020
+ * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
2021
+ */
2022
+ cleanTags: ['meta'],
2023
+
2024
+ /* ----- internal options needed from base ----- */
2025
+ "window": window,
2026
+ "document": document,
2027
+ targetBlank: false,
2028
+ disableReturn: false,
2029
+
2030
+ // Need a reference to MediumEditor (this.base)
2031
+ parent: true,
2032
+
2033
+ init: function () {
2034
+ if (this.forcePlainText || this.cleanPastedHTML) {
2035
+ this.base.subscribe('editablePaste', this.handlePaste.bind(this));
2036
+ }
2037
+ },
2038
+
2039
+ handlePaste: function (event, element) {
2040
+ var paragraphs,
2041
+ html = '',
2042
+ p,
2043
+ dataFormatHTML = 'text/html',
2044
+ dataFormatPlain = 'text/plain',
2045
+ pastedHTML,
2046
+ pastedPlain;
2047
+
2048
+ if (this.window.clipboardData && event.clipboardData === undefined) {
2049
+ event.clipboardData = this.window.clipboardData;
2050
+ // If window.clipboardData exists, but event.clipboardData doesn't exist,
2051
+ // we're probably in IE. IE only has two possibilities for clipboard
2052
+ // data format: 'Text' and 'URL'.
2053
+ //
2054
+ // Of the two, we want 'Text':
2055
+ dataFormatHTML = 'Text';
2056
+ dataFormatPlain = 'Text';
2057
+ }
2058
+
2059
+ if (event.clipboardData &&
2060
+ event.clipboardData.getData &&
2061
+ !event.defaultPrevented) {
2062
+ event.preventDefault();
2063
+
2064
+ pastedHTML = event.clipboardData.getData(dataFormatHTML);
2065
+ pastedPlain = event.clipboardData.getData(dataFormatPlain);
2066
+
2067
+ if (!pastedHTML) {
2068
+ pastedHTML = pastedPlain;
2069
+ }
2070
+
2071
+ if (this.cleanPastedHTML && pastedHTML) {
2072
+ return this.cleanPaste(pastedHTML);
2073
+ }
2074
+
2075
+ if (!(this.disableReturn || element.getAttribute('data-disable-return'))) {
2076
+ paragraphs = pastedPlain.split(/[\r\n]+/g);
2077
+ // If there are no \r\n in data, don't wrap in <p>
2078
+ if (paragraphs.length > 1) {
2079
+ for (p = 0; p < paragraphs.length; p += 1) {
2080
+ if (paragraphs[p] !== '') {
2081
+ html += '<p>' + Util.htmlEntities(paragraphs[p]) + '</p>';
2082
+ }
2083
+ }
2084
+ } else {
2085
+ html = Util.htmlEntities(paragraphs[0]);
2086
+ }
2087
+ } else {
2088
+ html = Util.htmlEntities(pastedPlain);
2089
+ }
2090
+ Util.insertHTMLCommand(this.document, html);
2091
+ }
2092
+ },
2093
+
2094
+ cleanPaste: function (text) {
2095
+ var i, elList, workEl,
2096
+ el = Selection.getSelectionElement(this.window),
2097
+ multiline = /<p|<br|<div/.test(text),
2098
+ replacements = createReplacements().concat(this.cleanReplacements || []);
2099
+
2100
+ for (i = 0; i < replacements.length; i += 1) {
2101
+ text = text.replace(replacements[i][0], replacements[i][1]);
2102
+ }
2103
+
2104
+ if (multiline) {
2105
+ // double br's aren't converted to p tags, but we want paragraphs.
2106
+ elList = text.split('<br><br>');
2107
+
2108
+ this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>');
2109
+
2110
+ try {
2111
+ this.document.execCommand('insertText', false, "\n");
2112
+ } catch (ignore) { }
2113
+
2114
+ // block element cleanup
2115
+ elList = el.querySelectorAll('a,p,div,br');
2116
+ for (i = 0; i < elList.length; i += 1) {
2117
+ workEl = elList[i];
2118
+
2119
+ // Microsoft Word replaces some spaces with newlines.
2120
+ // While newlines between block elements are meaningless, newlines within
2121
+ // elements are sometimes actually spaces.
2122
+ workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' ');
2123
+
2124
+ switch (workEl.tagName.toLowerCase()) {
2125
+ case 'a':
2126
+ if (this.targetBlank) {
2127
+ Util.setTargetBlank(workEl);
2128
+ }
2129
+ break;
2130
+ case 'p':
2131
+ case 'div':
2132
+ this.filterCommonBlocks(workEl);
2133
+ break;
2134
+ case 'br':
2135
+ this.filterLineBreak(workEl);
2136
+ break;
2137
+ }
2138
+ }
2139
+ } else {
2140
+ this.pasteHTML(text);
2141
+ }
2142
+ },
2143
+
2144
+ pasteHTML: function (html, options) {
2145
+ options = Util.defaults({}, options, {
2146
+ cleanAttrs: this.cleanAttrs,
2147
+ cleanTags: this.cleanTags
2148
+ });
2149
+
2150
+ var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();
2151
+
2152
+ pasteBlock.appendChild(this.document.createElement('body'));
2153
+
2154
+ fragmentBody = pasteBlock.querySelector('body');
2155
+ fragmentBody.innerHTML = html;
2156
+
2157
+ this.cleanupSpans(fragmentBody);
2158
+
2159
+ elList = fragmentBody.querySelectorAll('*');
2160
+
2161
+ for (i = 0; i < elList.length; i += 1) {
2162
+ workEl = elList[i];
2163
+ Util.cleanupAttrs(workEl, options.cleanAttrs);
2164
+ Util.cleanupTags(workEl, options.cleanTags);
2165
+ }
2166
+
2167
+ Util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
2168
+ },
2169
+
2170
+ isCommonBlock: function (el) {
2171
+ return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
2172
+ },
2173
+
2174
+ filterCommonBlocks: function (el) {
2175
+ if (/^\s*$/.test(el.textContent) && el.parentNode) {
2176
+ el.parentNode.removeChild(el);
2177
+ }
2178
+ },
2179
+
2180
+ filterLineBreak: function (el) {
2181
+
2182
+ if (this.isCommonBlock(el.previousElementSibling)) {
2183
+ // remove stray br's following common block elements
2184
+ this.removeWithParent(el);
2185
+ } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
2186
+ // remove br's just inside open or close tags of a div/p
2187
+ this.removeWithParent(el);
2188
+ } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
2189
+ // and br's that are the only child of elements other than div/p
2190
+ this.removeWithParent(el);
2191
+ }
2192
+ },
2193
+
2194
+ // remove an element, including its parent, if it is the only element within its parent
2195
+ removeWithParent: function (el) {
2196
+ if (el && el.parentNode) {
2197
+ if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
2198
+ el.parentNode.parentNode.removeChild(el.parentNode);
2199
+ } else {
2200
+ el.parentNode.removeChild(el);
2201
+ }
2202
+ }
2203
+ },
2204
+
2205
+ cleanupSpans: function (container_el) {
2206
+ var i,
2207
+ el,
2208
+ new_el,
2209
+ spans = container_el.querySelectorAll('.replace-with'),
2210
+ isCEF = function (el) {
2211
+ return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
2212
+ };
2213
+
2214
+ for (i = 0; i < spans.length; i += 1) {
2215
+ el = spans[i];
2216
+ new_el = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');
2217
+
2218
+ if (el.classList.contains('bold') && el.classList.contains('italic')) {
2219
+ // add an i tag as well if this has both italics and bold
2220
+ new_el.innerHTML = '<i>' + el.innerHTML + '</i>';
2221
+ } else {
2222
+ new_el.innerHTML = el.innerHTML;
2223
+ }
2224
+ el.parentNode.replaceChild(new_el, el);
2225
+ }
2226
+
2227
+ spans = container_el.querySelectorAll('span');
2228
+ for (i = 0; i < spans.length; i += 1) {
2229
+ el = spans[i];
2230
+
2231
+ // bail if span is in contenteditable = false
2232
+ if (Util.traverseUp(el, isCEF)) {
2233
+ return false;
2234
+ }
2235
+
2236
+ // remove empty spans, replace others with their contents
2237
+ Util.unwrap(el, this.document);
2238
+ }
2239
+ }
2240
+ });
2241
+
2242
+ }());
2243
+
2244
+ var AnchorExtension;
2245
+
2246
+ (function () {
2247
+ 'use strict';
2248
+
2249
+ function AnchorDerived() {
2250
+ this.parent = true;
2251
+ this.options = {
2252
+ name: 'anchor',
2253
+ action: 'createLink',
2254
+ aria: 'link',
2255
+ tagNames: ['a'],
2256
+ contentDefault: '<b>#</b>',
2257
+ contentFA: '<i class="fa fa-link"></i>'
2258
+ };
2259
+ this.name = 'anchor';
2260
+ this.hasForm = true;
2261
+ }
2262
+
2263
+ AnchorDerived.prototype = {
2264
+
2265
+ // Button and Extension handling
2266
+
2267
+ // labels for the anchor-edit form buttons
2268
+ formSaveLabel: '&#10003;',
2269
+ formCloseLabel: '&times;',
2270
+
2271
+ // Called when the button the toolbar is clicked
2272
+ // Overrides DefaultButton.handleClick
2273
+ handleClick: function (evt) {
2274
+ evt.preventDefault();
2275
+ evt.stopPropagation();
2276
+
2277
+ var selectedParentElement = Selection.getSelectedParentElement(Util.getSelectionRange(this.base.options.ownerDocument));
2278
+ if (selectedParentElement.tagName &&
2279
+ selectedParentElement.tagName.toLowerCase() === 'a') {
2280
+ return this.base.execAction('unlink');
2281
+ }
2282
+
2283
+ if (!this.isDisplayed()) {
2284
+ this.showForm();
2285
+ }
2286
+
2287
+ return false;
2288
+ },
2289
+
2290
+ // Called by medium-editor to append form to the toolbar
2291
+ getForm: function () {
2292
+ if (!this.form) {
2293
+ this.form = this.createForm();
2294
+ }
2295
+ return this.form;
2296
+ },
2297
+
2298
+ getTemplate: function () {
2299
+
2300
+ var template = [
2301
+ '<input type="text" class="medium-editor-toolbar-input" placeholder="', this.base.options.anchorInputPlaceholder, '">'
2302
+ ];
2303
+
2304
+ template.push(
2305
+ '<a href="#" class="medium-editor-toolbar-save">',
2306
+ this.base.options.buttonLabels === 'fontawesome' ? '<i class="fa fa-check"></i>' : this.formSaveLabel,
2307
+ '</a>'
2308
+ );
2309
+
2310
+ template.push('<a href="#" class="medium-editor-toolbar-close">',
2311
+ this.base.options.buttonLabels === 'fontawesome' ? '<i class="fa fa-times"></i>' : this.formCloseLabel,
2312
+ '</a>');
2313
+
2314
+ // both of these options are slightly moot with the ability to
2315
+ // override the various form buildup/serialize functions.
2316
+
2317
+ if (this.base.options.anchorTarget) {
2318
+ // fixme: ideally, this options.anchorInputCheckboxLabel would be a formLabel too,
2319
+ // figure out how to deprecate? also consider `fa-` icon default implcations.
2320
+ template.push(
2321
+ '<input type="checkbox" class="medium-editor-toolbar-anchor-target">',
2322
+ '<label>',
2323
+ this.base.options.anchorInputCheckboxLabel,
2324
+ '</label>'
2325
+ );
2326
+ }
2327
+
2328
+ if (this.base.options.anchorButton) {
2329
+ // fixme: expose this `Button` text as a formLabel property, too
2330
+ // and provide similar access to a `fa-` icon default.
2331
+ template.push(
2332
+ '<input type="checkbox" class="medium-editor-toolbar-anchor-button">',
2333
+ '<label>Button</label>'
2334
+ );
2335
+ }
2336
+
2337
+ return template.join("");
2338
+
2339
+ },
2340
+
2341
+ // Used by medium-editor when the default toolbar is to be displayed
2342
+ isDisplayed: function () {
2343
+ return this.getForm().style.display === 'block';
2344
+ },
2345
+
2346
+ hideForm: function () {
2347
+ this.getForm().style.display = 'none';
2348
+ this.getInput().value = '';
2349
+ },
2350
+
2351
+ showForm: function (link_value) {
2352
+ var input = this.getInput();
2353
+
2354
+ this.base.saveSelection();
2355
+ this.base.hideToolbarDefaultActions();
2356
+ this.getForm().style.display = 'block';
2357
+ this.base.setToolbarPosition();
2358
+
2359
+ input.value = link_value || '';
2360
+ input.focus();
2361
+ },
2362
+
2363
+ // Called by core when tearing down medium-editor (deactivate)
2364
+ deactivate: function () {
2365
+ if (!this.form) {
2366
+ return false;
2367
+ }
2368
+
2369
+ if (this.form.parentNode) {
2370
+ this.form.parentNode.removeChild(this.form);
2371
+ }
2372
+
2373
+ delete this.form;
2374
+ },
2375
+
2376
+ // core methods
2377
+
2378
+ getFormOpts: function () {
2379
+ // no notion of private functions? wanted `_getFormOpts`
2380
+ var targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'),
2381
+ buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button'),
2382
+ opts = {
2383
+ url: this.getInput().value
2384
+ };
2385
+
2386
+ if (this.base.options.checkLinkFormat) {
2387
+ opts.url = this.checkLinkFormat(opts.url);
2388
+ }
2389
+
2390
+ if (targetCheckbox && targetCheckbox.checked) {
2391
+ opts.target = "_blank";
2392
+ } else {
2393
+ opts.target = "_self";
2394
+ }
2395
+
2396
+ if (buttonCheckbox && buttonCheckbox.checked) {
2397
+ opts.buttonClass = this.base.options.anchorButtonClass;
2398
+ }
2399
+
2400
+ return opts;
2401
+ },
2402
+
2403
+ doFormSave: function () {
2404
+ var opts = this.getFormOpts();
2405
+ this.completeFormSave(opts);
2406
+ },
2407
+
2408
+ completeFormSave: function (opts) {
2409
+ this.base.restoreSelection();
2410
+ this.base.createLink(opts);
2411
+ this.base.checkSelection();
2412
+ },
2413
+
2414
+ checkLinkFormat: function (value) {
2415
+ var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
2416
+ return (re.test(value) ? '' : 'http://') + value;
2417
+ },
2418
+
2419
+ doFormCancel: function () {
2420
+ this.base.restoreSelection();
2421
+ this.base.checkSelection();
2422
+ },
2423
+
2424
+ // form creation and event handling
2425
+
2426
+ attachFormEvents: function (form) {
2427
+ var close = form.querySelector(".medium-editor-toolbar-close"),
2428
+ save = form.querySelector(".medium-editor-toolbar-save"),
2429
+ input = form.querySelector(".medium-editor-toolbar-input");
2430
+
2431
+ // Handle clicks on the form itself
2432
+ this.base.on(form, 'click', this.handleFormClick.bind(this));
2433
+
2434
+ // Handle typing in the textbox
2435
+ this.base.on(input, 'keyup', this.handleTextboxKeyup.bind(this));
2436
+
2437
+ // Handle close button clicks
2438
+ this.base.on(close, 'click', this.handleCloseClick.bind(this));
2439
+
2440
+ // Handle save button clicks (capture)
2441
+ this.base.on(save, 'click', this.handleSaveClick.bind(this), true);
2442
+
2443
+ },
2444
+
2445
+ createForm: function () {
2446
+ var doc = this.base.options.ownerDocument,
2447
+ form = doc.createElement('div');
2448
+
2449
+ // Anchor Form (div)
2450
+ form.className = 'medium-editor-toolbar-form';
2451
+ form.id = 'medium-editor-toolbar-form-anchor-' + this.base.id;
2452
+ form.innerHTML = this.getTemplate();
2453
+ this.attachFormEvents(form);
2454
+
2455
+ return form;
2456
+ },
2457
+
2458
+ getInput: function () {
2459
+ return this.getForm().querySelector('input.medium-editor-toolbar-input');
2460
+ },
2461
+
2462
+ handleTextboxKeyup: function (event) {
2463
+ // For ENTER -> create the anchor
2464
+ if (event.keyCode === Util.keyCode.ENTER) {
2465
+ event.preventDefault();
2466
+ this.doFormSave();
2467
+ return;
2468
+ }
2469
+
2470
+ // For ESCAPE -> close the form
2471
+ if (event.keyCode === Util.keyCode.ESCAPE) {
2472
+ event.preventDefault();
2473
+ this.doFormCancel();
2474
+ }
2475
+ },
2476
+
2477
+ handleFormClick: function (event) {
2478
+ // make sure not to hide form when clicking inside the form
2479
+ event.stopPropagation();
2480
+ },
2481
+
2482
+ handleSaveClick: function (event) {
2483
+ // Clicking Save -> create the anchor
2484
+ event.preventDefault();
2485
+ this.doFormSave();
2486
+ },
2487
+
2488
+ handleCloseClick: function (event) {
2489
+ // Click Close -> close the form
2490
+ event.preventDefault();
2491
+ this.doFormCancel();
2492
+ }
2493
+ };
2494
+
2495
+ AnchorExtension = Util.derives(DefaultButton, AnchorDerived);
2496
+
2497
+ }());
2498
+
2499
+ var AnchorPreview;
2500
+
2501
+ (function () {
2502
+ 'use strict';
2503
+
2504
+ AnchorPreview = function () {
2505
+ this.parent = true;
2506
+ this.name = 'anchor-preview';
2507
+ };
2508
+
2509
+ AnchorPreview.prototype = {
2510
+
2511
+ // the default selector to locate where to
2512
+ // put the activeAnchor value in the preview
2513
+ previewValueSelector: 'a',
2514
+
2515
+ init: function (instance) {
2516
+
2517
+ this.base = instance;
2518
+ this.anchorPreview = this.createPreview();
2519
+ this.base.options.elementsContainer.appendChild(this.anchorPreview);
2520
+
2521
+ this.attachToEditables();
2522
+ },
2523
+
2524
+ getPreviewElement: function () {
2525
+ return this.anchorPreview;
2526
+ },
2527
+
2528
+ createPreview: function () {
2529
+ var el = this.base.options.ownerDocument.createElement('div');
2530
+
2531
+ el.id = 'medium-editor-anchor-preview-' + this.base.id;
2532
+ el.className = 'medium-editor-anchor-preview';
2533
+ el.innerHTML = this.getTemplate();
2534
+
2535
+ this.base.on(el, 'click', this.handleClick.bind(this));
2536
+
2537
+ return el;
2538
+ },
2539
+
2540
+ getTemplate: function () {
2541
+ return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
2542
+ ' <a class="medium-editor-toolbar-anchor-preview-inner"></a>' +
2543
+ '</div>';
2544
+ },
2545
+
2546
+ deactivate: function () {
2547
+ if (this.anchorPreview) {
2548
+ if (this.anchorPreview.parentNode) {
2549
+ this.anchorPreview.parentNode.removeChild(this.anchorPreview);
2550
+ }
2551
+ delete this.anchorPreview;
2552
+ }
2553
+ },
2554
+
2555
+ hidePreview: function () {
2556
+ this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
2557
+ this.activeAnchor = null;
2558
+ },
2559
+
2560
+ showPreview: function (anchorEl) {
2561
+ if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') ||
2562
+ anchorEl.getAttribute('data-disable-preview')) {
2563
+ return true;
2564
+ }
2565
+
2566
+ if (this.previewValueSelector) {
2567
+ this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value;
2568
+ this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value;
2569
+ }
2570
+
2571
+ this.anchorPreview.classList.add('medium-toolbar-arrow-over');
2572
+ this.anchorPreview.classList.remove('medium-toolbar-arrow-under');
2573
+
2574
+ if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
2575
+ this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
2576
+ }
2577
+
2578
+ this.activeAnchor = anchorEl;
2579
+
2580
+ this.positionPreview();
2581
+ this.attachPreviewHandlers();
2582
+
2583
+ return this;
2584
+ },
2585
+
2586
+ positionPreview: function () {
2587
+ var buttonHeight = 40,
2588
+ boundary = this.activeAnchor.getBoundingClientRect(),
2589
+ middleBoundary = (boundary.left + boundary.right) / 2,
2590
+ halfOffsetWidth,
2591
+ defaultLeft;
2592
+
2593
+ halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
2594
+ defaultLeft = this.base.options.diffLeft - halfOffsetWidth;
2595
+
2596
+ this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - this.base.options.diffTop + this.base.options.contentWindow.pageYOffset - this.anchorPreview.offsetHeight) + 'px';
2597
+ if (middleBoundary < halfOffsetWidth) {
2598
+ this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
2599
+ } else if ((this.base.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
2600
+ this.anchorPreview.style.left = this.base.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
2601
+ } else {
2602
+ this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
2603
+ }
2604
+ },
2605
+
2606
+ attachToEditables: function () {
2607
+ this.base.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
2608
+ },
2609
+
2610
+ handleClick: function (event) {
2611
+ var anchorExtension = this.base.getExtensionByName('anchor'),
2612
+ activeAnchor = this.activeAnchor;
2613
+
2614
+ if (anchorExtension && activeAnchor) {
2615
+ event.preventDefault();
2616
+
2617
+ this.base.selectElement(this.activeAnchor);
2618
+
2619
+ // Using setTimeout + options.delay because:
2620
+ // We may actually be displaying the anchor form, which should be controlled by options.delay
2621
+ this.base.delay(function () {
2622
+ if (activeAnchor) {
2623
+ anchorExtension.showForm(activeAnchor.attributes.href.value);
2624
+ activeAnchor = null;
2625
+ }
2626
+ }.bind(this));
2627
+ }
2628
+
2629
+ this.hidePreview();
2630
+ },
2631
+
2632
+ handleAnchorMouseout: function () {
2633
+ this.anchorToPreview = null;
2634
+ this.base.off(this.activeAnchor, 'mouseout', this.instance_handleAnchorMouseout);
2635
+ this.instance_handleAnchorMouseout = null;
2636
+ },
2637
+
2638
+
2639
+ handleEditableMouseover: function (event) {
2640
+ var target = Util.getClosestTag(event.target, 'a');
2641
+
2642
+ if (target) {
2643
+
2644
+ // Detect empty href attributes
2645
+ // The browser will make href="" or href="#top"
2646
+ // into absolute urls when accessed as event.targed.href, so check the html
2647
+ if (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML)) {
2648
+ return true;
2649
+ }
2650
+
2651
+ // only show when hovering on anchors
2652
+ if (this.base.toolbar && this.base.toolbar.isDisplayed()) {
2653
+ // only show when toolbar is not present
2654
+ return true;
2655
+ }
2656
+
2657
+ // detach handler for other anchor in case we hovered multiple anchors quickly
2658
+ if (this.activeAnchor && this.activeAnchor !== target) {
2659
+ this.detachPreviewHandlers();
2660
+ }
2661
+
2662
+ this.anchorToPreview = target;
2663
+
2664
+ this.instance_handleAnchorMouseout = this.handleAnchorMouseout.bind(this);
2665
+ this.base.on(this.anchorToPreview, 'mouseout', this.instance_handleAnchorMouseout);
2666
+ // Using setTimeout + options.delay because:
2667
+ // - We're going to show the anchor preview according to the configured delay
2668
+ // if the mouse has not left the anchor tag in that time
2669
+ this.base.delay(function () {
2670
+ if (this.anchorToPreview) {
2671
+ //this.activeAnchor = this.anchorToPreview;
2672
+ this.showPreview(this.anchorToPreview);
2673
+ }
2674
+ }.bind(this));
2675
+ }
2676
+ },
2677
+
2678
+ handlePreviewMouseover: function () {
2679
+ this.lastOver = (new Date()).getTime();
2680
+ this.hovering = true;
2681
+ },
2682
+
2683
+ handlePreviewMouseout: function (event) {
2684
+ if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {
2685
+ this.hovering = false;
2686
+ }
2687
+ },
2688
+
2689
+ updatePreview: function () {
2690
+ if (this.hovering) {
2691
+ return true;
2692
+ }
2693
+ var durr = (new Date()).getTime() - this.lastOver;
2694
+ if (durr > this.base.options.anchorPreviewHideDelay) {
2695
+ // hide the preview 1/2 second after mouse leaves the link
2696
+ this.detachPreviewHandlers();
2697
+ }
2698
+ },
2699
+
2700
+ detachPreviewHandlers: function () {
2701
+ // cleanup
2702
+ clearInterval(this.interval_timer);
2703
+ if (this.instance_handlePreviewMouseover) {
2704
+ this.base.off(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover);
2705
+ this.base.off(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout);
2706
+ if (this.activeAnchor) {
2707
+ this.base.off(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover);
2708
+ this.base.off(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout);
2709
+ }
2710
+ }
2711
+
2712
+ this.hidePreview();
2713
+
2714
+ this.hovering = this.instance_handlePreviewMouseover = this.instance_handlePreviewMouseout = null;
2715
+ },
2716
+
2717
+ // TODO: break up method and extract out handlers
2718
+ attachPreviewHandlers: function () {
2719
+ this.lastOver = (new Date()).getTime();
2720
+ this.hovering = true;
2721
+
2722
+ this.instance_handlePreviewMouseover = this.handlePreviewMouseover.bind(this);
2723
+ this.instance_handlePreviewMouseout = this.handlePreviewMouseout.bind(this);
2724
+
2725
+ this.interval_timer = setInterval(this.updatePreview.bind(this), 200);
2726
+
2727
+ this.base.on(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover);
2728
+ this.base.on(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout);
2729
+ this.base.on(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover);
2730
+ this.base.on(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout);
2731
+ }
2732
+ };
2733
+ }());
2734
+
2735
+ var FontSizeExtension;
2736
+
2737
+ (function () {
2738
+ 'use strict';
2739
+
2740
+ function FontSizeDerived() {
2741
+ this.parent = true;
2742
+ this.options = {
2743
+ name: 'fontsize',
2744
+ action: 'fontSize',
2745
+ aria: 'increase/decrease font size',
2746
+ contentDefault: '&#xB1;', // ±
2747
+ contentFA: '<i class="fa fa-text-height"></i>'
2748
+ };
2749
+ this.name = 'fontsize';
2750
+ this.hasForm = true;
2751
+ }
2752
+
2753
+ FontSizeDerived.prototype = {
2754
+
2755
+ // Button and Extension handling
2756
+
2757
+ // Called when the button the toolbar is clicked
2758
+ // Overrides DefaultButton.handleClick
2759
+ handleClick: function (evt) {
2760
+ evt.preventDefault();
2761
+ evt.stopPropagation();
2762
+
2763
+ if (!this.isDisplayed()) {
2764
+ // Get fontsize of current selection (convert to string since IE returns this as number)
2765
+ var fontSize = this.base.options.ownerDocument.queryCommandValue('fontSize') + '';
2766
+ this.showForm(fontSize);
2767
+ }
2768
+
2769
+ return false;
2770
+ },
2771
+
2772
+ // Called by medium-editor to append form to the toolbar
2773
+ getForm: function () {
2774
+ if (!this.form) {
2775
+ this.form = this.createForm();
2776
+ }
2777
+ return this.form;
2778
+ },
2779
+
2780
+ // Used by medium-editor when the default toolbar is to be displayed
2781
+ isDisplayed: function () {
2782
+ return this.getForm().style.display === 'block';
2783
+ },
2784
+
2785
+ hideForm: function () {
2786
+ this.getForm().style.display = 'none';
2787
+ this.getInput().value = '';
2788
+ },
2789
+
2790
+ showForm: function (fontSize) {
2791
+ var input = this.getInput();
2792
+
2793
+ this.base.saveSelection();
2794
+ this.base.hideToolbarDefaultActions();
2795
+ this.getForm().style.display = 'block';
2796
+ this.base.setToolbarPosition();
2797
+
2798
+ input.value = fontSize || '';
2799
+ input.focus();
2800
+ },
2801
+
2802
+ // Called by core when tearing down medium-editor (deactivate)
2803
+ deactivate: function () {
2804
+ if (!this.form) {
2805
+ return false;
2806
+ }
2807
+
2808
+ if (this.form.parentNode) {
2809
+ this.form.parentNode.removeChild(this.form);
2810
+ }
2811
+
2812
+ delete this.form;
2813
+ },
2814
+
2815
+ // core methods
2816
+
2817
+ doFormSave: function () {
2818
+ this.base.restoreSelection();
2819
+ this.base.checkSelection();
2820
+ },
2821
+
2822
+ doFormCancel: function () {
2823
+ this.base.restoreSelection();
2824
+ this.clearFontSize();
2825
+ this.base.checkSelection();
2826
+ },
2827
+
2828
+ // form creation and event handling
2829
+
2830
+ createForm: function () {
2831
+ var doc = this.base.options.ownerDocument,
2832
+ form = doc.createElement('div'),
2833
+ input = doc.createElement('input'),
2834
+ close = doc.createElement('a'),
2835
+ save = doc.createElement('a');
2836
+
2837
+ // Font Size Form (div)
2838
+ form.className = 'medium-editor-toolbar-form';
2839
+ form.id = 'medium-editor-toolbar-form-fontsize-' + this.base.id;
2840
+
2841
+ // Handle clicks on the form itself
2842
+ this.base.on(form, 'click', this.handleFormClick.bind(this));
2843
+
2844
+ // Add font size slider
2845
+ input.setAttribute('type', 'range');
2846
+ input.setAttribute('min', '1');
2847
+ input.setAttribute('max', '7');
2848
+ input.className = 'medium-editor-toolbar-input';
2849
+ form.appendChild(input);
2850
+
2851
+ // Handle typing in the textbox
2852
+ this.base.on(input, 'change', this.handleSliderChange.bind(this));
2853
+
2854
+ // Add save buton
2855
+ save.setAttribute('href', '#');
2856
+ save.className = 'medium-editor-toobar-save';
2857
+ save.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?
2858
+ '<i class="fa fa-check"></i>' :
2859
+ '&#10003;';
2860
+ form.appendChild(save);
2861
+
2862
+ // Handle save button clicks (capture)
2863
+ this.base.on(save, 'click', this.handleSaveClick.bind(this), true);
2864
+
2865
+ // Add close button
2866
+ close.setAttribute('href', '#');
2867
+ close.className = 'medium-editor-toobar-close';
2868
+ close.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?
2869
+ '<i class="fa fa-times"></i>' :
2870
+ '&times;';
2871
+ form.appendChild(close);
2872
+
2873
+ // Handle close button clicks
2874
+ this.base.on(close, 'click', this.handleCloseClick.bind(this));
2875
+
2876
+ return form;
2877
+ },
2878
+
2879
+ getInput: function () {
2880
+ return this.getForm().querySelector('input.medium-editor-toolbar-input');
2881
+ },
2882
+
2883
+ clearFontSize: function () {
2884
+ Selection.getSelectedElements(this.base.options.ownerDocument).forEach(function (el) {
2885
+ if (el.tagName === 'FONT' && el.hasAttribute('size')) {
2886
+ el.removeAttribute('size');
2887
+ }
2888
+ });
2889
+ },
2890
+
2891
+ handleSliderChange: function () {
2892
+ var size = this.getInput().value;
2893
+ if (size === '4') {
2894
+ this.clearFontSize();
2895
+ } else {
2896
+ this.base.execAction('fontSize', { size: size });
2897
+ }
2898
+ },
2899
+
2900
+ handleFormClick: function (event) {
2901
+ // make sure not to hide form when clicking inside the form
2902
+ event.stopPropagation();
2903
+ },
2904
+
2905
+ handleSaveClick: function (event) {
2906
+ // Clicking Save -> create the font size
2907
+ event.preventDefault();
2908
+ this.doFormSave();
2909
+ },
2910
+
2911
+ handleCloseClick: function (event) {
2912
+ // Click Close -> close the form
2913
+ event.preventDefault();
2914
+ this.doFormCancel();
2915
+ }
2916
+ };
2917
+
2918
+ FontSizeExtension = Util.derives(DefaultButton, FontSizeDerived);
2919
+ }());
2920
+
2921
+ var Toolbar;
2922
+
2923
+ (function () {
2924
+ 'use strict';
2925
+
2926
+ Toolbar = function Toolbar(instance) {
2927
+ this.base = instance;
2928
+ this.options = instance.options;
2929
+ this.initThrottledMethods();
2930
+ };
2931
+
2932
+ Toolbar.prototype = {
2933
+
2934
+ // Toolbar creation/deletion
2935
+
2936
+ createToolbar: function () {
2937
+ var toolbar = this.base.options.ownerDocument.createElement('div');
2938
+
2939
+ toolbar.id = 'medium-editor-toolbar-' + this.base.id;
2940
+ toolbar.className = 'medium-editor-toolbar';
2941
+
2942
+ if (this.options.staticToolbar) {
2943
+ toolbar.className += " static-toolbar";
2944
+ } else {
2945
+ toolbar.className += " stalker-toolbar";
2946
+ }
2947
+
2948
+
2949
+ toolbar.appendChild(this.createToolbarButtons());
2950
+
2951
+ // Add any forms that extensions may have
2952
+ this.base.commands.forEach(function (extension) {
2953
+ if (extension.hasForm) {
2954
+ toolbar.appendChild(extension.getForm());
2955
+ }
2956
+ });
2957
+
2958
+ this.attachEventHandlers();
2959
+
2960
+ return toolbar;
2961
+ },
2962
+
2963
+ createToolbarButtons: function () {
2964
+ var ul = this.base.options.ownerDocument.createElement('ul'),
2965
+ li,
2966
+ btn,
2967
+ buttons,
2968
+ extension;
2969
+
2970
+ ul.id = 'medium-editor-toolbar-actions' + this.base.id;
2971
+ ul.className = 'medium-editor-toolbar-actions clearfix';
2972
+ ul.style.display = 'block';
2973
+
2974
+ this.base.options.buttons.forEach(function (button) {
2975
+ extension = this.base.getExtensionByName(button);
2976
+ if (typeof extension.getButton === 'function') {
2977
+ btn = extension.getButton(this.base);
2978
+ li = this.base.options.ownerDocument.createElement('li');
2979
+ if (Util.isElement(btn)) {
2980
+ li.appendChild(btn);
2981
+ } else {
2982
+ li.innerHTML = btn;
2983
+ }
2984
+ ul.appendChild(li);
2985
+ }
2986
+ }.bind(this));
2987
+
2988
+ buttons = ul.querySelectorAll('button');
2989
+ if (buttons.length > 0) {
2990
+ buttons[0].classList.add(this.options.firstButtonClass);
2991
+ buttons[buttons.length - 1].classList.add(this.options.lastButtonClass);
2992
+ }
2993
+
2994
+ return ul;
2995
+ },
2996
+
2997
+ deactivate: function () {
2998
+ if (this.toolbar) {
2999
+ if (this.toolbar.parentNode) {
3000
+ this.toolbar.parentNode.removeChild(this.toolbar);
3001
+ }
3002
+ delete this.toolbar;
3003
+ }
3004
+ },
3005
+
3006
+ // Toolbar accessors
3007
+
3008
+ getToolbarElement: function () {
3009
+ if (!this.toolbar) {
3010
+ this.toolbar = this.createToolbar();
3011
+ }
3012
+
3013
+ return this.toolbar;
3014
+ },
3015
+
3016
+ getToolbarActionsElement: function () {
3017
+ return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');
3018
+ },
3019
+
3020
+ // Toolbar event handlers
3021
+
3022
+ initThrottledMethods: function () {
3023
+ // throttledPositionToolbar is throttled because:
3024
+ // - It will be called when the browser is resizing, which can fire many times very quickly
3025
+ // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
3026
+ this.throttledPositionToolbar = Util.throttle(function () {
3027
+ if (this.base.isActive) {
3028
+ this.positionToolbarIfShown();
3029
+ }
3030
+ }.bind(this));
3031
+ },
3032
+
3033
+ attachEventHandlers: function () {
3034
+
3035
+ // MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements
3036
+ this.base.subscribe('blur', this.handleBlur.bind(this));
3037
+ this.base.subscribe('focus', this.handleFocus.bind(this));
3038
+
3039
+ // Updating the state of the toolbar as things change
3040
+ this.base.subscribe('editableClick', this.handleEditableClick.bind(this));
3041
+ this.base.subscribe('editableKeyup', this.handleEditableKeyup.bind(this));
3042
+
3043
+ // Handle mouseup on document for updating the selection in the toolbar
3044
+ this.base.on(this.options.ownerDocument.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));
3045
+
3046
+ // Add a scroll event for sticky toolbar
3047
+ if (this.options.staticToolbar && this.options.stickyToolbar) {
3048
+ // On scroll (capture), re-position the toolbar
3049
+ this.base.on(this.options.contentWindow, 'scroll', this.handleWindowScroll.bind(this), true);
3050
+ }
3051
+
3052
+ // On resize, re-position the toolbar
3053
+ this.base.on(this.options.contentWindow, 'resize', this.handleWindowResize.bind(this));
3054
+ },
3055
+
3056
+ handleWindowScroll: function () {
3057
+ this.positionToolbarIfShown();
3058
+ },
3059
+
3060
+ handleWindowResize: function () {
3061
+ this.throttledPositionToolbar();
3062
+ },
3063
+
3064
+ handleDocumentMouseup: function (event) {
3065
+ // Do not trigger checkState when mouseup fires over the toolbar
3066
+ if (event &&
3067
+ event.target &&
3068
+ Util.isDescendant(this.getToolbarElement(), event.target)) {
3069
+ return false;
3070
+ }
3071
+ this.checkState();
3072
+ },
3073
+
3074
+ handleEditableClick: function () {
3075
+ // Delay the call to checkState to handle bug where selection is empty
3076
+ // immediately after clicking inside a pre-existing selection
3077
+ setTimeout(function () {
3078
+ this.checkState();
3079
+ }.bind(this), 0);
3080
+ },
3081
+
3082
+ handleEditableKeyup: function () {
3083
+ this.checkState();
3084
+ },
3085
+
3086
+ handleBlur: function () {
3087
+ // Kill any previously delayed calls to hide the toolbar
3088
+ clearTimeout(this.hideTimeout);
3089
+
3090
+ // Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar
3091
+ // calls from happening in this specific case
3092
+ clearTimeout(this.delayShowTimeout);
3093
+
3094
+ // Delay the call to hideToolbar to handle bug with multiple editors on the page at once
3095
+ this.hideTimeout = setTimeout(function () {
3096
+ this.hideToolbar();
3097
+ }.bind(this), 1);
3098
+ },
3099
+
3100
+ handleFocus: function () {
3101
+ this.checkState();
3102
+ },
3103
+
3104
+ // Hiding/showing toolbar
3105
+
3106
+ isDisplayed: function () {
3107
+ return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');
3108
+ },
3109
+
3110
+ showToolbar: function () {
3111
+ clearTimeout(this.hideTimeout);
3112
+ if (!this.isDisplayed()) {
3113
+ this.getToolbarElement().classList.add('medium-editor-toolbar-active');
3114
+ if (typeof this.options.onShowToolbar === 'function') {
3115
+ this.options.onShowToolbar();
3116
+ }
3117
+ }
3118
+ },
3119
+
3120
+ hideToolbar: function () {
3121
+ if (this.isDisplayed()) {
3122
+ this.base.commands.forEach(function (extension) {
3123
+ if (typeof extension.onHide === 'function') {
3124
+ extension.onHide();
3125
+ }
3126
+ });
3127
+
3128
+ this.getToolbarElement().classList.remove('medium-editor-toolbar-active');
3129
+ if (typeof this.options.onHideToolbar === 'function') {
3130
+ this.options.onHideToolbar();
3131
+ }
3132
+ }
3133
+ },
3134
+
3135
+ isToolbarDefaultActionsDisplayed: function () {
3136
+ return this.getToolbarActionsElement().style.display === 'block';
3137
+ },
3138
+
3139
+ hideToolbarDefaultActions: function () {
3140
+ if (this.isToolbarDefaultActionsDisplayed()) {
3141
+ this.getToolbarActionsElement().style.display = 'none';
3142
+ }
3143
+ },
3144
+
3145
+ showToolbarDefaultActions: function () {
3146
+ this.hideExtensionForms();
3147
+
3148
+ if (!this.isToolbarDefaultActionsDisplayed()) {
3149
+ this.getToolbarActionsElement().style.display = 'block';
3150
+ }
3151
+
3152
+ // Using setTimeout + options.delay because:
3153
+ // We will actually be displaying the toolbar, which should be controlled by options.delay
3154
+ this.delayShowTimeout = this.base.delay(function () {
3155
+ this.showToolbar();
3156
+ }.bind(this));
3157
+ },
3158
+
3159
+ hideExtensionForms: function () {
3160
+ // Hide all extension forms
3161
+ this.base.commands.forEach(function (extension) {
3162
+ if (extension.hasForm && extension.isDisplayed()) {
3163
+ extension.hideForm();
3164
+ }
3165
+ });
3166
+ },
3167
+
3168
+ // Responding to changes in user selection
3169
+
3170
+ // Checks for existance of multiple block elements in the current selection
3171
+ multipleBlockElementsSelected: function () {
3172
+ /*jslint regexp: true*/
3173
+ var selectionHtml = Selection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
3174
+ hasMultiParagraphs = selectionHtml.match(/<(p|h[1-6]|blockquote)[^>]*>/g);
3175
+ /*jslint regexp: false*/
3176
+
3177
+ return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
3178
+ },
3179
+
3180
+ modifySelection: function () {
3181
+ var selection = this.options.contentWindow.getSelection(),
3182
+ selectionRange = selection.getRangeAt(0);
3183
+
3184
+ /*
3185
+ * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
3186
+ * will be at the very end of an element. In other browsers, the selectionRange start
3187
+ * would instead be at the very beginning of an element that actually has content.
3188
+ * example:
3189
+ * <span>foo</span><span>bar</span>
3190
+ *
3191
+ * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
3192
+ * of the 'bar' span. However, there are cases where firefox will have the selectionRange start
3193
+ * at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any
3194
+ * properties on the 'bar' span, they won't be reflected accurately in the toolbar
3195
+ * (ie 'Bold' button wouldn't be active)
3196
+ *
3197
+ * So, for cases where the selectionRange start is at the end of an element/node, find the next
3198
+ * adjacent text node that actually has content in it, and move the selectionRange start there.
3199
+ */
3200
+ if (this.options.standardizeSelectionStart &&
3201
+ selectionRange.startContainer.nodeValue &&
3202
+ (selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) {
3203
+ var adjacentNode = Util.findAdjacentTextNodeWithContent(Selection.getSelectionElement(this.options.contentWindow), selectionRange.startContainer, this.options.ownerDocument);
3204
+ if (adjacentNode) {
3205
+ var offset = 0;
3206
+ while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
3207
+ offset = offset + 1;
3208
+ }
3209
+ var newRange = this.options.ownerDocument.createRange();
3210
+ newRange.setStart(adjacentNode, offset);
3211
+ newRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
3212
+ selection.removeAllRanges();
3213
+ selection.addRange(newRange);
3214
+ selectionRange = newRange;
3215
+ }
3216
+ }
3217
+ },
3218
+
3219
+ checkState: function () {
3220
+
3221
+ if (!this.base.preventSelectionUpdates) {
3222
+
3223
+ // If no editable has focus OR selection is inside contenteditable = false
3224
+ // hide toolbar
3225
+ if (!this.getFocusedElement() ||
3226
+ Selection.selectionInContentEditableFalse(this.options.contentWindow)) {
3227
+ this.hideToolbar();
3228
+ return;
3229
+ }
3230
+
3231
+ // If there's no selection element, selection element doesn't belong to this editor
3232
+ // or toolbar is disabled for this selection element
3233
+ // hide toolbar
3234
+ var selectionElement = Selection.getSelectionElement(this.options.contentWindow);
3235
+ if (!selectionElement ||
3236
+ this.base.elements.indexOf(selectionElement) === -1 ||
3237
+ selectionElement.getAttribute('data-disable-toolbar')) {
3238
+ this.hideToolbar();
3239
+ return;
3240
+ }
3241
+
3242
+ // Now we know there's a focused editable with a selection
3243
+
3244
+ // If the updateOnEmptySelection option is true, show the toolbar
3245
+ if (this.options.updateOnEmptySelection && this.options.staticToolbar) {
3246
+ this.showAndUpdateToolbar();
3247
+ return;
3248
+ }
3249
+
3250
+ // If we don't have a "valid" selection -> hide toolbar
3251
+ if (this.options.contentWindow.getSelection().toString().trim() === '' ||
3252
+ (this.options.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {
3253
+ this.hideToolbar();
3254
+ } else {
3255
+ this.showAndUpdateToolbar();
3256
+ }
3257
+ }
3258
+ },
3259
+
3260
+ getFocusedElement: function () {
3261
+ for (var i = 0; i < this.base.elements.length; i += 1) {
3262
+ if (this.base.elements[i].getAttribute('data-medium-focused')) {
3263
+ return this.base.elements[i];
3264
+ }
3265
+ }
3266
+ return null;
3267
+ },
3268
+
3269
+ // Updating the toolbar
3270
+
3271
+ showAndUpdateToolbar: function () {
3272
+ this.modifySelection();
3273
+ this.setToolbarButtonStates();
3274
+ this.showToolbarDefaultActions();
3275
+ this.setToolbarPosition();
3276
+ },
3277
+
3278
+ setToolbarButtonStates: function () {
3279
+ this.base.commands.forEach(function (extension) {
3280
+ if (typeof extension.isActive === 'function' &&
3281
+ typeof extension.setInactive === 'function') {
3282
+ extension.setInactive();
3283
+ }
3284
+ }.bind(this));
3285
+ this.checkActiveButtons();
3286
+ },
3287
+
3288
+ checkActiveButtons: function () {
3289
+ var manualStateChecks = [],
3290
+ queryState = null,
3291
+ selectionRange = Util.getSelectionRange(this.options.ownerDocument),
3292
+ parentNode,
3293
+ updateExtensionState = function (extension) {
3294
+ if (typeof extension.checkState === 'function') {
3295
+ extension.checkState(parentNode);
3296
+ } else if (typeof extension.isActive === 'function' &&
3297
+ typeof extension.isAlreadyApplied === 'function' &&
3298
+ typeof extension.setActive === 'function') {
3299
+ if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
3300
+ extension.setActive();
3301
+ }
3302
+ }
3303
+ };
3304
+
3305
+ if (!selectionRange) {
3306
+ return;
3307
+ }
3308
+
3309
+ parentNode = Selection.getSelectedParentElement(selectionRange);
3310
+
3311
+ // Loop through all commands
3312
+ this.base.commands.forEach(function (command) {
3313
+ // For those commands where we can use document.queryCommandState(), do so
3314
+ if (typeof command.queryCommandState === 'function') {
3315
+ queryState = command.queryCommandState();
3316
+ // If queryCommandState returns a valid value, we can trust the browser
3317
+ // and don't need to do our manual checks
3318
+ if (queryState !== null) {
3319
+ if (queryState && typeof command.setActive === 'function') {
3320
+ command.setActive();
3321
+ }
3322
+ return;
3323
+ }
3324
+ }
3325
+ // We can't use queryCommandState for this command, so add to manualStateChecks
3326
+ manualStateChecks.push(command);
3327
+ });
3328
+
3329
+ // Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node
3330
+ while (parentNode.tagName !== undefined && Util.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
3331
+ manualStateChecks.forEach(updateExtensionState);
3332
+
3333
+ // we can abort the search upwards if we leave the contentEditable element
3334
+ if (this.base.elements.indexOf(parentNode) !== -1) {
3335
+ break;
3336
+ }
3337
+ parentNode = parentNode.parentNode;
3338
+ }
3339
+ },
3340
+
3341
+ // Positioning toolbar
3342
+
3343
+ positionToolbarIfShown: function () {
3344
+ if (this.isDisplayed()) {
3345
+ this.setToolbarPosition();
3346
+ }
3347
+ },
3348
+
3349
+ setToolbarPosition: function () {
3350
+ var container = this.getFocusedElement(),
3351
+ selection = this.options.contentWindow.getSelection(),
3352
+ anchorPreview;
3353
+
3354
+ // If there isn't a valid selection, bail
3355
+ if (!container) {
3356
+ return this;
3357
+ }
3358
+
3359
+ if (this.options.staticToolbar) {
3360
+ this.showToolbar();
3361
+ this.positionStaticToolbar(container);
3362
+
3363
+ } else if (!selection.isCollapsed) {
3364
+ this.showToolbar();
3365
+ this.positionToolbar(selection);
3366
+ }
3367
+
3368
+ anchorPreview = this.base.getExtensionByName('anchor-preview');
3369
+
3370
+ if (anchorPreview && typeof anchorPreview.hidePreview === 'function') {
3371
+ anchorPreview.hidePreview();
3372
+ }
3373
+ },
3374
+
3375
+ positionStaticToolbar: function (container) {
3376
+ // position the toolbar at left 0, so we can get the real width of the toolbar
3377
+ this.getToolbarElement().style.left = '0';
3378
+
3379
+ // document.documentElement for IE 9
3380
+ var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
3381
+ windowWidth = this.options.contentWindow.innerWidth,
3382
+ toolbarElement = this.getToolbarElement(),
3383
+ containerRect = container.getBoundingClientRect(),
3384
+ containerTop = containerRect.top + scrollTop,
3385
+ containerCenter = (containerRect.left + (containerRect.width / 2)),
3386
+ toolbarHeight = toolbarElement.offsetHeight,
3387
+ toolbarWidth = toolbarElement.offsetWidth,
3388
+ halfOffsetWidth = toolbarWidth / 2,
3389
+ targetLeft;
3390
+
3391
+ if (this.options.stickyToolbar) {
3392
+ // If it's beyond the height of the editor, position it at the bottom of the editor
3393
+ if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) {
3394
+ toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
3395
+ toolbarElement.classList.remove('sticky-toolbar');
3396
+
3397
+ // Stick the toolbar to the top of the window
3398
+ } else if (scrollTop > (containerTop - toolbarHeight)) {
3399
+ toolbarElement.classList.add('sticky-toolbar');
3400
+ toolbarElement.style.top = "0px";
3401
+
3402
+ // Normal static toolbar position
3403
+ } else {
3404
+ toolbarElement.classList.remove('sticky-toolbar');
3405
+ toolbarElement.style.top = containerTop - toolbarHeight + "px";
3406
+ }
3407
+ } else {
3408
+ toolbarElement.style.top = containerTop - toolbarHeight + "px";
3409
+ }
3410
+
3411
+ if (this.options.toolbarAlign === 'left') {
3412
+ targetLeft = containerRect.left;
3413
+ } else if (this.options.toolbarAlign === 'center') {
3414
+ targetLeft = containerCenter - halfOffsetWidth;
3415
+ } else if (this.options.toolbarAlign === 'right') {
3416
+ targetLeft = containerRect.right - toolbarWidth;
3417
+ }
3418
+
3419
+ if (targetLeft < 0) {
3420
+ targetLeft = 0;
3421
+ } else if ((targetLeft + toolbarWidth) > windowWidth) {
3422
+ targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1);
3423
+ }
3424
+
3425
+ toolbarElement.style.left = targetLeft + 'px';
3426
+ },
3427
+
3428
+ positionToolbar: function (selection) {
3429
+ // position the toolbar at left 0, so we can get the real width of the toolbar
3430
+ this.getToolbarElement().style.left = '0';
3431
+
3432
+ var windowWidth = this.options.contentWindow.innerWidth,
3433
+ range = selection.getRangeAt(0),
3434
+ boundary = range.getBoundingClientRect(),
3435
+ middleBoundary = (boundary.left + boundary.right) / 2,
3436
+ toolbarElement = this.getToolbarElement(),
3437
+ toolbarHeight = toolbarElement.offsetHeight,
3438
+ toolbarWidth = toolbarElement.offsetWidth,
3439
+ halfOffsetWidth = toolbarWidth / 2,
3440
+ buttonHeight = 50,
3441
+ defaultLeft = this.options.diffLeft - halfOffsetWidth;
3442
+
3443
+ if (boundary.top < buttonHeight) {
3444
+ toolbarElement.classList.add('medium-toolbar-arrow-over');
3445
+ toolbarElement.classList.remove('medium-toolbar-arrow-under');
3446
+ toolbarElement.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
3447
+ } else {
3448
+ toolbarElement.classList.add('medium-toolbar-arrow-under');
3449
+ toolbarElement.classList.remove('medium-toolbar-arrow-over');
3450
+ toolbarElement.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
3451
+ }
3452
+ if (middleBoundary < halfOffsetWidth) {
3453
+ toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px';
3454
+ } else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
3455
+ toolbarElement.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px';
3456
+ } else {
3457
+ toolbarElement.style.left = defaultLeft + middleBoundary + 'px';
3458
+ }
3459
+ }
3460
+ };
3461
+ }());
3462
+
3463
+ var Placeholders;
3464
+
3465
+ (function () {
3466
+ 'use strict';
3467
+
3468
+ Placeholders = function (instance) {
3469
+ this.base = instance;
3470
+
3471
+ this.initPlaceholders();
3472
+ this.attachEventHandlers();
3473
+ };
3474
+
3475
+ Placeholders.prototype = {
3476
+
3477
+ initPlaceholders: function () {
3478
+ this.base.elements.forEach(function (el) {
3479
+ this.updatePlaceholder(el);
3480
+ }, this);
3481
+ },
3482
+
3483
+ showPlaceholder: function (el) {
3484
+ if (el) {
3485
+ el.classList.add('medium-editor-placeholder');
3486
+ }
3487
+ },
3488
+
3489
+ hidePlaceholder: function (el) {
3490
+ if (el) {
3491
+ el.classList.remove('medium-editor-placeholder');
3492
+ }
3493
+ },
3494
+
3495
+ updatePlaceholder: function (el) {
3496
+ if (!(el.querySelector('img')) &&
3497
+ !(el.querySelector('blockquote')) &&
3498
+ el.textContent.replace(/^\s+|\s+$/g, '') === '') {
3499
+ this.showPlaceholder(el);
3500
+ } else {
3501
+ this.hidePlaceholder(el);
3502
+ }
3503
+ },
3504
+
3505
+ attachEventHandlers: function () {
3506
+ // Custom events
3507
+ this.base.subscribe('blur', this.handleExternalInteraction.bind(this));
3508
+
3509
+ // Check placeholder on blur
3510
+ this.base.subscribe('editableBlur', this.handleBlur.bind(this));
3511
+
3512
+ // Events where we always hide the placeholder
3513
+ this.base.subscribe('editableClick', this.handleHidePlaceholderEvent.bind(this));
3514
+ this.base.subscribe('editableKeypress', this.handleHidePlaceholderEvent.bind(this));
3515
+ this.base.subscribe('editablePaste', this.handleHidePlaceholderEvent.bind(this));
3516
+ },
3517
+
3518
+ handleHidePlaceholderEvent: function (event, element) {
3519
+ // Events where we hide the placeholder
3520
+ this.hidePlaceholder(element);
3521
+ },
3522
+
3523
+ handleBlur: function (event, element) {
3524
+ // Update placeholder for element that lost focus
3525
+ this.updatePlaceholder(element);
3526
+ },
3527
+
3528
+ handleExternalInteraction: function () {
3529
+ // Update all placeholders
3530
+ this.initPlaceholders();
3531
+ }
3532
+ };
3533
+
3534
+ }());
3535
+
3536
+ var extensionDefaults;
3537
+ (function(){
3538
+
3539
+ // for now this is empty because nothing interally uses an Extension default.
3540
+ // as they are converted, provide them here.
3541
+ extensionDefaults = {
3542
+ paste: PasteHandler
3543
+ };
3544
+
3545
+ })();
3546
+ function MediumEditor(elements, options) {
3547
+ 'use strict';
3548
+ return this.init(elements, options);
3549
+ }
3550
+
3551
+ (function () {
3552
+ 'use strict';
3553
+
3554
+ // Event handlers that shouldn't be exposed externally
3555
+
3556
+ function handleDisabledEnterKeydown(event, element) {
3557
+ if (this.options.disableReturn || element.getAttribute('data-disable-return')) {
3558
+ event.preventDefault();
3559
+ } else if (this.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
3560
+ var node = Util.getSelectionStart(this.options.ownerDocument);
3561
+ if (node && node.textContent.trim() === '') {
3562
+ event.preventDefault();
3563
+ }
3564
+ }
3565
+ }
3566
+
3567
+ function handleTabKeydown(event) {
3568
+ // Override tab only for pre nodes
3569
+ var node = Util.getSelectionStart(this.options.ownerDocument),
3570
+ tag = node && node.tagName.toLowerCase();
3571
+
3572
+ if (tag === 'pre') {
3573
+ event.preventDefault();
3574
+ Util.insertHTMLCommand(this.options.ownerDocument, ' ');
3575
+ }
3576
+
3577
+ // Tab to indent list structures!
3578
+ if (Util.isListItem(node)) {
3579
+ event.preventDefault();
3580
+
3581
+ // If Shift is down, outdent, otherwise indent
3582
+ if (event.shiftKey) {
3583
+ this.options.ownerDocument.execCommand('outdent', false, null);
3584
+ } else {
3585
+ this.options.ownerDocument.execCommand('indent', false, null);
3586
+ }
3587
+ }
3588
+ }
3589
+
3590
+ function handleBlockDeleteKeydowns(event) {
3591
+ var range, sel, p, node = Util.getSelectionStart(this.options.ownerDocument),
3592
+ tagName = node.tagName.toLowerCase(),
3593
+ isEmpty = /^(\s+|<br\/?>)?$/i,
3594
+ isHeader = /h\d/i;
3595
+
3596
+ if ((event.which === Util.keyCode.BACKSPACE || event.which === Util.keyCode.ENTER) &&
3597
+ // has a preceeding sibling
3598
+ node.previousElementSibling &&
3599
+ // in a header
3600
+ isHeader.test(tagName) &&
3601
+ // at the very end of the block
3602
+ Selection.getCaretOffsets(node).left === 0) {
3603
+ if (event.which === Util.keyCode.BACKSPACE && isEmpty.test(node.previousElementSibling.innerHTML)) {
3604
+ // backspacing the begining of a header into an empty previous element will
3605
+ // change the tagName of the current node to prevent one
3606
+ // instead delete previous node and cancel the event.
3607
+ node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
3608
+ event.preventDefault();
3609
+ } else if (event.which === Util.keyCode.ENTER) {
3610
+ // hitting return in the begining of a header will create empty header elements before the current one
3611
+ // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
3612
+ p = this.options.ownerDocument.createElement('p');
3613
+ p.innerHTML = '<br>';
3614
+ node.previousElementSibling.parentNode.insertBefore(p, node);
3615
+ event.preventDefault();
3616
+ }
3617
+ } else if (event.which === Util.keyCode.DELETE &&
3618
+ // between two sibling elements
3619
+ node.nextElementSibling &&
3620
+ node.previousElementSibling &&
3621
+ // not in a header
3622
+ !isHeader.test(tagName) &&
3623
+ // in an empty tag
3624
+ isEmpty.test(node.innerHTML) &&
3625
+ // when the next tag *is* a header
3626
+ isHeader.test(node.nextElementSibling.tagName)) {
3627
+ // hitting delete in an empty element preceding a header, ex:
3628
+ // <p>[CURSOR]</p><h1>Header</h1>
3629
+ // Will cause the h1 to become a paragraph.
3630
+ // Instead, delete the paragraph node and move the cursor to the begining of the h1
3631
+
3632
+ // remove node and move cursor to start of header
3633
+ range = this.options.ownerDocument.createRange();
3634
+ sel = this.options.ownerDocument.getSelection();
3635
+
3636
+ range.setStart(node.nextElementSibling, 0);
3637
+ range.collapse(true);
3638
+
3639
+ sel.removeAllRanges();
3640
+ sel.addRange(range);
3641
+
3642
+ node.previousElementSibling.parentNode.removeChild(node);
3643
+
3644
+ event.preventDefault();
3645
+ } else if (event.which === Util.keyCode.BACKSPACE &&
3646
+ tagName === 'li' &&
3647
+ // hitting backspace inside an empty li
3648
+ isEmpty.test(node.innerHTML) &&
3649
+ // is first element (no preceeding siblings)
3650
+ !node.previousElementSibling &&
3651
+ // parent also does not have a sibling
3652
+ !node.parentElement.previousElementSibling &&
3653
+ // is not the only li in a list
3654
+ node.nextElementSibling.tagName.toLowerCase() === 'li') {
3655
+ // backspacing in an empty first list element in the first list (with more elements) ex:
3656
+ // <ul><li>[CURSOR]</li><li>List Item 2</li></ul>
3657
+ // will remove the first <li> but add some extra element before (varies based on browser)
3658
+ // Instead, this will:
3659
+ // 1) remove the list element
3660
+ // 2) create a paragraph before the list
3661
+ // 3) move the cursor into the paragraph
3662
+
3663
+ // create a paragraph before the list
3664
+ p = this.options.ownerDocument.createElement('p');
3665
+ p.innerHTML = '<br>';
3666
+ node.parentElement.parentElement.insertBefore(p, node.parentElement);
3667
+
3668
+ // move the cursor into the new paragraph
3669
+ range = this.options.ownerDocument.createRange();
3670
+ sel = this.options.ownerDocument.getSelection();
3671
+ range.setStart(p, 0);
3672
+ range.collapse(true);
3673
+ sel.removeAllRanges();
3674
+ sel.addRange(range);
3675
+
3676
+ // remove the list element
3677
+ node.parentElement.removeChild(node);
3678
+
3679
+ event.preventDefault();
3680
+ }
3681
+ }
3682
+
3683
+ function handleDrag(event) {
3684
+ var className = 'medium-editor-dragover';
3685
+ event.preventDefault();
3686
+ event.dataTransfer.dropEffect = 'copy';
3687
+
3688
+ if (event.type === 'dragover') {
3689
+ event.target.classList.add(className);
3690
+ } else if (event.type === 'dragleave') {
3691
+ event.target.classList.remove(className);
3692
+ }
3693
+ }
3694
+
3695
+ function handleDrop(event) {
3696
+ var className = 'medium-editor-dragover',
3697
+ files;
3698
+ event.preventDefault();
3699
+ event.stopPropagation();
3700
+
3701
+ // IE9 does not support the File API, so prevent file from opening in a new window
3702
+ // but also don't try to actually get the file
3703
+ if (event.dataTransfer.files) {
3704
+ files = Array.prototype.slice.call(event.dataTransfer.files, 0);
3705
+ files.some(function (file) {
3706
+ if (file.type.match("image")) {
3707
+ var fileReader, id;
3708
+ fileReader = new FileReader();
3709
+ fileReader.readAsDataURL(file);
3710
+
3711
+ id = 'medium-img-' + (+new Date());
3712
+ Util.insertHTMLCommand(this.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
3713
+
3714
+ fileReader.onload = function () {
3715
+ var img = this.options.ownerDocument.getElementById(id);
3716
+ if (img) {
3717
+ img.removeAttribute('id');
3718
+ img.removeAttribute('class');
3719
+ img.src = fileReader.result;
3720
+ }
3721
+ }.bind(this);
3722
+ }
3723
+ }.bind(this));
3724
+ }
3725
+ event.target.classList.remove(className);
3726
+ }
3727
+
3728
+ function handleKeyup(event) {
3729
+ var node = Util.getSelectionStart(this.options.ownerDocument),
3730
+ tagName;
3731
+
3732
+ if (!node) {
3733
+ return;
3734
+ }
3735
+
3736
+ if (node.getAttribute('data-medium-element') && node.children.length === 0) {
3737
+ this.options.ownerDocument.execCommand('formatBlock', false, 'p');
3738
+ }
3739
+
3740
+ if (event.which === Util.keyCode.ENTER && !Util.isListItem(node)) {
3741
+ tagName = node.tagName.toLowerCase();
3742
+ // For anchor tags, unlink
3743
+ if (tagName === 'a') {
3744
+ this.options.ownerDocument.execCommand('unlink', false, null);
3745
+ } else if (!event.shiftKey) {
3746
+ // only format block if this is not a header tag
3747
+ if (!/h\d/.test(tagName)) {
3748
+ this.options.ownerDocument.execCommand('formatBlock', false, 'p');
3749
+ }
3750
+ }
3751
+ }
3752
+ }
3753
+
3754
+ // Internal helper methods which shouldn't be exposed externally
3755
+
3756
+ function createElementsArray(selector) {
3757
+ if (!selector) {
3758
+ selector = [];
3759
+ }
3760
+ // If string, use as query selector
3761
+ if (typeof selector === 'string') {
3762
+ selector = this.options.ownerDocument.querySelectorAll(selector);
3763
+ }
3764
+ // If element, put into array
3765
+ if (Util.isElement(selector)) {
3766
+ selector = [selector];
3767
+ }
3768
+ // Convert NodeList (or other array like object) into an array
3769
+ this.elements = Array.prototype.slice.apply(selector);
3770
+ }
3771
+
3772
+ function initExtension(extension, name, instance) {
3773
+ if (extension.parent) {
3774
+ extension.base = instance;
3775
+ }
3776
+ if (typeof extension.init === 'function') {
3777
+ extension.init(instance);
3778
+ }
3779
+ if (!extension.name) {
3780
+ extension.name = name;
3781
+ }
3782
+ return extension;
3783
+ }
3784
+
3785
+ function shouldAddDefaultAnchorPreview() {
3786
+ var i,
3787
+ shouldAdd = false;
3788
+
3789
+ // If anchor-preview is disabled, don't add
3790
+ if (this.options.disableAnchorPreview) {
3791
+ return false;
3792
+ }
3793
+ // If anchor-preview extension has been overriden, don't add
3794
+ if (this.options.extensions['anchor-preview']) {
3795
+ return false;
3796
+ }
3797
+ // If toolbar is disabled, don't add
3798
+ if (this.options.disableToolbar) {
3799
+ return false;
3800
+ }
3801
+ // If all elements have 'data-disable-toolbar' attribute, don't add
3802
+ for (i = 0; i < this.elements.length; i += 1) {
3803
+ if (!this.elements[i].getAttribute('data-disable-toolbar')) {
3804
+ shouldAdd = true;
3805
+ break;
3806
+ }
3807
+ }
3808
+
3809
+ return shouldAdd;
3810
+ }
3811
+
3812
+ function createContentEditable(index) {
3813
+ var div = this.options.ownerDocument.createElement('div');
3814
+ var id = (+new Date());
3815
+ var textarea = this.elements[index];
3816
+
3817
+ div.className = textarea.className;
3818
+ div.id = id;
3819
+ div.innerHTML = textarea.value;
3820
+
3821
+ textarea.classList.add('medium-editor-hidden');
3822
+ textarea.parentNode.insertBefore(
3823
+ div,
3824
+ textarea
3825
+ );
3826
+
3827
+ this.on(div, 'input', function () {
3828
+ textarea.value = this.serialize()[id].value;
3829
+ }.bind(this));
3830
+ return div;
3831
+ }
3832
+
3833
+ function initElements() {
3834
+ var i,
3835
+ addToolbar = false;
3836
+ for (i = 0; i < this.elements.length; i += 1) {
3837
+ if (!this.options.disableEditing && !this.elements[i].getAttribute('data-disable-editing')) {
3838
+ if (this.elements[i].tagName.toLowerCase() === 'textarea') {
3839
+ this.elements[i] = createContentEditable.call(this, i);
3840
+ }
3841
+ this.elements[i].setAttribute('contentEditable', true);
3842
+ this.elements[i].setAttribute('spellcheck', this.options.spellcheck);
3843
+ }
3844
+ if (!this.elements[i].getAttribute('data-placeholder')) {
3845
+ this.elements[i].setAttribute('data-placeholder', this.options.placeholder);
3846
+ }
3847
+ this.elements[i].setAttribute('data-medium-element', true);
3848
+ this.elements[i].setAttribute('role', 'textbox');
3849
+ this.elements[i].setAttribute('aria-multiline', true);
3850
+ if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) {
3851
+ addToolbar = true;
3852
+ }
3853
+ }
3854
+ // Init toolbar
3855
+ if (!this.toolbar && addToolbar) {
3856
+ this.toolbar = new Toolbar(this);
3857
+ this.options.elementsContainer.appendChild(this.toolbar.getToolbarElement());
3858
+ }
3859
+ }
3860
+
3861
+ function attachHandlers() {
3862
+ var i;
3863
+
3864
+ // attach to tabs
3865
+ this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));
3866
+
3867
+ // Bind keys which can create or destroy a block element: backspace, delete, return
3868
+ this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this));
3869
+ this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this));
3870
+
3871
+ // disabling return or double return
3872
+ if (this.options.disableReturn || this.options.disableDoubleReturn) {
3873
+ this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
3874
+ } else {
3875
+ for (i = 0; i < this.elements.length; i += 1) {
3876
+ if (this.elements[i].getAttribute('data-disable-return') || this.elements[i].getAttribute('data-disable-double-return')) {
3877
+ this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
3878
+ break;
3879
+ }
3880
+ }
3881
+ }
3882
+
3883
+ // if we're not disabling return, add a handler to help handle cleanup
3884
+ // for certain cases when enter is pressed
3885
+ if (!this.options.disableReturn) {
3886
+ this.elements.forEach(function (element) {
3887
+ if (!element.getAttribute('data-disable-return')) {
3888
+ this.on(element, 'keyup', handleKeyup.bind(this));
3889
+ }
3890
+ }, this);
3891
+ }
3892
+
3893
+ // drag and drop of images
3894
+ if (this.options.imageDragging) {
3895
+ this.subscribe('editableDrag', handleDrag.bind(this));
3896
+ this.subscribe('editableDrop', handleDrop.bind(this));
3897
+ }
3898
+ }
3899
+
3900
+ function initPasteHandler(options) {
3901
+ // Backwards compatability
3902
+ var defaultsBC = {
3903
+ forcePlainText: this.options.forcePlainText, // deprecated
3904
+ cleanPastedHTML: this.options.cleanPastedHTML, // deprecated
3905
+ disableReturn: this.options.disableReturn,
3906
+ targetBlank: this.options.targetBlank,
3907
+ "window": this.options.contentWindow,
3908
+ "document": this.options.ownerDocument
3909
+ };
3910
+
3911
+ return new MediumEditor.extensions.paste(
3912
+ Util.extend({}, options, defaultsBC)
3913
+ );
3914
+ }
3915
+
3916
+ function initCommands() {
3917
+ var buttons = this.options.buttons,
3918
+ extensions = this.options.extensions,
3919
+ ext,
3920
+ name;
3921
+ this.commands = [];
3922
+
3923
+ buttons.forEach(function (buttonName) {
3924
+ if (extensions[buttonName]) {
3925
+ ext = initExtension(extensions[buttonName], buttonName, this);
3926
+ this.commands.push(ext);
3927
+ } else if (buttonName === 'anchor') {
3928
+ ext = initExtension(new AnchorExtension(), buttonName, this);
3929
+ this.commands.push(ext);
3930
+ } else if (buttonName === 'fontsize') {
3931
+ ext = initExtension(new FontSizeExtension(), buttonName, this);
3932
+ this.commands.push(ext);
3933
+ } else if (ButtonsData.hasOwnProperty(buttonName)) {
3934
+ ext = new DefaultButton(ButtonsData[buttonName], this);
3935
+ this.commands.push(ext);
3936
+ }
3937
+ }, this);
3938
+
3939
+ for (name in extensions) {
3940
+ if (extensions.hasOwnProperty(name) && buttons.indexOf(name) === -1) {
3941
+ ext = initExtension(extensions[name], name, this);
3942
+ this.commands.push(ext);
3943
+ }
3944
+ }
3945
+
3946
+ // Only add default paste extension if it wasn't overriden
3947
+ if (!this.options.extensions['paste']) {
3948
+ this.commands.push(initExtension(initPasteHandler.call(this, this.options.paste), 'paste', this));
3949
+ }
3950
+
3951
+ // Add AnchorPreview as extension if needed
3952
+ if (shouldAddDefaultAnchorPreview.call(this)) {
3953
+ this.commands.push(initExtension(new AnchorPreview(), 'anchor-preview', this));
3954
+ }
3955
+ }
3956
+
3957
+ function mergeOptions(defaults, options) {
3958
+ // warn about using deprecated properties
3959
+ if (options) {
3960
+ [['forcePlainText', 'paste.forcePlainText'],
3961
+ ['cleanPastedHTML', 'paste.cleanPastedHTML']].forEach(function (pair) {
3962
+ if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) {
3963
+ Util.deprecated(pair[0], pair[1], 'v5.0.0');
3964
+ }
3965
+ });
3966
+ }
3967
+
3968
+ var nestedMerges = ['paste'];
3969
+ var tempOpts = Util.extend({}, options);
3970
+
3971
+ nestedMerges.forEach(function (toMerge) {
3972
+ if (!tempOpts[toMerge]) {
3973
+ tempOpts[toMerge] = defaults[toMerge];
3974
+ } else {
3975
+ tempOpts[toMerge] = Util.defaults({}, tempOpts[toMerge], defaults[toMerge]);
3976
+ }
3977
+ });
3978
+
3979
+ return Util.defaults(tempOpts, defaults);
3980
+ }
3981
+
3982
+ function execActionInternal(action, opts) {
3983
+ /*jslint regexp: true*/
3984
+ var appendAction = /^append-(.+)$/gi,
3985
+ match;
3986
+ /*jslint regexp: false*/
3987
+
3988
+ // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
3989
+ // type of block element (ie append-blockquote, append-h1, append-pre, etc.)
3990
+ match = appendAction.exec(action);
3991
+ if (match) {
3992
+ return Util.execFormatBlock(this.options.ownerDocument, match[1]);
3993
+ }
3994
+
3995
+ if (action === 'fontSize') {
3996
+ return this.options.ownerDocument.execCommand('fontSize', false, opts.size);
3997
+ }
3998
+
3999
+ if (action === 'createLink') {
4000
+ return this.createLink(opts);
4001
+ }
4002
+
4003
+ if (action === 'image') {
4004
+ return this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
4005
+ }
4006
+
4007
+ return this.options.ownerDocument.execCommand(action, false, null);
4008
+ }
4009
+
4010
+ // deprecate
4011
+ MediumEditor.statics = {
4012
+ ButtonsData: ButtonsData,
4013
+ DefaultButton: DefaultButton,
4014
+ AnchorExtension: AnchorExtension,
4015
+ FontSizeExtension: FontSizeExtension,
4016
+ Toolbar: Toolbar,
4017
+ AnchorPreview: AnchorPreview
4018
+ };
4019
+
4020
+ MediumEditor.Extension = Extension;
4021
+
4022
+ MediumEditor.extensions = extensionDefaults;
4023
+ MediumEditor.util = Util;
4024
+ MediumEditor.selection = Selection;
4025
+
4026
+ MediumEditor.prototype = {
4027
+
4028
+ defaults: editorDefaults,
4029
+
4030
+ // NOT DOCUMENTED - exposed for backwards compatability
4031
+ init: function (elements, options) {
4032
+ var uniqueId = 1;
4033
+
4034
+ this.options = mergeOptions.call(this, this.defaults, options);
4035
+ createElementsArray.call(this, elements);
4036
+ if (this.elements.length === 0) {
4037
+ return;
4038
+ }
4039
+
4040
+ if (!this.options.elementsContainer) {
4041
+ this.options.elementsContainer = this.options.ownerDocument.body;
4042
+ }
4043
+
4044
+ while (this.options.elementsContainer.querySelector('#medium-editor-toolbar-' + uniqueId)) {
4045
+ uniqueId = uniqueId + 1;
4046
+ }
4047
+
4048
+ this.id = uniqueId;
4049
+
4050
+ return this.setup();
4051
+ },
4052
+
4053
+ setup: function () {
4054
+ if (this.isActive) {
4055
+ return;
4056
+ }
4057
+
4058
+ this.events = new Events(this);
4059
+ this.isActive = true;
4060
+
4061
+ // Call initialization helpers
4062
+ initCommands.call(this);
4063
+ initElements.call(this);
4064
+ attachHandlers.call(this);
4065
+
4066
+ if (!this.options.disablePlaceholders) {
4067
+ this.placeholders = new Placeholders(this);
4068
+ }
4069
+ },
4070
+
4071
+ destroy: function () {
4072
+ if (!this.isActive) {
4073
+ return;
4074
+ }
4075
+
4076
+ var i;
4077
+
4078
+ this.isActive = false;
4079
+
4080
+ if (this.toolbar !== undefined) {
4081
+ this.toolbar.deactivate();
4082
+ delete this.toolbar;
4083
+ }
4084
+
4085
+ for (i = 0; i < this.elements.length; i += 1) {
4086
+ this.elements[i].removeAttribute('contentEditable');
4087
+ this.elements[i].removeAttribute('spellcheck');
4088
+ this.elements[i].removeAttribute('data-medium-element');
4089
+ }
4090
+
4091
+ this.commands.forEach(function (extension) {
4092
+ if (typeof extension.deactivate === 'function') {
4093
+ extension.deactivate();
4094
+ }
4095
+ }, this);
4096
+
4097
+ this.events.detachAllDOMEvents();
4098
+ this.events.detachAllCustomEvents();
4099
+ },
4100
+
4101
+ on: function (target, event, listener, useCapture) {
4102
+ this.events.attachDOMEvent(target, event, listener, useCapture);
4103
+ },
4104
+
4105
+ off: function (target, event, listener, useCapture) {
4106
+ this.events.detachDOMEvent(target, event, listener, useCapture);
4107
+ },
4108
+
4109
+ subscribe: function (event, listener) {
4110
+ this.events.attachCustomEvent(event, listener);
4111
+ },
4112
+
4113
+ unsubscribe: function (event, listener) {
4114
+ this.events.detachCustomEvent(event, listener);
4115
+ },
4116
+
4117
+ delay: function (fn) {
4118
+ var self = this;
4119
+ return setTimeout(function () {
4120
+ if (self.isActive) {
4121
+ fn();
4122
+ }
4123
+ }, this.options.delay);
4124
+ },
4125
+
4126
+ serialize: function () {
4127
+ var i,
4128
+ elementid,
4129
+ content = {};
4130
+ for (i = 0; i < this.elements.length; i += 1) {
4131
+ elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
4132
+ content[elementid] = {
4133
+ value: this.elements[i].innerHTML.trim()
4134
+ };
4135
+ }
4136
+ return content;
4137
+ },
4138
+
4139
+ getExtensionByName: function (name) {
4140
+ var extension;
4141
+ if (this.commands && this.commands.length) {
4142
+ this.commands.some(function (ext) {
4143
+ if (ext.name === name) {
4144
+ extension = ext;
4145
+ return true;
4146
+ }
4147
+ return false;
4148
+ });
4149
+ }
4150
+ return extension;
4151
+ },
4152
+
4153
+ /**
4154
+ * NOT DOCUMENTED - exposed for backwards compatability
4155
+ * Helper function to call a method with a number of parameters on all registered extensions.
4156
+ * The function assures that the function exists before calling.
4157
+ *
4158
+ * @param {string} funcName name of the function to call
4159
+ * @param [args] arguments passed into funcName
4160
+ */
4161
+ callExtensions: function (funcName) {
4162
+ if (arguments.length < 1) {
4163
+ return;
4164
+ }
4165
+
4166
+ var args = Array.prototype.slice.call(arguments, 1),
4167
+ ext,
4168
+ name;
4169
+
4170
+ for (name in this.options.extensions) {
4171
+ if (this.options.extensions.hasOwnProperty(name)) {
4172
+ ext = this.options.extensions[name];
4173
+ if (ext[funcName] !== undefined) {
4174
+ ext[funcName].apply(ext, args);
4175
+ }
4176
+ }
4177
+ }
4178
+ return this;
4179
+ },
4180
+
4181
+ stopSelectionUpdates: function () {
4182
+ this.preventSelectionUpdates = true;
4183
+ },
4184
+
4185
+ startSelectionUpdates: function () {
4186
+ this.preventSelectionUpdates = false;
4187
+ },
4188
+
4189
+ // NOT DOCUMENTED - exposed as extension helper and for backwards compatability
4190
+ checkSelection: function () {
4191
+ if (this.toolbar) {
4192
+ this.toolbar.checkState();
4193
+ }
4194
+ return this;
4195
+ },
4196
+
4197
+ // Wrapper around document.queryCommandState for checking whether an action has already
4198
+ // been applied to the current selection
4199
+ queryCommandState: function (action) {
4200
+ var fullAction = /^full-(.+)$/gi,
4201
+ match,
4202
+ queryState = null;
4203
+
4204
+ // Actions starting with 'full-' need to be modified since this is a medium-editor concept
4205
+ match = fullAction.exec(action);
4206
+ if (match) {
4207
+ action = match[1];
4208
+ }
4209
+
4210
+ try {
4211
+ queryState = this.options.ownerDocument.queryCommandState(action);
4212
+ } catch (exc) {
4213
+ queryState = null;
4214
+ }
4215
+
4216
+ return queryState;
4217
+ },
4218
+
4219
+ execAction: function (action, opts) {
4220
+ /*jslint regexp: true*/
4221
+ var fullAction = /^full-(.+)$/gi,
4222
+ match,
4223
+ result;
4224
+ /*jslint regexp: false*/
4225
+
4226
+ // Actions starting with 'full-' should be applied to to the entire contents of the editable element
4227
+ // (ie full-bold, full-append-pre, etc.)
4228
+ match = fullAction.exec(action);
4229
+ if (match) {
4230
+ // Store the current selection to be restored after applying the action
4231
+ this.saveSelection();
4232
+ // Select all of the contents before calling the action
4233
+ this.selectAllContents();
4234
+ result = execActionInternal.call(this, match[1], opts);
4235
+ // Restore the previous selection
4236
+ this.restoreSelection();
4237
+ } else {
4238
+ result = execActionInternal.call(this, action, opts);
4239
+ }
4240
+
4241
+ // do some DOM clean-up for known browser issues after the action
4242
+ if (action === 'insertunorderedlist' || action === 'insertorderedlist') {
4243
+ Util.cleanListDOM(this.getSelectedParentElement());
4244
+ }
4245
+
4246
+ this.checkSelection();
4247
+ return result;
4248
+ },
4249
+
4250
+ getSelectedParentElement: function (range) {
4251
+ if (range === undefined) {
4252
+ range = this.options.contentWindow.getSelection().getRangeAt(0);
4253
+ }
4254
+ return Selection.getSelectedParentElement(range);
4255
+ },
4256
+
4257
+ // NOT DOCUMENTED - exposed as extension helper
4258
+ hideToolbarDefaultActions: function () {
4259
+ if (this.toolbar) {
4260
+ this.toolbar.hideToolbarDefaultActions();
4261
+ }
4262
+ return this;
4263
+ },
4264
+
4265
+ // NOT DOCUMENTED - exposed as extension helper and for backwards compatability
4266
+ setToolbarPosition: function () {
4267
+ if (this.toolbar) {
4268
+ this.toolbar.setToolbarPosition();
4269
+ }
4270
+ },
4271
+
4272
+ selectAllContents: function () {
4273
+ var currNode = Selection.getSelectionElement(this.options.contentWindow);
4274
+
4275
+ if (currNode) {
4276
+ // Move to the lowest descendant node that still selects all of the contents
4277
+ while (currNode.children.length === 1) {
4278
+ currNode = currNode.children[0];
4279
+ }
4280
+
4281
+ this.selectElement(currNode);
4282
+ }
4283
+ },
4284
+
4285
+ selectElement: function (element) {
4286
+ Selection.selectNode(element, this.options.ownerDocument);
4287
+
4288
+ var selElement = Selection.getSelectionElement(this.options.contentWindow);
4289
+ if (selElement) {
4290
+ this.events.focusElement(selElement);
4291
+ }
4292
+ },
4293
+
4294
+ // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
4295
+ // Tim Down
4296
+ // TODO: move to selection.js and clean up old methods there
4297
+ saveSelection: function () {
4298
+ this.selectionState = null;
4299
+
4300
+ var selection = this.options.contentWindow.getSelection(),
4301
+ range,
4302
+ preSelectionRange,
4303
+ start,
4304
+ editableElementIndex = -1;
4305
+
4306
+ if (selection.rangeCount > 0) {
4307
+ range = selection.getRangeAt(0);
4308
+ preSelectionRange = range.cloneRange();
4309
+
4310
+ // Find element current selection is inside
4311
+ this.elements.some(function (el, index) {
4312
+ if (el === range.startContainer || Util.isDescendant(el, range.startContainer)) {
4313
+ editableElementIndex = index;
4314
+ return true;
4315
+ }
4316
+ return false;
4317
+ });
4318
+
4319
+ if (editableElementIndex > -1) {
4320
+ preSelectionRange.selectNodeContents(this.elements[editableElementIndex]);
4321
+ preSelectionRange.setEnd(range.startContainer, range.startOffset);
4322
+ start = preSelectionRange.toString().length;
4323
+
4324
+ this.selectionState = {
4325
+ start: start,
4326
+ end: start + range.toString().length,
4327
+ editableElementIndex: editableElementIndex
4328
+ };
4329
+ }
4330
+ }
4331
+ },
4332
+
4333
+ // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
4334
+ // Tim Down
4335
+ // TODO: move to selection.js and clean up old methods there
4336
+ restoreSelection: function () {
4337
+ if (!this.selectionState) {
4338
+ return;
4339
+ }
4340
+
4341
+ var editableElement = this.elements[this.selectionState.editableElementIndex],
4342
+ charIndex = 0,
4343
+ range = this.options.ownerDocument.createRange(),
4344
+ nodeStack = [editableElement],
4345
+ node,
4346
+ foundStart = false,
4347
+ stop = false,
4348
+ i,
4349
+ sel,
4350
+ nextCharIndex;
4351
+
4352
+ range.setStart(editableElement, 0);
4353
+ range.collapse(true);
4354
+
4355
+ node = nodeStack.pop();
4356
+ while (!stop && node) {
4357
+ if (node.nodeType === 3) {
4358
+ nextCharIndex = charIndex + node.length;
4359
+ if (!foundStart && this.selectionState.start >= charIndex && this.selectionState.start <= nextCharIndex) {
4360
+ range.setStart(node, this.selectionState.start - charIndex);
4361
+ foundStart = true;
4362
+ }
4363
+ if (foundStart && this.selectionState.end >= charIndex && this.selectionState.end <= nextCharIndex) {
4364
+ range.setEnd(node, this.selectionState.end - charIndex);
4365
+ stop = true;
4366
+ }
4367
+ charIndex = nextCharIndex;
4368
+ } else {
4369
+ i = node.childNodes.length - 1;
4370
+ while (i >= 0) {
4371
+ nodeStack.push(node.childNodes[i]);
4372
+ i -= 1;
4373
+ }
4374
+ }
4375
+ if (!stop) {
4376
+ node = nodeStack.pop();
4377
+ }
4378
+ }
4379
+
4380
+ sel = this.options.contentWindow.getSelection();
4381
+ sel.removeAllRanges();
4382
+ sel.addRange(range);
4383
+ },
4384
+
4385
+ createLink: function (opts) {
4386
+ var customEvent,
4387
+ i;
4388
+
4389
+ if (opts.url && opts.url.trim().length > 0) {
4390
+ this.options.ownerDocument.execCommand('createLink', false, opts.url);
4391
+
4392
+ if (this.options.targetBlank || opts.target === '_blank') {
4393
+ Util.setTargetBlank(Util.getSelectionStart(this.options.ownerDocument));
4394
+ }
4395
+
4396
+ if (opts.buttonClass) {
4397
+ Util.addClassToAnchors(Util.getSelectionStart(this.options.ownerDocument), opts.buttonClass);
4398
+ }
4399
+ }
4400
+
4401
+ if (this.options.targetBlank || opts.target === "_blank" || opts.buttonClass) {
4402
+ customEvent = this.options.ownerDocument.createEvent("HTMLEvents");
4403
+ customEvent.initEvent("input", true, true, this.options.contentWindow);
4404
+ for (i = 0; i < this.elements.length; i += 1) {
4405
+ this.elements[i].dispatchEvent(customEvent);
4406
+ }
4407
+ }
4408
+ },
4409
+
4410
+ // alias for setup - keeping for backwards compatability
4411
+ activate: function () {
4412
+ Util.deprecatedMethod.call(this, 'activate', 'setup', arguments, 'v5.0.0');
4413
+ },
4414
+
4415
+ // alias for destroy - keeping for backwards compatability
4416
+ deactivate: function () {
4417
+ Util.deprecatedMethod.call(this, 'deactivate', 'destroy', arguments, 'v5.0.0');
4418
+ },
4419
+
4420
+ cleanPaste: function (text) {
4421
+ this.getExtensionByName('paste').cleanPaste(text);
4422
+ },
4423
+
4424
+ pasteHTML: function (html, options) {
4425
+ this.getExtensionByName('paste').pasteHTML(html, options);
4426
+ }
4427
+ };
4428
+ }());
4429
+
4430
+ MediumEditor.version = (function(major, minor, revision) {
4431
+ return {
4432
+ major: parseInt(major, 10),
4433
+ minor: parseInt(minor, 10),
4434
+ revision: parseInt(revision, 10),
4435
+ toString: function(){
4436
+ return [major, minor, revision].join(".");
4437
+ }
4438
+ };
4439
+ }).apply(this, ({
4440
+ // grunt-bump looks for this:
4441
+ "version": "4.6.0"
4442
+ }).version.split("."));
4443
+
4444
+ return MediumEditor;
4445
+ }()));