medium-editor-rails 1.4.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d0439b8eb2da727e37c1ebcc2fae29850e53a2f4
4
- data.tar.gz: 6d591143bd341268871c05bc9715686fee0186bd
3
+ metadata.gz: ea0310100e4bb03780e766942afdb94119f88c38
4
+ data.tar.gz: eee6063511bbbe4b7a8c3493100379eb2fa60f60
5
5
  SHA512:
6
- metadata.gz: d299576cf03b9a432c2f683e882dfdb818711bc9a7ca4bdff6e9c6c9c7e7a87b761c799c5094658eba123714d2e1aa0f427516f87c27a6cef0a8ac7819086cfb
7
- data.tar.gz: 82ba196bde1a183da2082f22ee083ab88e1624b7bc9f50426293033ee576df8d27777f5ecf43b48bbb26a5f4571f3a8ae8074934d967472525f07c3386c21a25
6
+ metadata.gz: e8699a3a049805261ddc04f90c73268056cae22d94633b5ca90539019bae92083f4a46de1435db42da4e7f0451c4a9257f01ae8ab86dd7998fc8c6b1774ed19c
7
+ data.tar.gz: deba6b68b29f27f4e44c8df1adfe544030d23831aa1db57c397c06ced4f08b553e5a4e036630d4cd68d83b0191bbe4beb3f8e905d981d88d179fba7776d7ba6c
@@ -1,5 +1,12 @@
1
1
 
2
2
  #### [Current]
3
+ * [c615afc](../../commit/c615afc) - __(Ahmet Sezgin Duran)__ Update Medium Editor files
4
+ * [ba615ed](../../commit/ba615ed) - __(Ahmet Sezgin Duran)__ Merge tag '1.4.5' into develop
5
+
6
+ 1.4.5
7
+
8
+ #### 1.4.5
9
+ * [a55de93](../../commit/a55de93) - __(Ahmet Sezgin Duran)__ Bump versions 1.4.5 and 2.4.5
3
10
  * [0b17fe4](../../commit/0b17fe4) - __(Ahmet Sezgin Duran)__ Update Medium Editor files
4
11
  * [ebfc535](../../commit/ebfc535) - __(Ahmet Sezgin Duran)__ Merge tag '1.4.4' into develop
5
12
 
data/README.md CHANGED
@@ -8,7 +8,7 @@ This gem integrates [Medium Editor](https://github.com/daviferreira/medium-edito
8
8
 
9
9
  ## Version
10
10
 
11
- The latest version of Medium Editor bundled by this gem is [2.4.5](https://github.com/daviferreira/medium-editor/releases)
11
+ The latest version of Medium Editor bundled by this gem is [3.0.0](https://github.com/daviferreira/medium-editor/releases)
12
12
 
13
13
  ## Installation
14
14
 
@@ -1,6 +1,6 @@
1
1
  module MediumEditorRails
2
2
  module Rails
3
- VERSION = '1.4.5'
4
- MEDIUM_EDITOR_VERSION = '2.4.5'
3
+ VERSION = '2.0.0'
4
+ MEDIUM_EDITOR_VERSION = '3.0.0'
5
5
  end
6
6
  end
@@ -1,3 +1,176 @@
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
+
1
174
  (function (root, factory) {
2
175
  'use strict';
3
176
  if (typeof module === 'object') {
@@ -11,12 +184,23 @@
11
184
 
12
185
  'use strict';
13
186
 
14
- var mediumEditorUtil;
187
+ var Util;
15
188
 
16
189
  (function (window, document) {
17
190
  'use strict';
18
191
 
19
- mediumEditorUtil = {
192
+ function copyInto(dest, source, overwrite) {
193
+ var prop;
194
+ dest = dest || {};
195
+ for (prop in source) {
196
+ if (source.hasOwnProperty(prop) && (overwrite || dest.hasOwnProperty(prop) === false)) {
197
+ dest[prop] = source[prop];
198
+ }
199
+ }
200
+ return dest;
201
+ }
202
+
203
+ Util = {
20
204
 
21
205
  // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
22
206
  // by rg89
@@ -34,17 +218,22 @@ var mediumEditorUtil;
34
218
 
35
219
  parentElements: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'],
36
220
 
37
- extend: function extend(b, a) {
38
- var prop;
39
- if (b === undefined) {
40
- return a;
41
- }
42
- for (prop in a) {
43
- if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
44
- b[prop] = a[prop];
45
- }
46
- }
47
- return b;
221
+ defaults: function defaults(dest, source) {
222
+ return copyInto(dest, source);
223
+ },
224
+
225
+ extend: function extend(dest, source) {
226
+ return copyInto(dest, source, true);
227
+ },
228
+
229
+ derives: function derives(base, derived) {
230
+ var origPrototype = derived.prototype;
231
+ function Proto() { }
232
+ Proto.prototype = base.prototype;
233
+ derived.prototype = new Proto();
234
+ derived.prototype.constructor = base;
235
+ derived.prototype = copyInto(derived.prototype, origPrototype);
236
+ return derived;
48
237
  },
49
238
 
50
239
  // Find the next node in the DOM tree that represents any text that is being
@@ -114,7 +303,7 @@ var mediumEditorUtil;
114
303
  }
115
304
 
116
305
  later = function () {
117
- previous = mediumEditorUtil.now();
306
+ previous = Util.now();
118
307
  timeout = null;
119
308
  result = func.apply(context, args);
120
309
  if (!timeout) {
@@ -123,7 +312,7 @@ var mediumEditorUtil;
123
312
  };
124
313
 
125
314
  return function () {
126
- var currNow = mediumEditorUtil.now(),
315
+ var currNow = Util.now(),
127
316
  remaining = wait - (currNow - previous);
128
317
  context = this;
129
318
  args = arguments;
@@ -236,12 +425,12 @@ var mediumEditorUtil;
236
425
  };
237
426
  }(window, document));
238
427
 
239
- var meSelection;
428
+ var Selection;
240
429
 
241
430
  (function (window, document) {
242
431
  'use strict';
243
432
 
244
- meSelection = {
433
+ Selection = {
245
434
  // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
246
435
  // by You
247
436
  getSelectionStart: function (ownerDocument) {
@@ -260,7 +449,7 @@ var meSelection;
260
449
  range = selection.getRangeAt(0);
261
450
  current = range.commonAncestorContainer;
262
451
 
263
- return mediumEditorUtil.traverseUp(current, testElementFunction);
452
+ return Util.traverseUp(current, testElementFunction);
264
453
  },
265
454
 
266
455
  getSelectionElement: function (contentWindow) {
@@ -356,7 +545,7 @@ var meSelection;
356
545
  tagName = el.tagName.toLowerCase();
357
546
  }
358
547
 
359
- while (el && mediumEditorUtil.parentElements.indexOf(tagName) === -1) {
548
+ while (el && Util.parentElements.indexOf(tagName) === -1) {
360
549
  el = el.parentNode;
361
550
  if (el && el.tagName) {
362
551
  tagName = el.tagName.toLowerCase();
@@ -455,14 +644,6 @@ var DefaultButton,
455
644
  contentDefault: '<b>x<sub>1</sub></b>',
456
645
  contentFA: '<i class="fa fa-subscript"></i>'
457
646
  },
458
- 'anchor': {
459
- name: 'anchor',
460
- action: 'anchor',
461
- aria: 'link',
462
- tagNames: ['a'],
463
- contentDefault: '<b>#</b>',
464
- contentFA: '<i class="fa fa-link"></i>'
465
- },
466
647
  'image': {
467
648
  name: 'image',
468
649
  action: 'image',
@@ -646,32 +827,21 @@ var DefaultButton,
646
827
  handleClick: function (evt) {
647
828
  evt.preventDefault();
648
829
  evt.stopPropagation();
649
- var action = this.getAction();
650
- if (!this.base.selection) {
651
- this.base.checkSelection();
652
- }
653
830
 
654
- if (this.isActive()) {
655
- this.deactivate();
656
- } else {
657
- this.activate();
658
- }
831
+ var action = this.getAction();
659
832
 
660
833
  if (action) {
661
- this.base.execAction(action, evt);
834
+ this.base.execAction(action);
662
835
  }
663
- //if (this.options.form) {
664
- // this.base.showForm(this.form, evt);
665
- //}
666
836
  },
667
837
  isActive: function () {
668
838
  return this.button.classList.contains(this.base.options.activeButtonClass);
669
839
  },
670
- deactivate: function () {
840
+ setInactive: function () {
671
841
  this.button.classList.remove(this.base.options.activeButtonClass);
672
842
  delete this.knownState;
673
843
  },
674
- activate: function () {
844
+ setActive: function () {
675
845
  this.button.classList.add(this.base.options.activeButtonClass);
676
846
  delete this.knownState;
677
847
  },
@@ -686,11 +856,12 @@ var DefaultButton,
686
856
  }
687
857
  return queryState;
688
858
  },
689
- shouldActivate: function (node) {
859
+ isAlreadyApplied: function (node) {
690
860
  var isMatch = false,
691
861
  tagNames = this.getTagNames(),
692
862
  styleVals,
693
863
  computedStyle;
864
+
694
865
  if (this.knownState === false || this.knownState === true) {
695
866
  return this.knownState;
696
867
  }
@@ -789,20 +960,20 @@ var pasteHandler;
789
960
  paragraphs = evt.clipboardData.getData(dataFormatPlain).split(/[\r\n]/g);
790
961
  for (p = 0; p < paragraphs.length; p += 1) {
791
962
  if (paragraphs[p] !== '') {
792
- html += '<p>' + mediumEditorUtil.htmlEntities(paragraphs[p]) + '</p>';
963
+ html += '<p>' + Util.htmlEntities(paragraphs[p]) + '</p>';
793
964
  }
794
965
  }
795
- mediumEditorUtil.insertHTMLCommand(options.ownerDocument, html);
966
+ Util.insertHTMLCommand(options.ownerDocument, html);
796
967
  } else {
797
- html = mediumEditorUtil.htmlEntities(evt.clipboardData.getData(dataFormatPlain));
798
- mediumEditorUtil.insertHTMLCommand(options.ownerDocument, html);
968
+ html = Util.htmlEntities(evt.clipboardData.getData(dataFormatPlain));
969
+ Util.insertHTMLCommand(options.ownerDocument, html);
799
970
  }
800
971
  }
801
972
  },
802
973
 
803
974
  cleanPaste: function (text, options) {
804
975
  var i, elList, workEl,
805
- el = meSelection.getSelectionElement(options.contentWindow),
976
+ el = Selection.getSelectionElement(options.contentWindow),
806
977
  multiline = /<p|<br|<div/.test(text),
807
978
  replacements = createReplacements();
808
979
 
@@ -815,7 +986,10 @@ var pasteHandler;
815
986
  elList = text.split('<br><br>');
816
987
 
817
988
  this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>', options.ownerDocument);
818
- options.ownerDocument.execCommand('insertText', false, "\n");
989
+
990
+ try {
991
+ options.ownerDocument.execCommand('insertText', false, "\n");
992
+ } catch (ignore) { }
819
993
 
820
994
  // block element cleanup
821
995
  elList = el.querySelectorAll('a,p,div,br');
@@ -825,7 +999,7 @@ var pasteHandler;
825
999
  switch (workEl.tagName.toLowerCase()) {
826
1000
  case 'a':
827
1001
  if (options.targetBlank) {
828
- mediumEditorUtil.setTargetBlank(workEl);
1002
+ Util.setTargetBlank(workEl);
829
1003
  }
830
1004
  break;
831
1005
  case 'p':
@@ -865,7 +1039,7 @@ var pasteHandler;
865
1039
  workEl.parentNode.removeChild(workEl);
866
1040
  }
867
1041
  }
868
- mediumEditorUtil.insertHTMLCommand(ownerDocument, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
1042
+ Util.insertHTMLCommand(ownerDocument, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
869
1043
  },
870
1044
  isCommonBlock: function (el) {
871
1045
  return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
@@ -882,7 +1056,7 @@ var pasteHandler;
882
1056
  } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
883
1057
  // remove br's just inside open or close tags of a div/p
884
1058
  el.parentNode.removeChild(el);
885
- } else if (el.parentNode.childElementCount === 1) {
1059
+ } else if (el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
886
1060
  // and br's that are the only child of a div/p
887
1061
  this.removeWithParent(el);
888
1062
  }
@@ -927,7 +1101,7 @@ var pasteHandler;
927
1101
  el = spans[i];
928
1102
 
929
1103
  // bail if span is in contenteditable = false
930
- if (mediumEditorUtil.traverseUp(el, isCEF)) {
1104
+ if (Util.traverseUp(el, isCEF)) {
931
1105
  return false;
932
1106
  }
933
1107
 
@@ -947,12 +1121,48 @@ var AnchorExtension;
947
1121
  (function (window, document) {
948
1122
  'use strict';
949
1123
 
950
- AnchorExtension = function (instance) {
951
- this.base = instance;
952
- };
1124
+ function AnchorDerived() {
1125
+ this.parent = true;
1126
+ this.options = {
1127
+ name: 'anchor',
1128
+ action: 'createLink',
1129
+ aria: 'link',
1130
+ tagNames: ['a'],
1131
+ contentDefault: '<b>#</b>',
1132
+ contentFA: '<i class="fa fa-link"></i>'
1133
+ };
1134
+ this.name = 'anchor';
1135
+ this.hasForm = true;
1136
+ }
1137
+
1138
+ AnchorDerived.prototype = {
953
1139
 
954
- AnchorExtension.prototype = {
1140
+ // Button and Extension handling
955
1141
 
1142
+ // Called when the button the toolbar is clicked
1143
+ // Overrides DefaultButton.handleClick
1144
+ handleClick: function (evt) {
1145
+ evt.preventDefault();
1146
+ evt.stopPropagation();
1147
+
1148
+ if (!this.base.selection) {
1149
+ this.base.checkSelection();
1150
+ }
1151
+
1152
+ var selectedParentElement = Selection.getSelectedParentElement(this.base.selectionRange);
1153
+ if (selectedParentElement.tagName &&
1154
+ selectedParentElement.tagName.toLowerCase() === 'a') {
1155
+ return this.base.execAction('unlink');
1156
+ }
1157
+
1158
+ if (!this.isDisplayed()) {
1159
+ this.showForm();
1160
+ }
1161
+
1162
+ return false;
1163
+ },
1164
+
1165
+ // Called by medium-editor to append form to the toolbar
956
1166
  getForm: function () {
957
1167
  if (!this.anchorForm) {
958
1168
  this.anchorForm = this.createForm();
@@ -960,10 +1170,30 @@ var AnchorExtension;
960
1170
  return this.anchorForm;
961
1171
  },
962
1172
 
963
- getInput: function () {
964
- return this.getForm().querySelector('input.medium-editor-toolbar-input');
1173
+ // Used by medium-editor when the default toolbar is to be displayed
1174
+ isDisplayed: function () {
1175
+ return this.getForm().style.display === 'block';
965
1176
  },
966
1177
 
1178
+ hideForm: function () {
1179
+ this.getForm().style.display = 'none';
1180
+ this.getInput().value = '';
1181
+ },
1182
+
1183
+ showForm: function (link_value) {
1184
+ var input = this.getInput();
1185
+
1186
+ this.base.saveSelection();
1187
+ this.base.hideToolbarDefaultActions();
1188
+ this.getForm().style.display = 'block';
1189
+ this.base.setToolbarPosition();
1190
+ this.base.keepToolbarAlive = true;
1191
+
1192
+ input.value = link_value || '';
1193
+ input.focus();
1194
+ },
1195
+
1196
+ // Called by core when tearing down medium-editor (deactivate)
967
1197
  deactivate: function () {
968
1198
  if (!this.anchorForm) {
969
1199
  return false;
@@ -976,38 +1206,48 @@ var AnchorExtension;
976
1206
  delete this.anchorForm;
977
1207
  },
978
1208
 
1209
+ // core methods
1210
+
979
1211
  doLinkCreation: function () {
980
- var button = null,
981
- target,
982
- targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'),
983
- buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button');
1212
+ var targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'),
1213
+ buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button'),
1214
+ opts = {
1215
+ url: this.getInput().value
1216
+ };
1217
+
1218
+ this.base.restoreSelection();
1219
+
1220
+ if (this.base.options.checkLinkFormat) {
1221
+ opts.url = this.checkLinkFormat(opts.url);
1222
+ }
984
1223
 
985
1224
  if (targetCheckbox && targetCheckbox.checked) {
986
- target = "_blank";
1225
+ opts.target = "_blank";
987
1226
  } else {
988
- target = "_self";
1227
+ opts.target = "_self";
989
1228
  }
990
1229
 
991
1230
  if (buttonCheckbox && buttonCheckbox.checked) {
992
- button = this.base.options.anchorButtonClass;
1231
+ opts.buttonClass = this.base.options.anchorButtonClass;
993
1232
  }
994
1233
 
995
- this.base.createLink(this.getInput(), target, button);
1234
+ this.base.createLink(opts);
1235
+ this.base.keepToolbarAlive = false;
1236
+ this.base.checkSelection();
1237
+ },
1238
+
1239
+ checkLinkFormat: function (value) {
1240
+ var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
1241
+ return (re.test(value) ? '' : 'http://') + value;
996
1242
  },
997
1243
 
998
1244
  doFormCancel: function () {
999
- this.base.showToolbarActions();
1000
1245
  this.base.restoreSelection();
1246
+ this.base.keepToolbarAlive = false;
1247
+ this.base.checkSelection();
1001
1248
  },
1002
1249
 
1003
- handleOutsideInteraction: function (event) {
1004
- if (event.target !== this.getForm() &&
1005
- !mediumEditorUtil.isDescendant(this.getForm(), event.target) &&
1006
- !mediumEditorUtil.isDescendant(this.base.toolbarActions, event.target)) {
1007
- this.base.keepToolbarAlive = false;
1008
- this.base.checkSelection();
1009
- }
1010
- },
1250
+ // form creation and event handling
1011
1251
 
1012
1252
  createForm: function () {
1013
1253
  var doc = this.base.options.ownerDocument,
@@ -1025,10 +1265,7 @@ var AnchorExtension;
1025
1265
  form.id = 'medium-editor-toolbar-form-anchor-' + this.base.id;
1026
1266
 
1027
1267
  // Handle clicks on the form itself
1028
- this.base.on(form, 'click', function (event) {
1029
- event.stopPropagation();
1030
- this.base.keepToolbarAlive = true;
1031
- }.bind(this));
1268
+ this.base.on(form, 'click', this.handleFormClick.bind(this));
1032
1269
 
1033
1270
  // Add url textbox
1034
1271
  input.setAttribute('type', 'text');
@@ -1037,53 +1274,32 @@ var AnchorExtension;
1037
1274
  form.appendChild(input);
1038
1275
 
1039
1276
  // Handle typing in the textbox
1040
- this.base.on(input, 'keyup', function (event) {
1041
- // For ENTER -> create the anchor
1042
- if (event.keyCode === mediumEditorUtil.keyCode.ENTER) {
1043
- event.preventDefault();
1044
- this.doLinkCreation();
1045
- return;
1046
- }
1047
-
1048
- // For ESCAPE -> close the form
1049
- if (event.keyCode === mediumEditorUtil.keyCode.ESCAPE) {
1050
- event.preventDefault();
1051
- this.doFormCancel();
1052
- }
1053
- }.bind(this));
1277
+ this.base.on(input, 'keyup', this.handleTextboxKeyup.bind(this));
1054
1278
 
1055
1279
  // Handle clicks into the textbox
1056
- this.base.on(input, 'click', function (event) {
1057
- // make sure not to hide form when cliking into the input
1058
- event.stopPropagation();
1059
- this.base.keepToolbarAlive = true;
1060
- }.bind(this));
1280
+ this.base.on(input, 'click', this.handleFormClick.bind(this));
1061
1281
 
1062
1282
  // Add save buton
1063
1283
  save.setAttribute('href', '#');
1064
1284
  save.className = 'medium-editor-toobar-save';
1065
- save.innerHTML = '&#10003;';
1285
+ save.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?
1286
+ '<i class="fa fa-check"></i>' :
1287
+ '&#10003;';
1066
1288
  form.appendChild(save);
1067
1289
 
1068
1290
  // Handle save button clicks (capture)
1069
- this.base.on(save, 'click', function (event) {
1070
- // Clicking Save -> create the anchor
1071
- event.preventDefault();
1072
- this.doLinkCreation();
1073
- }.bind(this), true);
1291
+ this.base.on(save, 'click', this.handleSaveClick.bind(this), true);
1074
1292
 
1075
1293
  // Add close button
1076
1294
  close.setAttribute('href', '#');
1077
1295
  close.className = 'medium-editor-toobar-close';
1078
- close.innerHTML = '&times;';
1296
+ close.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?
1297
+ '<i class="fa fa-times"></i>' :
1298
+ '&times;';
1079
1299
  form.appendChild(close);
1080
1300
 
1081
1301
  // Handle close button clicks
1082
- this.base.on(close, 'click', function (event) {
1083
- // Click Close -> close the form
1084
- event.preventDefault();
1085
- this.doFormCancel();
1086
- }.bind(this));
1302
+ this.base.on(close, 'click', this.handleCloseClick.bind(this));
1087
1303
 
1088
1304
  // (Optional) Add 'open in new window' checkbox
1089
1305
  if (this.base.options.anchorTarget) {
@@ -1118,33 +1334,56 @@ var AnchorExtension;
1118
1334
  return form;
1119
1335
  },
1120
1336
 
1121
- focus: function (value) {
1122
- var input = this.getInput();
1123
- input.focus();
1124
- input.value = value || '';
1337
+ getInput: function () {
1338
+ return this.getForm().querySelector('input.medium-editor-toolbar-input');
1125
1339
  },
1126
1340
 
1127
- hideForm: function () {
1128
- this.getForm().style.display = 'none';
1341
+ handleOutsideInteraction: function (event) {
1342
+ if (event.target !== this.getForm() &&
1343
+ !Util.isDescendant(this.getForm(), event.target) &&
1344
+ !Util.isDescendant(this.base.toolbarActions, event.target)) {
1345
+ this.base.keepToolbarAlive = false;
1346
+ this.base.checkSelection();
1347
+ }
1129
1348
  },
1130
1349
 
1131
- showForm: function () {
1132
- this.getForm().style.display = 'block';
1350
+ handleTextboxKeyup: function (event) {
1351
+ // For ENTER -> create the anchor
1352
+ if (event.keyCode === Util.keyCode.ENTER) {
1353
+ event.preventDefault();
1354
+ this.doLinkCreation();
1355
+ return;
1356
+ }
1357
+
1358
+ // For ESCAPE -> close the form
1359
+ if (event.keyCode === Util.keyCode.ESCAPE) {
1360
+ event.preventDefault();
1361
+ this.doFormCancel();
1362
+ }
1133
1363
  },
1134
1364
 
1135
- isDisplayed: function () {
1136
- return this.getForm().style.display === 'block';
1365
+ handleFormClick: function (event) {
1366
+ // make sure not to hide form when clicking inside the form
1367
+ event.stopPropagation();
1368
+ this.base.keepToolbarAlive = true;
1137
1369
  },
1138
1370
 
1139
- isClickIntoForm: function (event) {
1140
- return (event &&
1141
- event.type &&
1142
- event.type.toLowerCase() === 'blur' &&
1143
- event.relatedTarget &&
1144
- event.relatedTarget === this.getInput());
1371
+ handleSaveClick: function (event) {
1372
+ // Clicking Save -> create the anchor
1373
+ event.preventDefault();
1374
+ this.doLinkCreation();
1375
+ },
1376
+
1377
+ handleCloseClick: function (event) {
1378
+ // Click Close -> close the form
1379
+ event.preventDefault();
1380
+ this.doFormCancel();
1145
1381
  }
1146
1382
  };
1383
+
1384
+ AnchorExtension = Util.derives(DefaultButton, AnchorDerived);
1147
1385
  }(window, document));
1386
+
1148
1387
  function MediumEditor(elements, options) {
1149
1388
  'use strict';
1150
1389
  return this.init(elements, options);
@@ -1176,8 +1415,8 @@ function MediumEditor(elements, options) {
1176
1415
  disableDoubleReturn: false,
1177
1416
  disableToolbar: false,
1178
1417
  disableEditing: false,
1179
- disableAnchorForm: false,
1180
1418
  disablePlaceholders: false,
1419
+ toolbarAlign: 'center',
1181
1420
  elementsContainer: false,
1182
1421
  imageDragging: true,
1183
1422
  standardizeSelectionStart: false,
@@ -1200,7 +1439,7 @@ function MediumEditor(elements, options) {
1200
1439
  init: function (elements, options) {
1201
1440
  var uniqueId = 1;
1202
1441
 
1203
- this.options = mediumEditorUtil.extend(options, this.defaults);
1442
+ this.options = Util.defaults(options, this.defaults);
1204
1443
  this.setElementSelection(elements);
1205
1444
  if (this.elements.length === 0) {
1206
1445
  return;
@@ -1281,7 +1520,7 @@ function MediumEditor(elements, options) {
1281
1520
  // handleResize is throttled because:
1282
1521
  // - It will be called when the browser is resizing, which can fire many times very quickly
1283
1522
  // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
1284
- this.handleResize = mediumEditorUtil.throttle(function () {
1523
+ this.handleResize = Util.throttle(function () {
1285
1524
  if (self.isActive) {
1286
1525
  self.positionToolbarIfShown();
1287
1526
  }
@@ -1291,7 +1530,7 @@ function MediumEditor(elements, options) {
1291
1530
  // - This method could be called many times due to the type of event handlers that are calling it
1292
1531
  // - We want a slight delay so that other events in the stack can run, some of which may
1293
1532
  // prevent the toolbar from being hidden (via this.keepToolbarAlive).
1294
- this.handleBlur = mediumEditorUtil.throttle(function () {
1533
+ this.handleBlur = Util.throttle(function () {
1295
1534
  if (self.isActive && !self.keepToolbarAlive) {
1296
1535
  self.hideToolbarActions();
1297
1536
  }
@@ -1321,7 +1560,7 @@ function MediumEditor(elements, options) {
1321
1560
  // Init toolbar
1322
1561
  if (addToolbar) {
1323
1562
  this.initToolbar()
1324
- .bindButtons()
1563
+ .setFirstAndLastButtons()
1325
1564
  .bindAnchorPreview();
1326
1565
  }
1327
1566
  return this;
@@ -1336,7 +1575,7 @@ function MediumEditor(elements, options) {
1336
1575
  selector = this.options.ownerDocument.querySelectorAll(selector);
1337
1576
  }
1338
1577
  // If element, put into array
1339
- if (mediumEditorUtil.isElement(selector)) {
1578
+ if (Util.isElement(selector)) {
1340
1579
  selector = [selector];
1341
1580
  }
1342
1581
  // Convert NodeList (or other array like object) into an array
@@ -1350,15 +1589,15 @@ function MediumEditor(elements, options) {
1350
1589
  selection = self.options.contentWindow.getSelection(),
1351
1590
  selRange = selection.isCollapsed ?
1352
1591
  null :
1353
- meSelection.getSelectedParentElement(selection.getRangeAt(0)),
1592
+ Selection.getSelectedParentElement(selection.getRangeAt(0)),
1354
1593
  i;
1355
1594
 
1356
1595
  // This control was introduced also to avoid the toolbar
1357
1596
  // to disapper when selecting from right to left and
1358
1597
  // the selection ends at the beginning of the text.
1359
1598
  for (i = 0; i < self.elements.length; i += 1) {
1360
- if (mediumEditorUtil.isDescendant(self.elements[i], e.target)
1361
- || mediumEditorUtil.isDescendant(self.elements[i], selRange)) {
1599
+ if (Util.isDescendant(self.elements[i], e.target)
1600
+ || Util.isDescendant(self.elements[i], selRange)) {
1362
1601
  isDescendantOfEditorElements = true;
1363
1602
  break;
1364
1603
  }
@@ -1367,8 +1606,8 @@ function MediumEditor(elements, options) {
1367
1606
  if (e.target !== self.toolbar
1368
1607
  && self.elements.indexOf(e.target) === -1
1369
1608
  && !isDescendantOfEditorElements
1370
- && !mediumEditorUtil.isDescendant(self.toolbar, e.target)
1371
- && !mediumEditorUtil.isDescendant(self.anchorPreview, e.target)) {
1609
+ && !Util.isDescendant(self.toolbar, e.target)
1610
+ && !Util.isDescendant(self.anchorPreview, e.target)) {
1372
1611
 
1373
1612
  // Activate the placeholder
1374
1613
  if (!self.options.disablePlaceholders) {
@@ -1481,6 +1720,9 @@ function MediumEditor(elements, options) {
1481
1720
  if (extensions[buttonName]) {
1482
1721
  ext = this.initExtension(extensions[buttonName], buttonName);
1483
1722
  this.commands.push(ext);
1723
+ } else if (buttonName === 'anchor') {
1724
+ ext = this.initExtension(new AnchorExtension(), buttonName);
1725
+ this.commands.push(ext);
1484
1726
  } else if (ButtonsData.hasOwnProperty(buttonName)) {
1485
1727
  ext = new DefaultButton(ButtonsData[buttonName], this);
1486
1728
  this.commands.push(ext);
@@ -1496,6 +1738,18 @@ function MediumEditor(elements, options) {
1496
1738
  return this;
1497
1739
  },
1498
1740
 
1741
+ getExtensionByName: function (name) {
1742
+ var extension;
1743
+ if (this.commands && this.commands.length) {
1744
+ this.commands.forEach(function (ext) {
1745
+ if (ext.name === name) {
1746
+ extension = ext;
1747
+ }
1748
+ });
1749
+ }
1750
+ return extension;
1751
+ },
1752
+
1499
1753
  /**
1500
1754
  * Helper function to call a method with a number of parameters on all registered extensions.
1501
1755
  * The function assures that the function exists before calling.
@@ -1528,8 +1782,8 @@ function MediumEditor(elements, options) {
1528
1782
  this.on(this.elements[index], 'keypress', function (e) {
1529
1783
  var node,
1530
1784
  tagName;
1531
- if (e.which === mediumEditorUtil.keyCode.SPACE) {
1532
- node = meSelection.getSelectionStart(self.options.ownerDocument);
1785
+ if (e.which === Util.keyCode.SPACE) {
1786
+ node = Selection.getSelectionStart(self.options.ownerDocument);
1533
1787
  tagName = node.tagName.toLowerCase();
1534
1788
  if (tagName === 'a') {
1535
1789
  self.options.ownerDocument.execCommand('unlink', false, null);
@@ -1538,20 +1792,20 @@ function MediumEditor(elements, options) {
1538
1792
  });
1539
1793
 
1540
1794
  this.on(this.elements[index], 'keyup', function (e) {
1541
- var node = meSelection.getSelectionStart(self.options.ownerDocument),
1795
+ var node = Selection.getSelectionStart(self.options.ownerDocument),
1542
1796
  tagName,
1543
1797
  editorElement;
1544
1798
 
1545
1799
  if (node && node.getAttribute('data-medium-element') && node.children.length === 0 && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
1546
1800
  self.options.ownerDocument.execCommand('formatBlock', false, 'p');
1547
1801
  }
1548
- if (e.which === mediumEditorUtil.keyCode.ENTER) {
1549
- node = meSelection.getSelectionStart(self.options.ownerDocument);
1802
+ if (e.which === Util.keyCode.ENTER) {
1803
+ node = Selection.getSelectionStart(self.options.ownerDocument);
1550
1804
  tagName = node.tagName.toLowerCase();
1551
- editorElement = meSelection.getSelectionElement(self.options.contentWindow);
1805
+ editorElement = Selection.getSelectionElement(self.options.contentWindow);
1552
1806
 
1553
1807
  if (!(self.options.disableReturn || editorElement.getAttribute('data-disable-return')) &&
1554
- tagName !== 'li' && !mediumEditorUtil.isListItemChild(node)) {
1808
+ tagName !== 'li' && !Util.isListItemChild(node)) {
1555
1809
  if (!e.shiftKey) {
1556
1810
 
1557
1811
  // paragraph creation should not be forced within a header tag
@@ -1571,11 +1825,11 @@ function MediumEditor(elements, options) {
1571
1825
  bindReturn: function (index) {
1572
1826
  var self = this;
1573
1827
  this.on(this.elements[index], 'keypress', function (e) {
1574
- if (e.which === mediumEditorUtil.keyCode.ENTER) {
1828
+ if (e.which === Util.keyCode.ENTER) {
1575
1829
  if (self.options.disableReturn || this.getAttribute('data-disable-return')) {
1576
1830
  e.preventDefault();
1577
1831
  } else if (self.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
1578
- var node = meSelection.getSelectionStart(self.options.contentWindow);
1832
+ var node = Selection.getSelectionStart(self.options.contentWindow);
1579
1833
  if (node && node.textContent.trim() === '') {
1580
1834
  e.preventDefault();
1581
1835
  }
@@ -1590,9 +1844,9 @@ function MediumEditor(elements, options) {
1590
1844
  this.on(this.elements[index], 'keydown', function (e) {
1591
1845
  var node, tag, key;
1592
1846
 
1593
- if (e.which === mediumEditorUtil.keyCode.TAB) {
1847
+ if (e.which === Util.keyCode.TAB) {
1594
1848
  // Override tab only for pre nodes
1595
- node = meSelection.getSelectionStart(self.options.ownerDocument);
1849
+ node = Selection.getSelectionStart(self.options.ownerDocument);
1596
1850
  tag = node && node.tagName.toLowerCase();
1597
1851
 
1598
1852
  if (tag === 'pre') {
@@ -1601,7 +1855,7 @@ function MediumEditor(elements, options) {
1601
1855
  }
1602
1856
 
1603
1857
  // Tab to indent list structures!
1604
- if (tag === 'li' || mediumEditorUtil.isListItemChild(node)) {
1858
+ if (tag === 'li' || Util.isListItemChild(node)) {
1605
1859
  e.preventDefault();
1606
1860
 
1607
1861
  // If Shift is down, outdent, otherwise indent
@@ -1611,12 +1865,12 @@ function MediumEditor(elements, options) {
1611
1865
  self.options.ownerDocument.execCommand('indent', e);
1612
1866
  }
1613
1867
  }
1614
- } else if (e.which === mediumEditorUtil.keyCode.BACKSPACE || e.which === mediumEditorUtil.keyCode.DELETE || e.which === mediumEditorUtil.keyCode.ENTER) {
1868
+ } else if (e.which === Util.keyCode.BACKSPACE || e.which === Util.keyCode.DELETE || e.which === Util.keyCode.ENTER) {
1615
1869
 
1616
1870
  // Bind keys which can create or destroy a block element: backspace, delete, return
1617
1871
  self.onBlockModifier(e);
1618
1872
 
1619
- } else if (e.ctrlKey) {
1873
+ } else if (e.ctrlKey || e.metaKey) {
1620
1874
  key = String.fromCharCode(e.which || e.keyCode).toLowerCase();
1621
1875
  self.commands.forEach(function (extension) {
1622
1876
  if (extension.options.key && extension.options.key === key) {
@@ -1629,24 +1883,24 @@ function MediumEditor(elements, options) {
1629
1883
  },
1630
1884
 
1631
1885
  onBlockModifier: function (e) {
1632
- var range, sel, p, node = meSelection.getSelectionStart(this.options.ownerDocument),
1886
+ var range, sel, p, node = Selection.getSelectionStart(this.options.ownerDocument),
1633
1887
  tagName = node.tagName.toLowerCase(),
1634
1888
  isEmpty = /^(\s+|<br\/?>)?$/i,
1635
1889
  isHeader = /h\d/i;
1636
1890
 
1637
- if ((e.which === mediumEditorUtil.keyCode.BACKSPACE || e.which === mediumEditorUtil.keyCode.ENTER)
1891
+ if ((e.which === Util.keyCode.BACKSPACE || e.which === Util.keyCode.ENTER)
1638
1892
  && node.previousElementSibling
1639
1893
  // in a header
1640
1894
  && isHeader.test(tagName)
1641
1895
  // at the very end of the block
1642
- && meSelection.getCaretOffsets(node).left === 0) {
1643
- if (e.which === mediumEditorUtil.keyCode.BACKSPACE && isEmpty.test(node.previousElementSibling.innerHTML)) {
1896
+ && Selection.getCaretOffsets(node).left === 0) {
1897
+ if (e.which === Util.keyCode.BACKSPACE && isEmpty.test(node.previousElementSibling.innerHTML)) {
1644
1898
  // backspacing the begining of a header into an empty previous element will
1645
1899
  // change the tagName of the current node to prevent one
1646
1900
  // instead delete previous node and cancel the event.
1647
1901
  node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
1648
1902
  e.preventDefault();
1649
- } else if (e.which === mediumEditorUtil.keyCode.ENTER) {
1903
+ } else if (e.which === Util.keyCode.ENTER) {
1650
1904
  // hitting return in the begining of a header will create empty header elements before the current one
1651
1905
  // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
1652
1906
  p = this.options.ownerDocument.createElement('p');
@@ -1654,7 +1908,7 @@ function MediumEditor(elements, options) {
1654
1908
  node.previousElementSibling.parentNode.insertBefore(p, node);
1655
1909
  e.preventDefault();
1656
1910
  }
1657
- } else if (e.which === mediumEditorUtil.keyCode.DELETE
1911
+ } else if (e.which === Util.keyCode.DELETE
1658
1912
  && node.nextElementSibling
1659
1913
  && node.previousElementSibling
1660
1914
  // not in a header
@@ -1693,8 +1947,6 @@ function MediumEditor(elements, options) {
1693
1947
  this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions');
1694
1948
  this.anchorPreview = this.createAnchorPreview();
1695
1949
 
1696
- this.addExtensionForms();
1697
-
1698
1950
  return this;
1699
1951
  },
1700
1952
 
@@ -1710,10 +1962,14 @@ function MediumEditor(elements, options) {
1710
1962
  }
1711
1963
 
1712
1964
  toolbar.appendChild(this.toolbarButtons());
1713
- if (!this.options.disableAnchorForm) {
1714
- this.anchorExtension = new AnchorExtension(this);
1715
- toolbar.appendChild(this.anchorExtension.getForm());
1716
- }
1965
+
1966
+ // Add any forms that extensions may have
1967
+ this.commands.forEach(function (extension) {
1968
+ if (extension.hasForm) {
1969
+ toolbar.appendChild(extension.getForm());
1970
+ }
1971
+ });
1972
+
1717
1973
  this.options.elementsContainer.appendChild(toolbar);
1718
1974
  return toolbar;
1719
1975
  },
@@ -1731,7 +1987,7 @@ function MediumEditor(elements, options) {
1731
1987
  if (typeof extension.getButton === 'function') {
1732
1988
  btn = extension.getButton(this);
1733
1989
  li = this.options.ownerDocument.createElement('li');
1734
- if (mediumEditorUtil.isElement(btn)) {
1990
+ if (Util.isElement(btn)) {
1735
1991
  li.appendChild(btn);
1736
1992
  } else {
1737
1993
  li.innerHTML = btn;
@@ -1743,89 +1999,36 @@ function MediumEditor(elements, options) {
1743
1999
  return ul;
1744
2000
  },
1745
2001
 
1746
- addExtensionForms: function () {
1747
- var form,
1748
- id;
1749
-
1750
- this.commands.forEach(function (extension) {
1751
- if (extension.hasForm) {
1752
- form = (typeof extension.getForm === 'function') ? extension.getForm() : null;
1753
- }
1754
- if (form) {
1755
- id = 'medium-editor-toolbar-form-' + extension.name + '-' + this.id;
1756
- form.className += ' medium-editor-toolbar-form';
1757
- form.id = id;
1758
- this.toolbar.appendChild(form);
1759
- }
1760
- }.bind(this));
1761
- },
1762
-
1763
2002
  bindSelect: function () {
1764
- var self = this,
1765
- i,
1766
- timeoutHelper;
1767
-
1768
- this.checkSelectionWrapper = function (e) {
1769
- // Do not close the toolbar when bluring the editable area and clicking into the anchor form
1770
- if (e && this.anchorExtension && this.anchorExtension.isClickIntoForm(e)) {
1771
- return false;
1772
- }
1773
-
1774
- self.checkSelection();
1775
- };
1776
-
1777
- timeoutHelper = function (event) {
1778
- setTimeout(function () {
1779
- this.checkSelectionWrapper(event);
1780
- }.bind(this), 0);
1781
- }.bind(this);
2003
+ var i,
2004
+ blurHelper = function (event) {
2005
+ // Do not close the toolbar when bluring the editable area and clicking into the anchor form
2006
+ if (event &&
2007
+ event.type &&
2008
+ event.type.toLowerCase() === 'blur' &&
2009
+ event.relatedTarget &&
2010
+ Util.isDescendant(this.toolbar, event.relatedTarget)) {
2011
+ return false;
2012
+ }
2013
+ this.checkSelection();
2014
+ }.bind(this),
2015
+ timeoutHelper = function () {
2016
+ setTimeout(function () {
2017
+ this.checkSelection();
2018
+ }.bind(this), 0);
2019
+ }.bind(this);
1782
2020
 
1783
- this.on(this.options.ownerDocument.documentElement, 'mouseup', this.checkSelectionWrapper);
2021
+ this.on(this.options.ownerDocument.documentElement, 'mouseup', this.checkSelection.bind(this));
1784
2022
 
1785
2023
  for (i = 0; i < this.elements.length; i += 1) {
1786
- this.on(this.elements[i], 'keyup', this.checkSelectionWrapper);
1787
- this.on(this.elements[i], 'blur', this.checkSelectionWrapper);
2024
+ this.on(this.elements[i], 'keyup', this.checkSelection.bind(this));
2025
+ this.on(this.elements[i], 'blur', blurHelper);
1788
2026
  this.on(this.elements[i], 'click', timeoutHelper);
1789
2027
  }
1790
2028
 
1791
2029
  return this;
1792
2030
  },
1793
2031
 
1794
- // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
1795
- insertHTML: function insertHTML(html) {
1796
- var selection, range, el, fragment, node, lastNode;
1797
-
1798
- if (this.options.ownerDocument.queryCommandSupported('insertHTML')) {
1799
- try {
1800
- return this.options.ownerDocument.execCommand('insertHTML', false, html);
1801
- } catch (ignore) {}
1802
- }
1803
-
1804
- selection = window.getSelection();
1805
- if (selection.getRangeAt && selection.rangeCount) {
1806
- range = selection.getRangeAt(0);
1807
- range.deleteContents();
1808
-
1809
- el = this.options.ownerDocument.createElement("div");
1810
- el.innerHTML = html;
1811
- fragment = this.options.ownerDocument.createDocumentFragment();
1812
- while (el.firstChild) {
1813
- node = el.firstChild;
1814
- lastNode = fragment.appendChild(node);
1815
- }
1816
- range.insertNode(fragment);
1817
-
1818
- // Preserve the selection:
1819
- if (lastNode) {
1820
- range = range.cloneRange();
1821
- range.setStartAfter(lastNode);
1822
- range.collapse(true);
1823
- selection.removeAllRanges();
1824
- selection.addRange(range);
1825
- }
1826
- }
1827
- },
1828
-
1829
2032
  bindDragDrop: function () {
1830
2033
  var self = this, i, className, onDrag, onDrop, element;
1831
2034
 
@@ -1858,7 +2061,7 @@ function MediumEditor(elements, options) {
1858
2061
  fileReader.readAsDataURL(file);
1859
2062
 
1860
2063
  id = 'medium-img-' + (+new Date());
1861
- mediumEditorUtil.insertHTMLCommand(self.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
2064
+ Util.insertHTMLCommand(self.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
1862
2065
 
1863
2066
  fileReader.onload = function () {
1864
2067
  var img = document.getElementById(id);
@@ -1901,20 +2104,17 @@ function MediumEditor(elements, options) {
1901
2104
  !this.options.disableToolbar) {
1902
2105
 
1903
2106
  newSelection = this.options.contentWindow.getSelection();
1904
-
1905
2107
  if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
1906
- (this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs()) ||
1907
- meSelection.selectionInContentEditableFalse(this.options.contentWindow)) {
1908
-
2108
+ (this.options.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected()) ||
2109
+ Selection.selectionInContentEditableFalse(this.options.contentWindow)) {
1909
2110
  if (!this.options.staticToolbar) {
1910
2111
  this.hideToolbarActions();
1911
- } else if (this.anchorExtension && this.anchorExtension.isDisplayed()) {
1912
- this.setToolbarButtonStates();
1913
- this.showToolbarActions();
2112
+ } else {
2113
+ this.showAndUpdateToolbar();
1914
2114
  }
1915
2115
 
1916
2116
  } else {
1917
- selectionElement = meSelection.getSelectionElement(this.options.contentWindow);
2117
+ selectionElement = Selection.getSelectionElement(this.options.contentWindow);
1918
2118
  if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
1919
2119
  if (!this.options.staticToolbar) {
1920
2120
  this.hideToolbarActions();
@@ -1927,11 +2127,14 @@ function MediumEditor(elements, options) {
1927
2127
  return this;
1928
2128
  },
1929
2129
 
1930
- hasMultiParagraphs: function () {
1931
- var selectionHtml = meSelection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
1932
- hasMultiParagraphs = selectionHtml.match(/<(p|h[0-6]|blockquote)>([\s\S]*?)<\/(p|h[0-6]|blockquote)>/g);
2130
+ // Checks for existance of multiple block elements in the current selection
2131
+ multipleBlockElementsSelected: function () {
2132
+ /*jslint regexp: true*/
2133
+ var selectionHtml = Selection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
2134
+ hasMultiParagraphs = selectionHtml.match(/<(p|h[1-6]|blockquote)[^>]*>/g);
2135
+ /*jslint regexp: false*/
1933
2136
 
1934
- return (hasMultiParagraphs ? hasMultiParagraphs.length : 0);
2137
+ return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
1935
2138
  },
1936
2139
 
1937
2140
  checkSelectionElement: function (newSelection, selectionElement) {
@@ -1961,7 +2164,7 @@ function MediumEditor(elements, options) {
1961
2164
  if (this.options.standardizeSelectionStart &&
1962
2165
  this.selectionRange.startContainer.nodeValue &&
1963
2166
  (this.selectionRange.startOffset === this.selectionRange.startContainer.nodeValue.length)) {
1964
- adjacentNode = mediumEditorUtil.findAdjacentTextNodeWithContent(meSelection.getSelectionElement(this.options.contentWindow), this.selectionRange.startContainer, this.options.ownerDocument);
2167
+ adjacentNode = Util.findAdjacentTextNodeWithContent(Selection.getSelectionElement(this.options.contentWindow), this.selectionRange.startContainer, this.options.ownerDocument);
1965
2168
  if (adjacentNode) {
1966
2169
  offset = 0;
1967
2170
  while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
@@ -1978,9 +2181,7 @@ function MediumEditor(elements, options) {
1978
2181
 
1979
2182
  for (i = 0; i < this.elements.length; i += 1) {
1980
2183
  if (this.elements[i] === selectionElement) {
1981
- this.setToolbarButtonStates()
1982
- .setToolbarPosition()
1983
- .showToolbarActions();
2184
+ this.showAndUpdateToolbar();
1984
2185
  return;
1985
2186
  }
1986
2187
  }
@@ -1990,62 +2191,95 @@ function MediumEditor(elements, options) {
1990
2191
  }
1991
2192
  },
1992
2193
 
2194
+ showAndUpdateToolbar: function () {
2195
+ this.setToolbarButtonStates()
2196
+ .setToolbarPosition()
2197
+ .showToolbarDefaultActions();
2198
+ },
2199
+
1993
2200
  setToolbarPosition: function () {
1994
2201
  // document.documentElement for IE 9
1995
2202
  var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
1996
- container = this.elements[0],
1997
- containerRect = container.getBoundingClientRect(),
1998
- containerTop = containerRect.top + scrollTop,
1999
- buttonHeight = 50,
2000
2203
  selection = this.options.contentWindow.getSelection(),
2204
+ windowWidth = this.options.contentWindow.innerWidth,
2205
+ container = Selection.getSelectionElement(this.options.contentWindow),
2206
+ buttonHeight = 50,
2207
+ toolbarWidth,
2208
+ toolbarHeight,
2209
+ halfOffsetWidth,
2210
+ defaultLeft,
2211
+ containerRect,
2212
+ containerTop,
2213
+ containerCenter,
2001
2214
  range,
2002
2215
  boundary,
2003
2216
  middleBoundary,
2004
- defaultLeft = (this.options.diffLeft) - (this.toolbar.offsetWidth / 2),
2005
- halfOffsetWidth = this.toolbar.offsetWidth / 2,
2006
- containerCenter = (containerRect.left + (containerRect.width / 2));
2217
+ targetLeft;
2007
2218
 
2008
- if (selection.focusNode === null) {
2219
+ // If there isn't a valid selection, bail
2220
+ if (!container || !this.options.contentWindow.getSelection().focusNode) {
2009
2221
  return this;
2010
2222
  }
2011
2223
 
2012
- this.showToolbar();
2224
+ // If the container isn't part of this medium-editor instance, bail
2225
+ if (this.elements.indexOf(container) === -1) {
2226
+ return this;
2227
+ }
2228
+
2229
+ // Calculate container dimensions
2230
+ containerRect = container.getBoundingClientRect();
2231
+ containerTop = containerRect.top + scrollTop;
2232
+ containerCenter = (containerRect.left + (containerRect.width / 2));
2233
+
2234
+ // position the toolbar at left 0, so we can get the real width of the toolbar
2235
+ this.toolbar.style.left = '0';
2236
+ toolbarWidth = this.toolbar.offsetWidth;
2237
+ toolbarHeight = this.toolbar.offsetHeight;
2238
+ halfOffsetWidth = toolbarWidth / 2;
2239
+ defaultLeft = this.options.diffLeft - halfOffsetWidth;
2013
2240
 
2014
2241
  if (this.options.staticToolbar) {
2242
+ this.showToolbar();
2015
2243
 
2016
2244
  if (this.options.stickyToolbar) {
2017
-
2018
2245
  // If it's beyond the height of the editor, position it at the bottom of the editor
2019
- if (scrollTop > (containerTop + this.elements[0].offsetHeight - this.toolbar.offsetHeight)) {
2020
- this.toolbar.style.top = (containerTop + this.elements[0].offsetHeight) + 'px';
2246
+ if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) {
2247
+ this.toolbar.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
2248
+ this.toolbar.classList.remove('sticky-toolbar');
2021
2249
 
2022
2250
  // Stick the toolbar to the top of the window
2023
- } else if (scrollTop > (containerTop - this.toolbar.offsetHeight)) {
2251
+ } else if (scrollTop > (containerTop - toolbarHeight)) {
2024
2252
  this.toolbar.classList.add('sticky-toolbar');
2025
2253
  this.toolbar.style.top = "0px";
2254
+
2026
2255
  // Normal static toolbar position
2027
2256
  } else {
2028
2257
  this.toolbar.classList.remove('sticky-toolbar');
2029
- this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
2258
+ this.toolbar.style.top = containerTop - toolbarHeight + "px";
2030
2259
  }
2031
-
2032
2260
  } else {
2033
- this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
2261
+ this.toolbar.style.top = containerTop - toolbarHeight + "px";
2034
2262
  }
2035
2263
 
2036
- if (this.options.toolbarAlign) {
2037
- if (this.options.toolbarAlign === 'left') {
2038
- this.toolbar.style.left = containerRect.left + "px";
2039
- } else if (this.options.toolbarAlign === 'center') {
2040
- this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "px";
2041
- } else {
2042
- this.toolbar.style.left = (containerRect.right - this.toolbar.offsetWidth) + "px";
2043
- }
2044
- } else {
2045
- this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "px";
2264
+ if (this.options.toolbarAlign === 'left') {
2265
+ targetLeft = containerRect.left;
2266
+ } else if (this.options.toolbarAlign === 'center') {
2267
+ targetLeft = containerCenter - halfOffsetWidth;
2268
+ } else if (this.options.toolbarAlign === 'right') {
2269
+ targetLeft = containerRect.right - toolbarWidth;
2046
2270
  }
2047
2271
 
2272
+ if (targetLeft < 0) {
2273
+ targetLeft = 0;
2274
+ } else if ((targetLeft + toolbarWidth) > windowWidth) {
2275
+ targetLeft = windowWidth - toolbarWidth;
2276
+ }
2277
+
2278
+ this.toolbar.style.left = targetLeft + 'px';
2279
+
2048
2280
  } else if (!selection.isCollapsed) {
2281
+ this.showToolbar();
2282
+
2049
2283
  range = selection.getRangeAt(0);
2050
2284
  boundary = range.getBoundingClientRect();
2051
2285
  middleBoundary = (boundary.left + boundary.right) / 2;
@@ -2053,16 +2287,16 @@ function MediumEditor(elements, options) {
2053
2287
  if (boundary.top < buttonHeight) {
2054
2288
  this.toolbar.classList.add('medium-toolbar-arrow-over');
2055
2289
  this.toolbar.classList.remove('medium-toolbar-arrow-under');
2056
- this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
2290
+ this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
2057
2291
  } else {
2058
2292
  this.toolbar.classList.add('medium-toolbar-arrow-under');
2059
2293
  this.toolbar.classList.remove('medium-toolbar-arrow-over');
2060
- this.toolbar.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
2294
+ this.toolbar.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
2061
2295
  }
2062
2296
  if (middleBoundary < halfOffsetWidth) {
2063
2297
  this.toolbar.style.left = defaultLeft + halfOffsetWidth + 'px';
2064
- } else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
2065
- this.toolbar.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
2298
+ } else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
2299
+ this.toolbar.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px';
2066
2300
  } else {
2067
2301
  this.toolbar.style.left = defaultLeft + middleBoundary + 'px';
2068
2302
  }
@@ -2075,8 +2309,8 @@ function MediumEditor(elements, options) {
2075
2309
 
2076
2310
  setToolbarButtonStates: function () {
2077
2311
  this.commands.forEach(function (extension) {
2078
- if (typeof extension.deactivate === 'function') {
2079
- extension.deactivate();
2312
+ if (typeof extension.isActive === 'function') {
2313
+ extension.setInactive();
2080
2314
  }
2081
2315
  }.bind(this));
2082
2316
  this.checkActiveButtons();
@@ -2087,17 +2321,23 @@ function MediumEditor(elements, options) {
2087
2321
  var elements = Array.prototype.slice.call(this.elements),
2088
2322
  manualStateChecks = [],
2089
2323
  queryState = null,
2090
- parentNode = meSelection.getSelectedParentElement(this.selectionRange),
2324
+ parentNode,
2091
2325
  checkExtension = function (extension) {
2092
2326
  if (typeof extension.checkState === 'function') {
2093
2327
  extension.checkState(parentNode);
2094
- } else if (typeof extension.isActive === 'function') {
2095
- if (!extension.isActive() && extension.shouldActivate(parentNode)) {
2096
- extension.activate();
2328
+ } else if (typeof extension.isActive === 'function' &&
2329
+ typeof extension.isAlreadyApplied === 'function') {
2330
+ if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
2331
+ extension.setActive();
2097
2332
  }
2098
2333
  }
2099
2334
  };
2100
2335
 
2336
+ if (!this.selectionRange) {
2337
+ return;
2338
+ }
2339
+ parentNode = Selection.getSelectedParentElement(this.selectionRange);
2340
+
2101
2341
  // Loop through all commands
2102
2342
  this.commands.forEach(function (command) {
2103
2343
  // For those commands where we can use document.queryCommandState(), do so
@@ -2107,7 +2347,7 @@ function MediumEditor(elements, options) {
2107
2347
  // and don't need to do our manual checks
2108
2348
  if (queryState !== null) {
2109
2349
  if (queryState) {
2110
- command.activate();
2350
+ command.setActive();
2111
2351
  }
2112
2352
  return;
2113
2353
  }
@@ -2117,8 +2357,7 @@ function MediumEditor(elements, options) {
2117
2357
  });
2118
2358
 
2119
2359
  // Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node
2120
- while (parentNode.tagName !== undefined && mediumEditorUtil.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
2121
- this.activateButton(parentNode.tagName.toLowerCase());
2360
+ while (parentNode.tagName !== undefined && Util.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
2122
2361
  manualStateChecks.forEach(checkExtension.bind(this));
2123
2362
 
2124
2363
  // we can abort the search upwards if we leave the contentEditable element
@@ -2129,33 +2368,20 @@ function MediumEditor(elements, options) {
2129
2368
  }
2130
2369
  },
2131
2370
 
2132
- activateButton: function (tag) {
2133
- var el = this.toolbar.querySelector('[data-element="' + tag + '"]');
2134
- if (el !== null && !el.classList.contains(this.options.activeButtonClass)) {
2135
- el.classList.add(this.options.activeButtonClass);
2136
- }
2137
- },
2138
-
2139
- bindButtons: function () {
2140
- this.setFirstAndLastItems(this.toolbar.querySelectorAll('button'));
2141
- return this;
2142
- },
2143
-
2144
- setFirstAndLastItems: function (buttons) {
2371
+ setFirstAndLastButtons: function () {
2372
+ var buttons = this.toolbar.querySelectorAll('button');
2145
2373
  if (buttons.length > 0) {
2146
-
2147
2374
  buttons[0].className += ' ' + this.options.firstButtonClass;
2148
2375
  buttons[buttons.length - 1].className += ' ' + this.options.lastButtonClass;
2149
2376
  }
2150
2377
  return this;
2151
2378
  },
2152
2379
 
2153
- execAction: function (action, e) {
2380
+ execAction: function (action, opts) {
2154
2381
  /*jslint regexp: true*/
2155
2382
  var fullAction = /^full-(.+)$/gi,
2156
- appendAction = /^append-(.+)$/gi,
2157
- justifyAction = /^justify(left|center|right|full)$/gi,
2158
- match;
2383
+ match,
2384
+ result;
2159
2385
  /*jslint regexp: false*/
2160
2386
 
2161
2387
  // Actions starting with 'full-' should be applied to to the entire contents of the editable element
@@ -2166,57 +2392,39 @@ function MediumEditor(elements, options) {
2166
2392
  this.saveSelection();
2167
2393
  // Select all of the contents before calling the action
2168
2394
  this.selectAllContents();
2169
- this.execAction(match[1], e);
2395
+ result = this.execActionInternal(match[1], opts);
2170
2396
  // Restore the previous selection
2171
2397
  this.restoreSelection();
2172
- return;
2398
+ } else {
2399
+ result = this.execActionInternal(action, opts);
2173
2400
  }
2174
2401
 
2402
+ this.checkSelection();
2403
+ return result;
2404
+ },
2405
+
2406
+ execActionInternal: function (action, opts) {
2407
+ /*jslint regexp: true*/
2408
+ var appendAction = /^append-(.+)$/gi,
2409
+ match;
2410
+ /*jslint regexp: false*/
2411
+
2175
2412
  // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
2176
2413
  // type of block element (ie append-blockquote, append-h1, append-pre, etc.)
2177
2414
  match = appendAction.exec(action);
2178
2415
  if (match) {
2179
- this.execFormatBlock(match[1]);
2180
- this.setToolbarPosition();
2181
- this.setToolbarButtonStates();
2182
- return;
2416
+ return this.execFormatBlock(match[1]);
2183
2417
  }
2184
2418
 
2185
- if (action === 'anchor') {
2186
- if (!this.options.disableAnchorForm) {
2187
- this.triggerAnchorAction(e);
2188
- }
2189
- } else if (action === 'image') {
2190
- this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
2191
- } else {
2192
- this.options.ownerDocument.execCommand(action, false, null);
2193
- this.setToolbarPosition();
2194
- // Manually update the toolbar for text-alignment actions
2195
- if (justifyAction.test(action)) {
2196
- this.setToolbarButtonStates();
2197
- }
2419
+ if (action === 'createLink') {
2420
+ return this.createLink(opts);
2198
2421
  }
2199
- },
2200
2422
 
2201
- // Method to show an extension's form
2202
- // TO DO: Improve this
2203
- showForm: function (formId, e) {
2204
- this.toolbarActions.style.display = 'none';
2205
- this.saveSelection();
2206
- var form = document.getElementById(formId);
2207
- form.style.display = 'block';
2208
- this.setToolbarPosition();
2209
- this.keepToolbarAlive = true;
2210
- },
2423
+ if (action === 'image') {
2424
+ return this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
2425
+ }
2211
2426
 
2212
- // Method to show an extension's form
2213
- // TO DO: Improve this
2214
- hideForm: function (form, e) {
2215
- var el = document.getElementById(form.id);
2216
- el.style.display = 'none';
2217
- this.showToolbarActions();
2218
- this.setToolbarPosition();
2219
- this.restoreSelection();
2427
+ return this.options.ownerDocument.execCommand(action, false, null);
2220
2428
  },
2221
2429
 
2222
2430
  // TODO: move these two methods to selection.js
@@ -2241,23 +2449,8 @@ function MediumEditor(elements, options) {
2241
2449
  return selectedParentElement;
2242
2450
  },
2243
2451
 
2244
- triggerAnchorAction: function () {
2245
- var selectedParentElement = meSelection.getSelectedParentElement(this.selectionRange);
2246
- if (selectedParentElement.tagName &&
2247
- selectedParentElement.tagName.toLowerCase() === 'a') {
2248
- this.options.ownerDocument.execCommand('unlink', false, null);
2249
- } else if (this.anchorExtension) {
2250
- if (this.anchorExtension.isDisplayed()) {
2251
- this.showToolbarActions();
2252
- } else {
2253
- this.showAnchorForm();
2254
- }
2255
- }
2256
- return this;
2257
- },
2258
-
2259
2452
  execFormatBlock: function (el) {
2260
- var selectionData = meSelection.getSelectionData(this.selection.anchorNode);
2453
+ var selectionData = Selection.getSelectionData(this.selection.anchorNode);
2261
2454
  // FF handles blockquote differently on formatBlock
2262
2455
  // allowing nesting, we need to use outdent
2263
2456
  // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
@@ -2272,7 +2465,7 @@ function MediumEditor(elements, options) {
2272
2465
  // blockquote needs to be called as indent
2273
2466
  // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
2274
2467
  // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
2275
- if (mediumEditorUtil.isIE) {
2468
+ if (Util.isIE) {
2276
2469
  if (el === 'blockquote') {
2277
2470
  return this.options.ownerDocument.execCommand('indent', false, el);
2278
2471
  }
@@ -2281,6 +2474,47 @@ function MediumEditor(elements, options) {
2281
2474
  return this.options.ownerDocument.execCommand('formatBlock', false, el);
2282
2475
  },
2283
2476
 
2477
+ isToolbarDefaultActionsShown: function () {
2478
+ return !!this.toolbarActions && this.toolbarActions.style.display === 'block';
2479
+ },
2480
+
2481
+ hideToolbarDefaultActions: function () {
2482
+ if (this.toolbarActions && this.isToolbarDefaultActionsShown()) {
2483
+ this.commands.forEach(function (extension) {
2484
+ if (extension.onHide && typeof extension.onHide === 'function') {
2485
+ extension.onHide();
2486
+ }
2487
+ });
2488
+ this.toolbarActions.style.display = 'none';
2489
+ }
2490
+ },
2491
+
2492
+ showToolbarDefaultActions: function () {
2493
+ this.hideExtensionForms();
2494
+
2495
+ if (this.toolbarActions && !this.isToolbarDefaultActionsShown()) {
2496
+ this.toolbarActions.style.display = 'block';
2497
+ }
2498
+
2499
+ this.keepToolbarAlive = false;
2500
+ // Using setTimeout + options.delay because:
2501
+ // We will actually be displaying the toolbar, which should be controlled by options.delay
2502
+ this.delay(function () {
2503
+ this.showToolbar();
2504
+ }.bind(this));
2505
+
2506
+ return this;
2507
+ },
2508
+
2509
+ hideExtensionForms: function () {
2510
+ // Hide all extension forms
2511
+ this.commands.forEach(function (extension) {
2512
+ if (extension.hasForm && extension.isDisplayed()) {
2513
+ extension.hideForm();
2514
+ }
2515
+ });
2516
+ },
2517
+
2284
2518
  isToolbarShown: function () {
2285
2519
  return this.toolbar && this.toolbar.classList.contains('medium-editor-toolbar-active');
2286
2520
  },
@@ -2288,8 +2522,8 @@ function MediumEditor(elements, options) {
2288
2522
  showToolbar: function () {
2289
2523
  if (this.toolbar && !this.isToolbarShown()) {
2290
2524
  this.toolbar.classList.add('medium-editor-toolbar-active');
2291
- if (this.onShowToolbar) {
2292
- this.onShowToolbar();
2525
+ if (typeof this.options.onShowToolbar === 'function') {
2526
+ this.options.onShowToolbar();
2293
2527
  }
2294
2528
  }
2295
2529
  },
@@ -2297,9 +2531,8 @@ function MediumEditor(elements, options) {
2297
2531
  hideToolbar: function () {
2298
2532
  if (this.isToolbarShown()) {
2299
2533
  this.toolbar.classList.remove('medium-editor-toolbar-active');
2300
- // TODO: this should be an option?
2301
- if (this.onHideToolbar) {
2302
- this.onHideToolbar();
2534
+ if (typeof this.options.onHideToolbar === 'function') {
2535
+ this.options.onHideToolbar();
2303
2536
  }
2304
2537
  }
2305
2538
  },
@@ -2314,24 +2547,10 @@ function MediumEditor(elements, options) {
2314
2547
  this.hideToolbar();
2315
2548
  },
2316
2549
 
2317
- showToolbarActions: function () {
2318
- var self = this;
2319
- if (this.anchorExtension) {
2320
- this.anchorExtension.hideForm();
2321
- }
2322
- this.toolbarActions.style.display = 'block';
2323
- this.keepToolbarAlive = false;
2324
- // Using setTimeout + options.delay because:
2325
- // We will actually be displaying the toolbar, which should be controlled by options.delay
2326
- this.delay(function () {
2327
- self.showToolbar();
2328
- });
2329
- },
2330
-
2331
2550
  selectAllContents: function () {
2332
2551
  var range = this.options.ownerDocument.createRange(),
2333
2552
  sel = this.options.contentWindow.getSelection(),
2334
- currNode = meSelection.getSelectionElement(this.options.contentWindow);
2553
+ currNode = Selection.getSelectionElement(this.options.contentWindow);
2335
2554
 
2336
2555
  if (currNode) {
2337
2556
  // Move to the lowest descendant node that still selects all of the contents
@@ -2363,7 +2582,7 @@ function MediumEditor(elements, options) {
2363
2582
 
2364
2583
  // Find element current selection is inside
2365
2584
  this.elements.forEach(function (el, index) {
2366
- if (el === range.startContainer || mediumEditorUtil.isDescendant(el, range.startContainer)) {
2585
+ if (el === range.startContainer || Util.isDescendant(el, range.startContainer)) {
2367
2586
  editableElementIndex = index;
2368
2587
  return false;
2369
2588
  }
@@ -2435,19 +2654,6 @@ function MediumEditor(elements, options) {
2435
2654
  sel.addRange(range);
2436
2655
  },
2437
2656
 
2438
- showAnchorForm: function (link_value) {
2439
- if (!this.anchorExtension) {
2440
- return;
2441
- }
2442
-
2443
- this.toolbarActions.style.display = 'none';
2444
- this.saveSelection();
2445
- this.anchorExtension.showForm();
2446
- this.setToolbarPosition();
2447
- this.keepToolbarAlive = true;
2448
- this.anchorExtension.focus(link_value);
2449
- },
2450
-
2451
2657
  hideAnchorPreview: function () {
2452
2658
  this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
2453
2659
  },
@@ -2551,25 +2757,26 @@ function MediumEditor(elements, options) {
2551
2757
  '</div>';
2552
2758
  },
2553
2759
 
2554
- anchorPreviewClickHandler: function (e) {
2555
- if (!this.options.disableAnchorForm && this.activeAnchor) {
2760
+ anchorPreviewClickHandler: function (event) {
2761
+ var range,
2762
+ sel,
2763
+ anchorExtension = this.getExtensionByName('anchor');
2556
2764
 
2557
- var self = this,
2558
- range = this.options.ownerDocument.createRange(),
2559
- sel = this.options.contentWindow.getSelection();
2765
+ if (anchorExtension && this.activeAnchor) {
2766
+ range = this.options.ownerDocument.createRange();
2767
+ range.selectNodeContents(this.activeAnchor);
2560
2768
 
2561
- range.selectNodeContents(self.activeAnchor);
2769
+ sel = this.options.contentWindow.getSelection();
2562
2770
  sel.removeAllRanges();
2563
2771
  sel.addRange(range);
2564
2772
  // Using setTimeout + options.delay because:
2565
2773
  // We may actually be displaying the anchor form, which should be controlled by options.delay
2566
2774
  this.delay(function () {
2567
- if (self.activeAnchor) {
2568
- self.showAnchorForm(self.activeAnchor.attributes.href.value);
2775
+ if (this.activeAnchor) {
2776
+ anchorExtension.showForm(this.activeAnchor.attributes.href.value);
2569
2777
  }
2570
- self.keepToolbarAlive = false;
2571
- });
2572
-
2778
+ this.keepToolbarAlive = false;
2779
+ }.bind(this));
2573
2780
  }
2574
2781
 
2575
2782
  this.hideAnchorPreview();
@@ -2622,13 +2829,33 @@ function MediumEditor(elements, options) {
2622
2829
  return this;
2623
2830
  },
2624
2831
 
2625
- checkLinkFormat: function (value) {
2626
- var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
2627
- return (re.test(value) ? '' : 'http://') + value;
2832
+ createLink: function (opts) {
2833
+ var customEvent,
2834
+ i;
2835
+
2836
+ if (opts.url && opts.url.trim().length > 0) {
2837
+ this.options.ownerDocument.execCommand('createLink', false, opts.url);
2838
+
2839
+ if (this.options.targetBlank || opts.target === '_blank') {
2840
+ Util.setTargetBlank(Selection.getSelectionStart(this.options.ownerDocument));
2841
+ }
2842
+
2843
+ if (opts.buttonClass) {
2844
+ this.setButtonClass(opts.buttonClass);
2845
+ }
2846
+ }
2847
+
2848
+ if (this.options.targetBlank || opts.target === "_blank" || opts.buttonClass) {
2849
+ customEvent = this.options.ownerDocument.createEvent("HTMLEvents");
2850
+ customEvent.initEvent("input", true, true, this.options.contentWindow);
2851
+ for (i = 0; i < this.elements.length; i += 1) {
2852
+ this.elements[i].dispatchEvent(customEvent);
2853
+ }
2854
+ }
2628
2855
  },
2629
2856
 
2630
2857
  setButtonClass: function (buttonClass) {
2631
- var el = meSelection.getSelectionStart(this.options.ownerDocument),
2858
+ var el = Selection.getSelectionStart(this.options.ownerDocument),
2632
2859
  classes = buttonClass.split(' '),
2633
2860
  i,
2634
2861
  j;
@@ -2646,48 +2873,6 @@ function MediumEditor(elements, options) {
2646
2873
  }
2647
2874
  },
2648
2875
 
2649
- createLink: function (input, target, buttonClass) {
2650
-
2651
- var i, event;
2652
-
2653
- this.createLinkInternal(input.value, target, buttonClass);
2654
-
2655
- if (this.options.targetBlank || target === "_blank" || buttonClass) {
2656
- event = this.options.ownerDocument.createEvent("HTMLEvents");
2657
- event.initEvent("input", true, true, this.options.contentWindow);
2658
- for (i = 0; i < this.elements.length; i += 1) {
2659
- this.elements[i].dispatchEvent(event);
2660
- }
2661
- }
2662
-
2663
- this.checkSelection();
2664
- this.showToolbarActions();
2665
- input.value = '';
2666
- },
2667
-
2668
- createLinkInternal: function (url, target, buttonClass) {
2669
- if (!url || url.trim().length === 0) {
2670
- this.hideToolbarActions();
2671
- return;
2672
- }
2673
-
2674
- this.restoreSelection();
2675
-
2676
- if (this.options.checkLinkFormat) {
2677
- url = this.checkLinkFormat(url);
2678
- }
2679
-
2680
- this.options.ownerDocument.execCommand('createLink', false, url);
2681
-
2682
- if (this.options.targetBlank || target === "_blank") {
2683
- mediumEditorUtil.setTargetBlank(meSelection.getSelectionStart(this.options.ownerDocument));
2684
- }
2685
-
2686
- if (buttonClass) {
2687
- this.setButtonClass(buttonClass);
2688
- }
2689
- },
2690
-
2691
2876
  positionToolbarIfShown: function () {
2692
2877
  if (this.isToolbarShown()) {
2693
2878
  this.setToolbarPosition();
@@ -2748,10 +2933,6 @@ function MediumEditor(elements, options) {
2748
2933
  }
2749
2934
  }.bind(this));
2750
2935
 
2751
- if (this.anchorExtension) {
2752
- this.anchorExtension.deactivate();
2753
- }
2754
-
2755
2936
  this.removeAllEvents();
2756
2937
  },
2757
2938