medium-editor-rails 1.4.5 → 2.0.0

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