godmin-medium 0.1.0

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