medium-editor-rails 2.0.7 → 2.0.9

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: d8fb5c49e21f1b03042ba8ad59518b87b3cf192d
4
- data.tar.gz: 7ab4220ad510d1fb71fbbb4701a8cc9f0bdad1e7
3
+ metadata.gz: f7001b46bce6793197c44e01b68a684b5e9cae6d
4
+ data.tar.gz: e3a755b3437e0d9fbdce90b8302446a68703b7f8
5
5
  SHA512:
6
- metadata.gz: 2a23c6ac2a76c3d10ca7618f1a9c5497c46e824c0b2f2a267ae615402aeeb135ed9faeac8e46fcb272e1dbdac84060a734e04ca73bf4c21166e52debf385e14c
7
- data.tar.gz: ff5df8ce9a1824510b46b8598960b8088c82d1c180f16859e17a12509039c68492d985267cd3d0937f0abbc5c10b677a4b47badee2ced3a97d5724e71f19f864
6
+ metadata.gz: 08250fccdf9ee1504c27583e385378ae7ad69ce9409b7a06a7ca7400040a24740401621126b23ddd5853444304627d7290b3b4a006dffbd24e148231622059bc
7
+ data.tar.gz: c26a53eb8a399c26dcaf772cedb9af2b3391155c450100e592a9905f3a0730faf99be895c57e329fc2dfb104da3a4bc219aeee0aeee967e262206ab32c8b8e6d
@@ -1,5 +1,12 @@
1
1
 
2
2
  #### [Current]
3
+ * [df257c8](../../commit/df257c8) - __(Ahmet Sezgin Duran)__ Update Medium Editor files
4
+ * [3a3a5c1](../../commit/3a3a5c1) - __(Ahmet Sezgin Duran)__ Merge tag '2.0.7' into develop
5
+
6
+ 2.0.7
7
+
8
+ #### 2.0.7
9
+ * [a7b71c7](../../commit/a7b71c7) - __(Ahmet Sezgin Duran)__ Bump versions 2.0.7 and 3.0.7
3
10
  * [8efdb52](../../commit/8efdb52) - __(Ahmet Sezgin Duran)__ Update Medium Editor files
4
11
  * [b838798](../../commit/b838798) - __(Ahmet Sezgin Duran)__ Merge tag '2.0.0' 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 [3.0.7](https://github.com/daviferreira/medium-editor/releases)
11
+ The latest version of Medium Editor bundled by this gem is [3.0.9](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 = '2.0.7'
4
- MEDIUM_EDITOR_VERSION = '3.0.7'
3
+ VERSION = '2.0.9'
4
+ MEDIUM_EDITOR_VERSION = '3.0.9'
5
5
  end
6
6
  end
@@ -171,6 +171,218 @@ if (!("classList" in document.createElement("_"))) {
171
171
  }(self));
172
172
  }
173
173
 
174
+ /* Blob.js
175
+ * A Blob implementation.
176
+ * 2014-07-24
177
+ *
178
+ * By Eli Grey, http://eligrey.com
179
+ * By Devin Samarin, https://github.com/dsamarin
180
+ * License: X11/MIT
181
+ * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
182
+ */
183
+
184
+ /*global self, unescape */
185
+ /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
186
+ plusplus: true */
187
+
188
+ /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
189
+
190
+ (function (view) {
191
+ "use strict";
192
+
193
+ view.URL = view.URL || view.webkitURL;
194
+
195
+ if (view.Blob && view.URL) {
196
+ try {
197
+ new Blob;
198
+ return;
199
+ } catch (e) {}
200
+ }
201
+
202
+ // Internally we use a BlobBuilder implementation to base Blob off of
203
+ // in order to support older browsers that only have BlobBuilder
204
+ var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
205
+ var
206
+ get_class = function(object) {
207
+ return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
208
+ }
209
+ , FakeBlobBuilder = function BlobBuilder() {
210
+ this.data = [];
211
+ }
212
+ , FakeBlob = function Blob(data, type, encoding) {
213
+ this.data = data;
214
+ this.size = data.length;
215
+ this.type = type;
216
+ this.encoding = encoding;
217
+ }
218
+ , FBB_proto = FakeBlobBuilder.prototype
219
+ , FB_proto = FakeBlob.prototype
220
+ , FileReaderSync = view.FileReaderSync
221
+ , FileException = function(type) {
222
+ this.code = this[this.name = type];
223
+ }
224
+ , file_ex_codes = (
225
+ "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
226
+ + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
227
+ ).split(" ")
228
+ , file_ex_code = file_ex_codes.length
229
+ , real_URL = view.URL || view.webkitURL || view
230
+ , real_create_object_URL = real_URL.createObjectURL
231
+ , real_revoke_object_URL = real_URL.revokeObjectURL
232
+ , URL = real_URL
233
+ , btoa = view.btoa
234
+ , atob = view.atob
235
+
236
+ , ArrayBuffer = view.ArrayBuffer
237
+ , Uint8Array = view.Uint8Array
238
+
239
+ , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
240
+ ;
241
+ FakeBlob.fake = FB_proto.fake = true;
242
+ while (file_ex_code--) {
243
+ FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
244
+ }
245
+ // Polyfill URL
246
+ if (!real_URL.createObjectURL) {
247
+ URL = view.URL = function(uri) {
248
+ var
249
+ uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
250
+ , uri_origin
251
+ ;
252
+ uri_info.href = uri;
253
+ if (!("origin" in uri_info)) {
254
+ if (uri_info.protocol.toLowerCase() === "data:") {
255
+ uri_info.origin = null;
256
+ } else {
257
+ uri_origin = uri.match(origin);
258
+ uri_info.origin = uri_origin && uri_origin[1];
259
+ }
260
+ }
261
+ return uri_info;
262
+ };
263
+ }
264
+ URL.createObjectURL = function(blob) {
265
+ var
266
+ type = blob.type
267
+ , data_URI_header
268
+ ;
269
+ if (type === null) {
270
+ type = "application/octet-stream";
271
+ }
272
+ if (blob instanceof FakeBlob) {
273
+ data_URI_header = "data:" + type;
274
+ if (blob.encoding === "base64") {
275
+ return data_URI_header + ";base64," + blob.data;
276
+ } else if (blob.encoding === "URI") {
277
+ return data_URI_header + "," + decodeURIComponent(blob.data);
278
+ } if (btoa) {
279
+ return data_URI_header + ";base64," + btoa(blob.data);
280
+ } else {
281
+ return data_URI_header + "," + encodeURIComponent(blob.data);
282
+ }
283
+ } else if (real_create_object_URL) {
284
+ return real_create_object_URL.call(real_URL, blob);
285
+ }
286
+ };
287
+ URL.revokeObjectURL = function(object_URL) {
288
+ if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
289
+ real_revoke_object_URL.call(real_URL, object_URL);
290
+ }
291
+ };
292
+ FBB_proto.append = function(data/*, endings*/) {
293
+ var bb = this.data;
294
+ // decode data to a binary string
295
+ if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
296
+ var
297
+ str = ""
298
+ , buf = new Uint8Array(data)
299
+ , i = 0
300
+ , buf_len = buf.length
301
+ ;
302
+ for (; i < buf_len; i++) {
303
+ str += String.fromCharCode(buf[i]);
304
+ }
305
+ bb.push(str);
306
+ } else if (get_class(data) === "Blob" || get_class(data) === "File") {
307
+ if (FileReaderSync) {
308
+ var fr = new FileReaderSync;
309
+ bb.push(fr.readAsBinaryString(data));
310
+ } else {
311
+ // async FileReader won't work as BlobBuilder is sync
312
+ throw new FileException("NOT_READABLE_ERR");
313
+ }
314
+ } else if (data instanceof FakeBlob) {
315
+ if (data.encoding === "base64" && atob) {
316
+ bb.push(atob(data.data));
317
+ } else if (data.encoding === "URI") {
318
+ bb.push(decodeURIComponent(data.data));
319
+ } else if (data.encoding === "raw") {
320
+ bb.push(data.data);
321
+ }
322
+ } else {
323
+ if (typeof data !== "string") {
324
+ data += ""; // convert unsupported types to strings
325
+ }
326
+ // decode UTF-16 to binary string
327
+ bb.push(unescape(encodeURIComponent(data)));
328
+ }
329
+ };
330
+ FBB_proto.getBlob = function(type) {
331
+ if (!arguments.length) {
332
+ type = null;
333
+ }
334
+ return new FakeBlob(this.data.join(""), type, "raw");
335
+ };
336
+ FBB_proto.toString = function() {
337
+ return "[object BlobBuilder]";
338
+ };
339
+ FB_proto.slice = function(start, end, type) {
340
+ var args = arguments.length;
341
+ if (args < 3) {
342
+ type = null;
343
+ }
344
+ return new FakeBlob(
345
+ this.data.slice(start, args > 1 ? end : this.data.length)
346
+ , type
347
+ , this.encoding
348
+ );
349
+ };
350
+ FB_proto.toString = function() {
351
+ return "[object Blob]";
352
+ };
353
+ FB_proto.close = function() {
354
+ this.size = 0;
355
+ delete this.data;
356
+ };
357
+ return FakeBlobBuilder;
358
+ }(view));
359
+
360
+ view.Blob = function(blobParts, options) {
361
+ var type = options ? (options.type || "") : "";
362
+ var builder = new BlobBuilder();
363
+ if (blobParts) {
364
+ for (var i = 0, len = blobParts.length; i < len; i++) {
365
+ if (Uint8Array && blobParts[i] instanceof Uint8Array) {
366
+ builder.append(blobParts[i].buffer);
367
+ }
368
+ else {
369
+ builder.append(blobParts[i]);
370
+ }
371
+ }
372
+ }
373
+ var blob = builder.getBlob(type);
374
+ if (!blob.slice && blob.webkitSlice) {
375
+ blob.slice = blob.webkitSlice;
376
+ }
377
+ return blob;
378
+ };
379
+
380
+ var getPrototypeOf = Object.getPrototypeOf || function(object) {
381
+ return object.__proto__;
382
+ };
383
+ view.Blob.prototype = getPrototypeOf(new view.Blob());
384
+ }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
385
+
174
386
  (function (root, factory) {
175
387
  'use strict';
176
388
  if (typeof module === 'object') {
@@ -224,10 +436,6 @@ var Util;
224
436
  return copyInto(dest, source);
225
437
  },
226
438
 
227
- extend: function extend(dest, source) {
228
- return copyInto(dest, source, true);
229
- },
230
-
231
439
  derives: function derives(base, derived) {
232
440
  var origPrototype = derived.prototype;
233
441
  function Proto() { }
@@ -361,7 +569,7 @@ var Util;
361
569
 
362
570
  // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
363
571
  insertHTMLCommand: function (doc, html) {
364
- var selection, range, el, fragment, node, lastNode;
572
+ var selection, range, el, fragment, node, lastNode, toReplace;
365
573
 
366
574
  if (doc.queryCommandSupported('insertHTML')) {
367
575
  try {
@@ -369,9 +577,21 @@ var Util;
369
577
  } catch (ignore) {}
370
578
  }
371
579
 
372
- selection = window.getSelection();
580
+ selection = doc.defaultView.getSelection();
373
581
  if (selection.getRangeAt && selection.rangeCount) {
374
582
  range = selection.getRangeAt(0);
583
+ toReplace = range.commonAncestorContainer;
584
+ // Ensure range covers maximum amount of nodes as possible
585
+ // By moving up the DOM and selecting ancestors whose only child is the range
586
+ if ((toReplace.nodeType === 3 && toReplace.nodeValue === range.toString()) ||
587
+ (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {
588
+ while (toReplace.parentNode &&
589
+ toReplace.parentNode.childNodes.length === 1 &&
590
+ !toReplace.parentNode.getAttribute('data-medium-element')) {
591
+ toReplace = toReplace.parentNode;
592
+ }
593
+ range.selectNode(toReplace);
594
+ }
375
595
  range.deleteContents();
376
596
 
377
597
  el = doc.createElement("div");
@@ -471,22 +691,15 @@ var Selection;
471
691
  getSelectionHtml: function getSelectionHtml() {
472
692
  var i,
473
693
  html = '',
474
- sel,
694
+ sel = this.options.contentWindow.getSelection(),
475
695
  len,
476
696
  container;
477
- if (this.options.contentWindow.getSelection !== undefined) {
478
- sel = this.options.contentWindow.getSelection();
479
- if (sel.rangeCount) {
480
- container = this.options.ownerDocument.createElement('div');
481
- for (i = 0, len = sel.rangeCount; i < len; i += 1) {
482
- container.appendChild(sel.getRangeAt(i).cloneContents());
483
- }
484
- html = container.innerHTML;
485
- }
486
- } else if (this.options.ownerDocument.selection !== undefined) {
487
- if (this.options.ownerDocument.selection.type === 'Text') {
488
- html = this.options.ownerDocument.selection.createRange().htmlText;
697
+ if (sel.rangeCount) {
698
+ container = this.options.ownerDocument.createElement('div');
699
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
700
+ container.appendChild(sel.getRangeAt(i).cloneContents());
489
701
  }
702
+ html = container.innerHTML;
490
703
  }
491
704
  return html;
492
705
  },
@@ -873,7 +1086,13 @@ var DefaultButton,
873
1086
  computedStyle = this.base.options.contentWindow.getComputedStyle(node, null).getPropertyValue(this.options.style.prop);
874
1087
  styleVals.forEach(function (val) {
875
1088
  if (!this.knownState) {
876
- this.knownState = isMatch = (computedStyle.indexOf(val) !== -1);
1089
+ isMatch = (computedStyle.indexOf(val) !== -1);
1090
+ // text-decoration is not inherited by default
1091
+ // so if the computed style for text-decoration doesn't match
1092
+ // don't write to knownState so we can fallback to other checks
1093
+ if (isMatch || this.options.style.prop !== 'text-decoration') {
1094
+ this.knownState = isMatch;
1095
+ }
877
1096
  }
878
1097
  }.bind(this));
879
1098
  }
@@ -1050,15 +1269,14 @@ var pasteHandler;
1050
1269
  filterLineBreak: function (el) {
1051
1270
  if (this.isCommonBlock(el.previousElementSibling)) {
1052
1271
  // remove stray br's following common block elements
1053
- el.parentNode.removeChild(el);
1272
+ this.removeWithParent(el);
1054
1273
  } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
1055
1274
  // remove br's just inside open or close tags of a div/p
1056
- el.parentNode.removeChild(el);
1275
+ this.removeWithParent(el);
1057
1276
  } else if (el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
1058
- // and br's that are the only child of a div/p
1277
+ // and br's that are the only child of elements other than div/p
1059
1278
  this.removeWithParent(el);
1060
1279
  }
1061
-
1062
1280
  },
1063
1281
 
1064
1282
  // remove an element, including its parent, if it is the only element within its parent
@@ -1067,7 +1285,7 @@ var pasteHandler;
1067
1285
  if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
1068
1286
  el.parentNode.parentNode.removeChild(el.parentNode);
1069
1287
  } else {
1070
- el.parentNode.removeChild(el.parentNode);
1288
+ el.parentNode.removeChild(el);
1071
1289
  }
1072
1290
  }
1073
1291
  },
@@ -1162,10 +1380,10 @@ var AnchorExtension;
1162
1380
 
1163
1381
  // Called by medium-editor to append form to the toolbar
1164
1382
  getForm: function () {
1165
- if (!this.anchorForm) {
1166
- this.anchorForm = this.createForm();
1383
+ if (!this.form) {
1384
+ this.form = this.createForm();
1167
1385
  }
1168
- return this.anchorForm;
1386
+ return this.form;
1169
1387
  },
1170
1388
 
1171
1389
  // Used by medium-editor when the default toolbar is to be displayed
@@ -1185,7 +1403,6 @@ var AnchorExtension;
1185
1403
  this.base.hideToolbarDefaultActions();
1186
1404
  this.getForm().style.display = 'block';
1187
1405
  this.base.setToolbarPosition();
1188
- this.base.keepToolbarAlive = true;
1189
1406
 
1190
1407
  input.value = link_value || '';
1191
1408
  input.focus();
@@ -1193,20 +1410,20 @@ var AnchorExtension;
1193
1410
 
1194
1411
  // Called by core when tearing down medium-editor (deactivate)
1195
1412
  deactivate: function () {
1196
- if (!this.anchorForm) {
1413
+ if (!this.form) {
1197
1414
  return false;
1198
1415
  }
1199
1416
 
1200
- if (this.anchorForm.parentNode) {
1201
- this.anchorForm.parentNode.removeChild(this.anchorForm);
1417
+ if (this.form.parentNode) {
1418
+ this.form.parentNode.removeChild(this.form);
1202
1419
  }
1203
1420
 
1204
- delete this.anchorForm;
1421
+ delete this.form;
1205
1422
  },
1206
1423
 
1207
1424
  // core methods
1208
1425
 
1209
- doLinkCreation: function () {
1426
+ doFormSave: function () {
1210
1427
  var targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'),
1211
1428
  buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button'),
1212
1429
  opts = {
@@ -1230,7 +1447,6 @@ var AnchorExtension;
1230
1447
  }
1231
1448
 
1232
1449
  this.base.createLink(opts);
1233
- this.base.keepToolbarAlive = false;
1234
1450
  this.base.checkSelection();
1235
1451
  },
1236
1452
 
@@ -1241,7 +1457,6 @@ var AnchorExtension;
1241
1457
 
1242
1458
  doFormCancel: function () {
1243
1459
  this.base.restoreSelection();
1244
- this.base.keepToolbarAlive = false;
1245
1460
  this.base.checkSelection();
1246
1461
  },
1247
1462
 
@@ -1274,9 +1489,6 @@ var AnchorExtension;
1274
1489
  // Handle typing in the textbox
1275
1490
  this.base.on(input, 'keyup', this.handleTextboxKeyup.bind(this));
1276
1491
 
1277
- // Handle clicks into the textbox
1278
- this.base.on(input, 'click', this.handleFormClick.bind(this));
1279
-
1280
1492
  // Add save buton
1281
1493
  save.setAttribute('href', '#');
1282
1494
  save.className = 'medium-editor-toobar-save';
@@ -1325,10 +1537,6 @@ var AnchorExtension;
1325
1537
  form.appendChild(button_label);
1326
1538
  }
1327
1539
 
1328
- // Handle click (capture) & focus (capture) outside of the form
1329
- this.base.on(doc.body, 'click', this.handleOutsideInteraction.bind(this), true);
1330
- this.base.on(doc.body, 'focus', this.handleOutsideInteraction.bind(this), true);
1331
-
1332
1540
  return form;
1333
1541
  },
1334
1542
 
@@ -1336,20 +1544,11 @@ var AnchorExtension;
1336
1544
  return this.getForm().querySelector('input.medium-editor-toolbar-input');
1337
1545
  },
1338
1546
 
1339
- handleOutsideInteraction: function (event) {
1340
- if (event.target !== this.getForm() &&
1341
- !Util.isDescendant(this.getForm(), event.target) &&
1342
- !Util.isDescendant(this.base.toolbarActions, event.target)) {
1343
- this.base.keepToolbarAlive = false;
1344
- this.base.checkSelection();
1345
- }
1346
- },
1347
-
1348
1547
  handleTextboxKeyup: function (event) {
1349
1548
  // For ENTER -> create the anchor
1350
1549
  if (event.keyCode === Util.keyCode.ENTER) {
1351
1550
  event.preventDefault();
1352
- this.doLinkCreation();
1551
+ this.doFormSave();
1353
1552
  return;
1354
1553
  }
1355
1554
 
@@ -1363,13 +1562,12 @@ var AnchorExtension;
1363
1562
  handleFormClick: function (event) {
1364
1563
  // make sure not to hide form when clicking inside the form
1365
1564
  event.stopPropagation();
1366
- this.base.keepToolbarAlive = true;
1367
1565
  },
1368
1566
 
1369
1567
  handleSaveClick: function (event) {
1370
1568
  // Clicking Save -> create the anchor
1371
1569
  event.preventDefault();
1372
- this.doLinkCreation();
1570
+ this.doFormSave();
1373
1571
  },
1374
1572
 
1375
1573
  handleCloseClick: function (event) {
@@ -1382,164 +1580,925 @@ var AnchorExtension;
1382
1580
  AnchorExtension = Util.derives(DefaultButton, AnchorDerived);
1383
1581
  }(window, document));
1384
1582
 
1385
- function MediumEditor(elements, options) {
1386
- 'use strict';
1387
- return this.init(elements, options);
1388
- }
1583
+ var AnchorPreview;
1389
1584
 
1390
- (function () {
1585
+ (function (window, document) {
1391
1586
  'use strict';
1392
1587
 
1393
- MediumEditor.statics = {
1394
- ButtonsData: ButtonsData,
1395
- DefaultButton: DefaultButton,
1396
- AnchorExtension: AnchorExtension
1588
+ AnchorPreview = function () {
1589
+ this.parent = true;
1590
+ this.name = 'anchor-preview';
1397
1591
  };
1398
1592
 
1399
- MediumEditor.prototype = {
1400
- defaults: {
1401
- allowMultiParagraphSelection: true,
1402
- anchorInputPlaceholder: 'Paste or type a link',
1403
- anchorInputCheckboxLabel: 'Open in new window',
1404
- anchorPreviewHideDelay: 500,
1405
- buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
1406
- buttonLabels: false,
1407
- checkLinkFormat: false,
1408
- cleanPastedHTML: false,
1409
- delay: 0,
1410
- diffLeft: 0,
1411
- diffTop: -10,
1412
- disableReturn: false,
1413
- disableDoubleReturn: false,
1414
- disableToolbar: false,
1415
- disableEditing: false,
1416
- disablePlaceholders: false,
1417
- toolbarAlign: 'center',
1418
- elementsContainer: false,
1419
- imageDragging: true,
1420
- standardizeSelectionStart: false,
1421
- contentWindow: window,
1422
- ownerDocument: document,
1423
- firstHeader: 'h3',
1424
- forcePlainText: true,
1425
- placeholder: 'Type your text',
1426
- secondHeader: 'h4',
1427
- targetBlank: false,
1428
- anchorTarget: false,
1429
- anchorButton: false,
1430
- anchorButtonClass: 'btn',
1431
- extensions: {},
1432
- activeButtonClass: 'medium-editor-button-active',
1433
- firstButtonClass: 'medium-editor-button-first',
1434
- lastButtonClass: 'medium-editor-button-last'
1593
+ AnchorPreview.prototype = {
1594
+
1595
+ init: function (instance) {
1596
+ this.base = instance;
1597
+ this.anchorPreview = this.createPreview();
1598
+ this.base.options.elementsContainer.appendChild(this.anchorPreview);
1599
+
1600
+ this.attachToEditables();
1435
1601
  },
1436
1602
 
1437
- init: function (elements, options) {
1438
- var uniqueId = 1;
1603
+ getPreviewElement: function () {
1604
+ return this.anchorPreview;
1605
+ },
1439
1606
 
1440
- this.options = Util.defaults(options, this.defaults);
1441
- this.setElementSelection(elements);
1442
- if (this.elements.length === 0) {
1443
- return;
1607
+ createPreview: function () {
1608
+ var el = this.base.options.ownerDocument.createElement('div');
1609
+
1610
+ el.id = 'medium-editor-anchor-preview-' + this.base.id;
1611
+ el.className = 'medium-editor-anchor-preview';
1612
+ el.innerHTML = this.getTemplate();
1613
+
1614
+ this.base.on(el, 'click', this.handleClick.bind(this));
1615
+
1616
+ return el;
1617
+ },
1618
+
1619
+ getTemplate: function () {
1620
+ return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
1621
+ ' <i class="medium-editor-toolbar-anchor-preview-inner"></i>' +
1622
+ '</div>';
1623
+ },
1624
+
1625
+ deactivate: function () {
1626
+ if (this.anchorPreview) {
1627
+ if (this.anchorPreview.parentNode) {
1628
+ this.anchorPreview.parentNode.removeChild(this.anchorPreview);
1629
+ }
1630
+ delete this.anchorPreview;
1444
1631
  }
1632
+ },
1445
1633
 
1446
- if (!this.options.elementsContainer) {
1447
- this.options.elementsContainer = this.options.ownerDocument.body;
1634
+ hidePreview: function () {
1635
+ this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
1636
+ this.activeAnchor = null;
1637
+ },
1638
+
1639
+ showPreview: function (anchorEl) {
1640
+ if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')
1641
+ || anchorEl.getAttribute('data-disable-preview')) {
1642
+ return true;
1448
1643
  }
1449
1644
 
1450
- while (this.options.elementsContainer.querySelector('#medium-editor-toolbar-' + uniqueId)) {
1451
- uniqueId = uniqueId + 1;
1645
+ this.anchorPreview.querySelector('i').textContent = anchorEl.attributes.href.value;
1646
+
1647
+ this.anchorPreview.classList.add('medium-toolbar-arrow-over');
1648
+ this.anchorPreview.classList.remove('medium-toolbar-arrow-under');
1649
+
1650
+ if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
1651
+ this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
1452
1652
  }
1453
1653
 
1454
- this.id = uniqueId;
1654
+ this.activeAnchor = anchorEl;
1455
1655
 
1456
- return this.setup();
1656
+ this.positionPreview();
1657
+ this.attachPreviewHandlers();
1658
+
1659
+ return this;
1457
1660
  },
1458
1661
 
1459
- setup: function () {
1460
- this.events = [];
1461
- this.isActive = true;
1462
- this.initThrottledMethods()
1463
- .initCommands()
1464
- .initElements()
1465
- .bindSelect()
1466
- .bindDragDrop()
1467
- .bindPaste()
1468
- .setPlaceholders()
1469
- .bindElementActions()
1470
- .bindWindowActions();
1662
+ positionPreview: function () {
1663
+ var buttonHeight = 40,
1664
+ boundary = this.activeAnchor.getBoundingClientRect(),
1665
+ middleBoundary = (boundary.left + boundary.right) / 2,
1666
+ halfOffsetWidth,
1667
+ defaultLeft;
1668
+
1669
+ halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
1670
+ defaultLeft = this.base.options.diffLeft - halfOffsetWidth;
1671
+
1672
+ this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - this.base.options.diffTop + this.base.options.contentWindow.pageYOffset - this.anchorPreview.offsetHeight) + 'px';
1673
+ if (middleBoundary < halfOffsetWidth) {
1674
+ this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
1675
+ } else if ((this.base.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
1676
+ this.anchorPreview.style.left = this.base.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
1677
+ } else {
1678
+ this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
1679
+ }
1471
1680
  },
1472
1681
 
1473
- on: function (target, event, listener, useCapture) {
1474
- target.addEventListener(event, listener, useCapture);
1475
- this.events.push([target, event, listener, useCapture]);
1682
+ attachToEditables: function () {
1683
+ this.base.elements.forEach(function (element) {
1684
+ this.base.on(element, 'mouseover', this.handleEditableMouseover.bind(this));
1685
+ }.bind(this));
1476
1686
  },
1477
1687
 
1478
- off: function (target, event, listener, useCapture) {
1479
- var index = this.indexOfListener(target, event, listener, useCapture),
1480
- e;
1481
- if (index !== -1) {
1482
- e = this.events.splice(index, 1)[0];
1483
- e[0].removeEventListener(e[1], e[2], e[3]);
1688
+ handleClick: function (event) {
1689
+ var range,
1690
+ sel,
1691
+ anchorExtension = this.base.getExtensionByName('anchor'),
1692
+ activeAnchor = this.activeAnchor;
1693
+
1694
+ if (anchorExtension && activeAnchor) {
1695
+ range = this.base.options.ownerDocument.createRange();
1696
+ range.selectNodeContents(this.activeAnchor);
1697
+
1698
+ sel = this.base.options.contentWindow.getSelection();
1699
+ sel.removeAllRanges();
1700
+ sel.addRange(range);
1701
+ // Using setTimeout + options.delay because:
1702
+ // We may actually be displaying the anchor form, which should be controlled by options.delay
1703
+ this.base.delay(function () {
1704
+ if (activeAnchor) {
1705
+ anchorExtension.showForm(activeAnchor.attributes.href.value);
1706
+ activeAnchor = null;
1707
+ }
1708
+ }.bind(this));
1484
1709
  }
1710
+
1711
+ this.hidePreview();
1485
1712
  },
1486
1713
 
1487
- indexOfListener: function (target, event, listener, useCapture) {
1488
- var i, n, item;
1489
- for (i = 0, n = this.events.length; i < n; i = i + 1) {
1490
- item = this.events[i];
1491
- if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
1492
- return i;
1714
+ handleAnchorMouseout: function (event) {
1715
+ this.anchorToPreview = null;
1716
+ this.base.off(this.activeAnchor, 'mouseout', this.instance_handleAnchorMouseout);
1717
+ this.instance_handleAnchorMouseout = null;
1718
+ },
1719
+
1720
+ handleEditableMouseover: function (event) {
1721
+ /*var overAnchor = true,
1722
+ leaveAnchor = function () {
1723
+ // mark the anchor as no longer hovered, and stop listening
1724
+ overAnchor = false;
1725
+ this.base.off(this.activeAnchor, 'mouseout', leaveAnchor);
1726
+ }.bind(this);*/
1727
+
1728
+ if (event.target && event.target.tagName.toLowerCase() === 'a') {
1729
+
1730
+ // Detect empty href attributes
1731
+ // The browser will make href="" or href="#top"
1732
+ // into absolute urls when accessed as event.targed.href, so check the html
1733
+ if (!/href=["']\S+["']/.test(event.target.outerHTML) || /href=["']#\S+["']/.test(event.target.outerHTML)) {
1734
+ return true;
1735
+ }
1736
+
1737
+ // only show when hovering on anchors
1738
+ if (this.base.toolbar && this.base.toolbar.isDisplayed()) {
1739
+ // only show when toolbar is not present
1740
+ return true;
1741
+ }
1742
+
1743
+ // detach handler for other anchor in case we hovered multiple anchors quickly
1744
+ if (this.activeAnchor && this.activeAnchor !== event.target) {
1745
+ this.detachPreviewHandlers();
1493
1746
  }
1747
+
1748
+ this.anchorToPreview = event.target;
1749
+
1750
+ this.instance_handleAnchorMouseout = this.handleAnchorMouseout.bind(this);
1751
+ this.base.on(this.anchorToPreview, 'mouseout', this.instance_handleAnchorMouseout);
1752
+ // Using setTimeout + options.delay because:
1753
+ // - We're going to show the anchor preview according to the configured delay
1754
+ // if the mouse has not left the anchor tag in that time
1755
+ this.base.delay(function () {
1756
+ if (this.anchorToPreview) {
1757
+ //this.activeAnchor = this.anchorToPreview;
1758
+ this.showPreview(this.anchorToPreview);
1759
+ }
1760
+ }.bind(this));
1494
1761
  }
1495
- return -1;
1496
1762
  },
1497
1763
 
1498
- delay: function (fn) {
1499
- var self = this;
1500
- setTimeout(function () {
1501
- if (self.isActive) {
1502
- fn();
1503
- }
1504
- }, this.options.delay);
1764
+ handlePreviewMouseover: function (event) {
1765
+ this.lastOver = (new Date()).getTime();
1766
+ this.hovering = true;
1505
1767
  },
1506
1768
 
1507
- removeAllEvents: function () {
1508
- var e = this.events.pop();
1509
- while (e) {
1510
- e[0].removeEventListener(e[1], e[2], e[3]);
1511
- e = this.events.pop();
1769
+ handlePreviewMouseout: function (event) {
1770
+ if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {
1771
+ this.hovering = false;
1512
1772
  }
1513
1773
  },
1514
1774
 
1515
- initThrottledMethods: function () {
1516
- var self = this;
1775
+ updatePreview: function () {
1776
+ if (this.hovering) {
1777
+ return true;
1778
+ }
1779
+ var durr = (new Date()).getTime() - this.lastOver;
1780
+ if (durr > this.base.options.anchorPreviewHideDelay) {
1781
+ // hide the preview 1/2 second after mouse leaves the link
1782
+ this.detachPreviewHandlers();
1783
+ }
1784
+ },
1517
1785
 
1518
- // handleResize is throttled because:
1519
- // - It will be called when the browser is resizing, which can fire many times very quickly
1520
- // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
1521
- this.handleResize = Util.throttle(function () {
1522
- if (self.isActive) {
1523
- self.positionToolbarIfShown();
1786
+ detachPreviewHandlers: function () {
1787
+ // cleanup
1788
+ clearInterval(this.interval_timer);
1789
+ if (this.instance_handlePreviewMouseover) {
1790
+ this.base.off(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover);
1791
+ this.base.off(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout);
1792
+ if (this.activeAnchor) {
1793
+ this.base.off(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover);
1794
+ this.base.off(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout);
1524
1795
  }
1525
- });
1796
+ }
1526
1797
 
1527
- // handleBlur is throttled because:
1528
- // - This method could be called many times due to the type of event handlers that are calling it
1529
- // - We want a slight delay so that other events in the stack can run, some of which may
1530
- // prevent the toolbar from being hidden (via this.keepToolbarAlive).
1531
- this.handleBlur = Util.throttle(function () {
1532
- if (self.isActive && !self.keepToolbarAlive) {
1533
- self.hideToolbarActions();
1534
- }
1535
- });
1798
+ this.hidePreview();
1536
1799
 
1537
- return this;
1800
+ this.hovering = this.instance_handlePreviewMouseover = this.instance_handlePreviewMouseout = null;
1538
1801
  },
1539
1802
 
1540
- initElements: function () {
1541
- var i,
1542
- addToolbar = false;
1803
+ // TODO: break up method and extract out handlers
1804
+ attachPreviewHandlers: function () {
1805
+ this.lastOver = (new Date()).getTime();
1806
+ this.hovering = true;
1807
+
1808
+ this.instance_handlePreviewMouseover = this.handlePreviewMouseover.bind(this);
1809
+ this.instance_handlePreviewMouseout = this.handlePreviewMouseout.bind(this);
1810
+
1811
+ this.interval_timer = setInterval(this.updatePreview.bind(this), 200);
1812
+
1813
+ this.base.on(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover);
1814
+ this.base.on(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout);
1815
+ this.base.on(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover);
1816
+ this.base.on(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout);
1817
+ }
1818
+ };
1819
+ }(window, document));
1820
+
1821
+ var Toolbar;
1822
+
1823
+ (function (window, document) {
1824
+ 'use strict';
1825
+
1826
+ Toolbar = function Toolbar(instance) {
1827
+ this.base = instance;
1828
+ this.options = instance.options;
1829
+ this.initThrottledMethods();
1830
+ };
1831
+
1832
+ Toolbar.prototype = {
1833
+
1834
+ // Toolbar creation/deletion
1835
+
1836
+ createToolbar: function () {
1837
+ var toolbar = this.base.options.ownerDocument.createElement('div');
1838
+
1839
+ toolbar.id = 'medium-editor-toolbar-' + this.base.id;
1840
+ toolbar.className = 'medium-editor-toolbar';
1841
+
1842
+ if (this.options.staticToolbar) {
1843
+ toolbar.className += " static-toolbar";
1844
+ } else {
1845
+ toolbar.className += " stalker-toolbar";
1846
+ }
1847
+
1848
+ toolbar.appendChild(this.createToolbarButtons());
1849
+
1850
+ // Add any forms that extensions may have
1851
+ this.base.commands.forEach(function (extension) {
1852
+ if (extension.hasForm) {
1853
+ toolbar.appendChild(extension.getForm());
1854
+ }
1855
+ });
1856
+
1857
+ this.attachEventHandlers();
1858
+
1859
+ return toolbar;
1860
+ },
1861
+
1862
+ createToolbarButtons: function () {
1863
+ var ul = this.base.options.ownerDocument.createElement('ul'),
1864
+ li,
1865
+ btn,
1866
+ buttons;
1867
+
1868
+ ul.id = 'medium-editor-toolbar-actions' + this.base.id;
1869
+ ul.className = 'medium-editor-toolbar-actions clearfix';
1870
+ ul.style.display = 'block';
1871
+
1872
+ this.base.commands.forEach(function (extension) {
1873
+ if (typeof extension.getButton === 'function') {
1874
+ btn = extension.getButton(this.base);
1875
+ li = this.base.options.ownerDocument.createElement('li');
1876
+ if (Util.isElement(btn)) {
1877
+ li.appendChild(btn);
1878
+ } else {
1879
+ li.innerHTML = btn;
1880
+ }
1881
+ ul.appendChild(li);
1882
+ }
1883
+ }.bind(this));
1884
+
1885
+ buttons = ul.querySelectorAll('button');
1886
+ if (buttons.length > 0) {
1887
+ buttons[0].classList.add(this.options.firstButtonClass);
1888
+ buttons[buttons.length - 1].classList.add(this.options.lastButtonClass);
1889
+ }
1890
+
1891
+ return ul;
1892
+ },
1893
+
1894
+ deactivate: function () {
1895
+ if (this.toolbar) {
1896
+ if (this.toolbar.parentNode) {
1897
+ this.toolbar.parentNode.removeChild(this.toolbar);
1898
+ }
1899
+ delete this.toolbar;
1900
+ }
1901
+ },
1902
+
1903
+ // Toolbar accessors
1904
+
1905
+ getToolbarElement: function () {
1906
+ if (!this.toolbar) {
1907
+ this.toolbar = this.createToolbar();
1908
+ }
1909
+
1910
+ return this.toolbar;
1911
+ },
1912
+
1913
+ getToolbarActionsElement: function () {
1914
+ return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');
1915
+ },
1916
+
1917
+ // Toolbar event handlers
1918
+
1919
+ initThrottledMethods: function () {
1920
+ // throttledPositionToolbar is throttled because:
1921
+ // - It will be called when the browser is resizing, which can fire many times very quickly
1922
+ // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
1923
+ this.throttledPositionToolbar = Util.throttle(function (event) {
1924
+ if (this.base.isActive) {
1925
+ this.positionToolbarIfShown();
1926
+ }
1927
+ }.bind(this));
1928
+
1929
+ // throttledHideToolbarActions is throttled because:
1930
+ // - This method could be called many times due to the type of event handlers that are calling it
1931
+ // - We want a slight delay so that other events in the stack can run, some of which may
1932
+ // prevent the toolbar from being hidden
1933
+ this.throttledHideToolbarActions = Util.throttle(function (event) {
1934
+ if (this.base.isActive) {
1935
+ this.hideToolbarActions();
1936
+ }
1937
+ }.bind(this));
1938
+ },
1939
+
1940
+ attachEventHandlers: function () {
1941
+ // Handle mouseup on document for updating the selection in the toolbar
1942
+ this.base.on(this.options.ownerDocument.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));
1943
+
1944
+ // Add a scroll event for sticky toolbar
1945
+ if (this.options.staticToolbar && this.options.stickyToolbar) {
1946
+ // On scroll (capture), re-position the toolbar
1947
+ this.base.on(this.options.contentWindow, 'scroll', this.handleWindowScroll.bind(this), true);
1948
+ }
1949
+
1950
+ // On resize, re-position the toolbar
1951
+ this.base.on(this.options.contentWindow, 'resize', this.handleWindowResize.bind(this));
1952
+
1953
+ // Handlers for each contentedtiable element
1954
+ this.base.elements.forEach(function (element) {
1955
+ // Attach click handler to each contenteditable element
1956
+ this.base.on(element, 'click', this.handleEditableClick.bind(this));
1957
+
1958
+ // Attach keyup handler to each contenteditable element
1959
+ this.base.on(element, 'keyup', this.handleEditableKeyup.bind(this));
1960
+
1961
+ // Attach blur handler to each contenteditable element
1962
+ this.base.on(element, 'blur', this.handleEditableBlur.bind(this));
1963
+ }.bind(this));
1964
+ },
1965
+
1966
+ handleWindowScroll: function (event) {
1967
+ this.positionToolbarIfShown();
1968
+ },
1969
+
1970
+ handleWindowResize: function (event) {
1971
+ this.throttledPositionToolbar();
1972
+ },
1973
+
1974
+ handleDocumentMouseup: function (event) {
1975
+ // Do not trigger checkState when mouseup fires over the toolbar
1976
+ if (event &&
1977
+ event.target &&
1978
+ Util.isDescendant(this.getToolbarElement(), event.target)) {
1979
+ return false;
1980
+ }
1981
+ this.checkState();
1982
+ },
1983
+
1984
+ handleEditableClick: function (event) {
1985
+ // Delay the call to checkState to handle bug where selection is empty
1986
+ // immediately after clicking inside a pre-existing selection
1987
+ setTimeout(function () {
1988
+ this.checkState();
1989
+ }.bind(this), 0);
1990
+ },
1991
+
1992
+ handleEditableKeyup: function (event) {
1993
+ this.checkState();
1994
+ },
1995
+
1996
+ handleEditableBlur: function (event) {
1997
+ // Do not trigger checkState when bluring the editable area and clicking into the toolbar
1998
+ if (event &&
1999
+ event.relatedTarget &&
2000
+ Util.isDescendant(this.getToolbarElement(), event.relatedTarget)) {
2001
+ return false;
2002
+ }
2003
+ this.checkState();
2004
+ },
2005
+
2006
+ handleBlur: function (event) {
2007
+ // Hide the toolbar after a small delay so we can prevent this on toolbar click
2008
+ this.throttledHideToolbarActions();
2009
+ },
2010
+
2011
+ // Hiding/showing toolbar
2012
+
2013
+ isDisplayed: function () {
2014
+ return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');
2015
+ },
2016
+
2017
+ showToolbar: function () {
2018
+ if (!this.isDisplayed()) {
2019
+ this.getToolbarElement().classList.add('medium-editor-toolbar-active');
2020
+ if (typeof this.options.onShowToolbar === 'function') {
2021
+ this.options.onShowToolbar();
2022
+ }
2023
+ }
2024
+ },
2025
+
2026
+ hideToolbar: function () {
2027
+ if (this.isDisplayed()) {
2028
+ this.getToolbarElement().classList.remove('medium-editor-toolbar-active');
2029
+ if (typeof this.options.onHideToolbar === 'function') {
2030
+ this.options.onHideToolbar();
2031
+ }
2032
+ }
2033
+ },
2034
+
2035
+ hideToolbarActions: function () {
2036
+ this.base.commands.forEach(function (extension) {
2037
+ if (extension.onHide && typeof extension.onHide === 'function') {
2038
+ extension.onHide();
2039
+ }
2040
+ });
2041
+ this.hideToolbar();
2042
+ },
2043
+
2044
+ isToolbarDefaultActionsDisplayed: function () {
2045
+ return this.getToolbarActionsElement().style.display === 'block';
2046
+ },
2047
+
2048
+ hideToolbarDefaultActions: function () {
2049
+ if (this.isToolbarDefaultActionsDisplayed()) {
2050
+ this.getToolbarActionsElement().style.display = 'none';
2051
+ }
2052
+ },
2053
+
2054
+ showToolbarDefaultActions: function () {
2055
+ this.hideExtensionForms();
2056
+
2057
+ if (!this.isToolbarDefaultActionsDisplayed()) {
2058
+ this.getToolbarActionsElement().style.display = 'block';
2059
+ }
2060
+
2061
+ // Using setTimeout + options.delay because:
2062
+ // We will actually be displaying the toolbar, which should be controlled by options.delay
2063
+ this.base.delay(function () {
2064
+ this.showToolbar();
2065
+ }.bind(this));
2066
+ },
2067
+
2068
+ hideExtensionForms: function () {
2069
+ // Hide all extension forms
2070
+ this.base.commands.forEach(function (extension) {
2071
+ if (extension.hasForm && extension.isDisplayed()) {
2072
+ extension.hideForm();
2073
+ }
2074
+ });
2075
+ },
2076
+
2077
+ // Responding to changes in user selection
2078
+
2079
+ // Checks for existance of multiple block elements in the current selection
2080
+ multipleBlockElementsSelected: function () {
2081
+ /*jslint regexp: true*/
2082
+ var selectionHtml = Selection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
2083
+ hasMultiParagraphs = selectionHtml.match(/<(p|h[1-6]|blockquote)[^>]*>/g);
2084
+ /*jslint regexp: false*/
2085
+
2086
+ return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
2087
+ },
2088
+
2089
+ // TODO: selection and selectionRange should be properties of the
2090
+ // Selection object
2091
+ checkSelectionElement: function (newSelection, selectionElement) {
2092
+ var i,
2093
+ adjacentNode,
2094
+ offset = 0,
2095
+ newRange;
2096
+ this.base.selection = newSelection;
2097
+ this.base.selectionRange = this.base.selection.getRangeAt(0);
2098
+
2099
+ /*
2100
+ * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
2101
+ * will be at the very end of an element. In other browsers, the selectionRange start
2102
+ * would instead be at the very beginning of an element that actually has content.
2103
+ * example:
2104
+ * <span>foo</span><span>bar</span>
2105
+ *
2106
+ * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
2107
+ * of the 'bar' span. However, there are cases where firefox will have the selectionRange start
2108
+ * at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any
2109
+ * properties on the 'bar' span, they won't be reflected accurately in the toolbar
2110
+ * (ie 'Bold' button wouldn't be active)
2111
+ *
2112
+ * So, for cases where the selectionRange start is at the end of an element/node, find the next
2113
+ * adjacent text node that actually has content in it, and move the selectionRange start there.
2114
+ */
2115
+ if (this.options.standardizeSelectionStart &&
2116
+ this.base.selectionRange.startContainer.nodeValue &&
2117
+ (this.base.selectionRange.startOffset === this.base.selectionRange.startContainer.nodeValue.length)) {
2118
+ adjacentNode = Util.findAdjacentTextNodeWithContent(Selection.getSelectionElement(this.options.contentWindow), this.base.selectionRange.startContainer, this.options.ownerDocument);
2119
+ if (adjacentNode) {
2120
+ offset = 0;
2121
+ while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
2122
+ offset = offset + 1;
2123
+ }
2124
+ newRange = this.options.ownerDocument.createRange();
2125
+ newRange.setStart(adjacentNode, offset);
2126
+ newRange.setEnd(this.base.selectionRange.endContainer, this.base.selectionRange.endOffset);
2127
+ this.base.selection.removeAllRanges();
2128
+ this.base.selection.addRange(newRange);
2129
+ this.base.selectionRange = newRange;
2130
+ }
2131
+ }
2132
+
2133
+ for (i = 0; i < this.base.elements.length; i += 1) {
2134
+ if (this.base.elements[i] === selectionElement) {
2135
+ this.showAndUpdateToolbar();
2136
+ return;
2137
+ }
2138
+ }
2139
+
2140
+ if (!this.options.staticToolbar) {
2141
+ this.hideToolbarActions();
2142
+ }
2143
+ },
2144
+
2145
+ checkState: function () {
2146
+ var newSelection,
2147
+ selectionElement;
2148
+
2149
+ if (!this.base.preventSelectionUpdates) {
2150
+ newSelection = this.options.contentWindow.getSelection();
2151
+ if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
2152
+ (this.options.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected()) ||
2153
+ Selection.selectionInContentEditableFalse(this.options.contentWindow)) {
2154
+ if (!this.options.staticToolbar) {
2155
+ this.hideToolbarActions();
2156
+ } else {
2157
+ this.showAndUpdateToolbar();
2158
+ }
2159
+
2160
+ } else {
2161
+ selectionElement = Selection.getSelectionElement(this.options.contentWindow);
2162
+ if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
2163
+ if (!this.options.staticToolbar) {
2164
+ this.hideToolbarActions();
2165
+ }
2166
+ } else {
2167
+ this.checkSelectionElement(newSelection, selectionElement);
2168
+ }
2169
+ }
2170
+ }
2171
+ },
2172
+
2173
+ // Updating the toolbar
2174
+
2175
+ showAndUpdateToolbar: function () {
2176
+ this.setToolbarButtonStates();
2177
+ this.showToolbarDefaultActions();
2178
+ this.setToolbarPosition();
2179
+ },
2180
+
2181
+ setToolbarButtonStates: function () {
2182
+ this.base.commands.forEach(function (extension) {
2183
+ if (typeof extension.isActive === 'function') {
2184
+ extension.setInactive();
2185
+ }
2186
+ }.bind(this));
2187
+ this.checkActiveButtons();
2188
+ },
2189
+
2190
+ checkActiveButtons: function () {
2191
+ var manualStateChecks = [],
2192
+ queryState = null,
2193
+ parentNode,
2194
+ updateExtensionState = function (extension) {
2195
+ if (typeof extension.checkState === 'function') {
2196
+ extension.checkState(parentNode);
2197
+ } else if (typeof extension.isActive === 'function' &&
2198
+ typeof extension.isAlreadyApplied === 'function') {
2199
+ if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
2200
+ extension.setActive();
2201
+ }
2202
+ }
2203
+ };
2204
+
2205
+ if (!this.base.selectionRange) {
2206
+ return;
2207
+ }
2208
+
2209
+ parentNode = Selection.getSelectedParentElement(this.base.selectionRange);
2210
+
2211
+ // Loop through all commands
2212
+ this.base.commands.forEach(function (command) {
2213
+ // For those commands where we can use document.queryCommandState(), do so
2214
+ if (typeof command.queryCommandState === 'function') {
2215
+ queryState = command.queryCommandState();
2216
+ // If queryCommandState returns a valid value, we can trust the browser
2217
+ // and don't need to do our manual checks
2218
+ if (queryState !== null) {
2219
+ if (queryState) {
2220
+ command.setActive();
2221
+ }
2222
+ return;
2223
+ }
2224
+ }
2225
+ // We can't use queryCommandState for this command, so add to manualStateChecks
2226
+ manualStateChecks.push(command);
2227
+ });
2228
+
2229
+ // Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node
2230
+ while (parentNode.tagName !== undefined && Util.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
2231
+ manualStateChecks.forEach(updateExtensionState);
2232
+
2233
+ // we can abort the search upwards if we leave the contentEditable element
2234
+ if (this.base.elements.indexOf(parentNode) !== -1) {
2235
+ break;
2236
+ }
2237
+ parentNode = parentNode.parentNode;
2238
+ }
2239
+ },
2240
+
2241
+ // Positioning toolbar
2242
+
2243
+ positionToolbarIfShown: function () {
2244
+ if (this.isDisplayed()) {
2245
+ this.setToolbarPosition();
2246
+ }
2247
+ },
2248
+
2249
+ setToolbarPosition: function () {
2250
+ var container = Selection.getSelectionElement(this.options.contentWindow),
2251
+ selection = this.options.contentWindow.getSelection(),
2252
+ anchorPreview;
2253
+
2254
+ // If there isn't a valid selection, bail
2255
+ if (!container || !this.options.contentWindow.getSelection().focusNode) {
2256
+ return this;
2257
+ }
2258
+
2259
+ // If the container isn't part of this medium-editor instance, bail
2260
+ if (this.base.elements.indexOf(container) === -1) {
2261
+ return this;
2262
+ }
2263
+
2264
+ if (this.options.staticToolbar) {
2265
+ this.showToolbar();
2266
+ this.positionStaticToolbar(container);
2267
+
2268
+ } else if (!selection.isCollapsed) {
2269
+ this.showToolbar();
2270
+ this.positionToolbar(selection);
2271
+ }
2272
+
2273
+ anchorPreview = this.base.getExtensionByName('anchor-preview');
2274
+
2275
+ if (anchorPreview && typeof anchorPreview.hidePreview === 'function') {
2276
+ anchorPreview.hidePreview();
2277
+ }
2278
+ },
2279
+
2280
+ positionStaticToolbar: function (container) {
2281
+ // position the toolbar at left 0, so we can get the real width of the toolbar
2282
+ this.getToolbarElement().style.left = '0';
2283
+
2284
+ // document.documentElement for IE 9
2285
+ var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
2286
+ windowWidth = this.options.contentWindow.innerWidth,
2287
+ toolbarElement = this.getToolbarElement(),
2288
+ containerRect = container.getBoundingClientRect(),
2289
+ containerTop = containerRect.top + scrollTop,
2290
+ containerCenter = (containerRect.left + (containerRect.width / 2)),
2291
+ toolbarHeight = toolbarElement.offsetHeight,
2292
+ toolbarWidth = toolbarElement.offsetWidth,
2293
+ halfOffsetWidth = toolbarWidth / 2,
2294
+ targetLeft;
2295
+
2296
+ if (this.options.stickyToolbar) {
2297
+ // If it's beyond the height of the editor, position it at the bottom of the editor
2298
+ if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) {
2299
+ toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
2300
+ toolbarElement.classList.remove('sticky-toolbar');
2301
+
2302
+ // Stick the toolbar to the top of the window
2303
+ } else if (scrollTop > (containerTop - toolbarHeight)) {
2304
+ toolbarElement.classList.add('sticky-toolbar');
2305
+ toolbarElement.style.top = "0px";
2306
+
2307
+ // Normal static toolbar position
2308
+ } else {
2309
+ toolbarElement.classList.remove('sticky-toolbar');
2310
+ toolbarElement.style.top = containerTop - toolbarHeight + "px";
2311
+ }
2312
+ } else {
2313
+ toolbarElement.style.top = containerTop - toolbarHeight + "px";
2314
+ }
2315
+
2316
+ if (this.options.toolbarAlign === 'left') {
2317
+ targetLeft = containerRect.left;
2318
+ } else if (this.options.toolbarAlign === 'center') {
2319
+ targetLeft = containerCenter - halfOffsetWidth;
2320
+ } else if (this.options.toolbarAlign === 'right') {
2321
+ targetLeft = containerRect.right - toolbarWidth;
2322
+ }
2323
+
2324
+ if (targetLeft < 0) {
2325
+ targetLeft = 0;
2326
+ } else if ((targetLeft + toolbarWidth) > windowWidth) {
2327
+ targetLeft = windowWidth - toolbarWidth;
2328
+ }
2329
+
2330
+ toolbarElement.style.left = targetLeft + 'px';
2331
+ },
2332
+
2333
+ positionToolbar: function (selection) {
2334
+ // position the toolbar at left 0, so we can get the real width of the toolbar
2335
+ this.getToolbarElement().style.left = '0';
2336
+
2337
+ var windowWidth = this.options.contentWindow.innerWidth,
2338
+ range = selection.getRangeAt(0),
2339
+ boundary = range.getBoundingClientRect(),
2340
+ middleBoundary = (boundary.left + boundary.right) / 2,
2341
+ toolbarElement = this.getToolbarElement(),
2342
+ toolbarHeight = toolbarElement.offsetHeight,
2343
+ toolbarWidth = toolbarElement.offsetWidth,
2344
+ halfOffsetWidth = toolbarWidth / 2,
2345
+ buttonHeight = 50,
2346
+ defaultLeft = this.options.diffLeft - halfOffsetWidth;
2347
+
2348
+ if (boundary.top < buttonHeight) {
2349
+ toolbarElement.classList.add('medium-toolbar-arrow-over');
2350
+ toolbarElement.classList.remove('medium-toolbar-arrow-under');
2351
+ toolbarElement.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
2352
+ } else {
2353
+ toolbarElement.classList.add('medium-toolbar-arrow-under');
2354
+ toolbarElement.classList.remove('medium-toolbar-arrow-over');
2355
+ toolbarElement.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
2356
+ }
2357
+ if (middleBoundary < halfOffsetWidth) {
2358
+ toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px';
2359
+ } else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
2360
+ toolbarElement.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px';
2361
+ } else {
2362
+ toolbarElement.style.left = defaultLeft + middleBoundary + 'px';
2363
+ }
2364
+ }
2365
+ };
2366
+ }(window, document));
2367
+
2368
+ function MediumEditor(elements, options) {
2369
+ 'use strict';
2370
+ return this.init(elements, options);
2371
+ }
2372
+
2373
+ (function () {
2374
+ 'use strict';
2375
+
2376
+ MediumEditor.statics = {
2377
+ ButtonsData: ButtonsData,
2378
+ DefaultButton: DefaultButton,
2379
+ AnchorExtension: AnchorExtension,
2380
+ Toolbar: Toolbar,
2381
+ AnchorPreview: AnchorPreview
2382
+ };
2383
+
2384
+ MediumEditor.prototype = {
2385
+ defaults: {
2386
+ allowMultiParagraphSelection: true,
2387
+ anchorInputPlaceholder: 'Paste or type a link',
2388
+ anchorInputCheckboxLabel: 'Open in new window',
2389
+ anchorPreviewHideDelay: 500,
2390
+ buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
2391
+ buttonLabels: false,
2392
+ checkLinkFormat: false,
2393
+ cleanPastedHTML: false,
2394
+ delay: 0,
2395
+ diffLeft: 0,
2396
+ diffTop: -10,
2397
+ disableReturn: false,
2398
+ disableDoubleReturn: false,
2399
+ disableToolbar: false,
2400
+ disableAnchorPreview: false,
2401
+ disableEditing: false,
2402
+ disablePlaceholders: false,
2403
+ toolbarAlign: 'center',
2404
+ elementsContainer: false,
2405
+ imageDragging: true,
2406
+ standardizeSelectionStart: false,
2407
+ contentWindow: window,
2408
+ ownerDocument: document,
2409
+ firstHeader: 'h3',
2410
+ forcePlainText: true,
2411
+ placeholder: 'Type your text',
2412
+ secondHeader: 'h4',
2413
+ targetBlank: false,
2414
+ anchorTarget: false,
2415
+ anchorButton: false,
2416
+ anchorButtonClass: 'btn',
2417
+ extensions: {},
2418
+ activeButtonClass: 'medium-editor-button-active',
2419
+ firstButtonClass: 'medium-editor-button-first',
2420
+ lastButtonClass: 'medium-editor-button-last'
2421
+ },
2422
+
2423
+ init: function (elements, options) {
2424
+ var uniqueId = 1;
2425
+
2426
+ this.options = Util.defaults(options, this.defaults);
2427
+ this.setElementSelection(elements);
2428
+ if (this.elements.length === 0) {
2429
+ return;
2430
+ }
2431
+
2432
+ if (!this.options.elementsContainer) {
2433
+ this.options.elementsContainer = this.options.ownerDocument.body;
2434
+ }
2435
+
2436
+ while (this.options.elementsContainer.querySelector('#medium-editor-toolbar-' + uniqueId)) {
2437
+ uniqueId = uniqueId + 1;
2438
+ }
2439
+
2440
+ this.id = uniqueId;
2441
+
2442
+ return this.setup();
2443
+ },
2444
+
2445
+ setup: function () {
2446
+ this.events = [];
2447
+ this.isActive = true;
2448
+ this.initCommands()
2449
+ .initElements()
2450
+ .bindDragDrop()
2451
+ .bindPaste()
2452
+ .setPlaceholders()
2453
+ .bindElementActions()
2454
+ .bindBlur();
2455
+ },
2456
+
2457
+ on: function (target, event, listener, useCapture) {
2458
+ target.addEventListener(event, listener, useCapture);
2459
+ this.events.push([target, event, listener, useCapture]);
2460
+ },
2461
+
2462
+ off: function (target, event, listener, useCapture) {
2463
+ var index = this.indexOfListener(target, event, listener, useCapture),
2464
+ e;
2465
+ if (index !== -1) {
2466
+ e = this.events.splice(index, 1)[0];
2467
+ e[0].removeEventListener(e[1], e[2], e[3]);
2468
+ }
2469
+ },
2470
+
2471
+ indexOfListener: function (target, event, listener, useCapture) {
2472
+ var i, n, item;
2473
+ for (i = 0, n = this.events.length; i < n; i = i + 1) {
2474
+ item = this.events[i];
2475
+ if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
2476
+ return i;
2477
+ }
2478
+ }
2479
+ return -1;
2480
+ },
2481
+
2482
+ delay: function (fn) {
2483
+ var self = this;
2484
+ setTimeout(function () {
2485
+ if (self.isActive) {
2486
+ fn();
2487
+ }
2488
+ }, this.options.delay);
2489
+ },
2490
+
2491
+ removeAllEvents: function () {
2492
+ var e = this.events.pop();
2493
+ while (e) {
2494
+ e[0].removeEventListener(e[1], e[2], e[3]);
2495
+ e = this.events.pop();
2496
+ }
2497
+ },
2498
+
2499
+ initElements: function () {
2500
+ var i,
2501
+ addToolbar = false;
1543
2502
  for (i = 0; i < this.elements.length; i += 1) {
1544
2503
  if (!this.options.disableEditing && !this.elements[i].getAttribute('data-disable-editing')) {
1545
2504
  this.elements[i].setAttribute('contentEditable', true);
@@ -1557,9 +2516,7 @@ function MediumEditor(elements, options) {
1557
2516
  }
1558
2517
  // Init toolbar
1559
2518
  if (addToolbar) {
1560
- this.initToolbar()
1561
- .setFirstAndLastButtons()
1562
- .bindAnchorPreview();
2519
+ this.initToolbar();
1563
2520
  }
1564
2521
  return this;
1565
2522
  },
@@ -1585,6 +2542,9 @@ function MediumEditor(elements, options) {
1585
2542
  blurFunction = function (e) {
1586
2543
  var isDescendantOfEditorElements = false,
1587
2544
  selection = self.options.contentWindow.getSelection(),
2545
+ toolbarEl = (self.toolbar) ? self.toolbar.getToolbarElement() : null,
2546
+ anchorPreview = self.getExtensionByName('anchor-preview'),
2547
+ previewEl = (anchorPreview && anchorPreview.getPreviewElement) ? anchorPreview.getPreviewElement() : null,
1588
2548
  selRange = selection.isCollapsed ?
1589
2549
  null :
1590
2550
  Selection.getSelectedParentElement(selection.getRangeAt(0)),
@@ -1594,26 +2554,27 @@ function MediumEditor(elements, options) {
1594
2554
  // to disapper when selecting from right to left and
1595
2555
  // the selection ends at the beginning of the text.
1596
2556
  for (i = 0; i < self.elements.length; i += 1) {
1597
- if (Util.isDescendant(self.elements[i], e.target)
2557
+ if (self.elements[i] === e.target
2558
+ || Util.isDescendant(self.elements[i], e.target)
1598
2559
  || Util.isDescendant(self.elements[i], selRange)) {
1599
2560
  isDescendantOfEditorElements = true;
1600
2561
  break;
1601
2562
  }
1602
2563
  }
1603
- // If it's not part of the editor, or the toolbar
1604
- if (e.target !== self.toolbar
1605
- && self.elements.indexOf(e.target) === -1
1606
- && !isDescendantOfEditorElements
1607
- && !Util.isDescendant(self.toolbar, e.target)
1608
- && !Util.isDescendant(self.anchorPreview, e.target)) {
2564
+ // If it's not part of the editor, toolbar, or anchor preview
2565
+ if (!isDescendantOfEditorElements
2566
+ && (!toolbarEl || (toolbarEl !== e.target && !Util.isDescendant(toolbarEl, e.target)))
2567
+ && (!previewEl || (previewEl !== e.target && !Util.isDescendant(previewEl, e.target)))) {
1609
2568
 
1610
2569
  // Activate the placeholder
1611
2570
  if (!self.options.disablePlaceholders) {
1612
2571
  self.placeholderWrapper(e, self.elements[0]);
1613
2572
  }
1614
2573
 
1615
- // Hide the toolbar after a small delay so we can prevent this on toolbar click
1616
- self.handleBlur();
2574
+ // Let the toolbar know that we've detected a blur
2575
+ if (self.toolbar) {
2576
+ self.toolbar.handleBlur(e);
2577
+ }
1617
2578
  }
1618
2579
  };
1619
2580
 
@@ -1625,18 +2586,12 @@ function MediumEditor(elements, options) {
1625
2586
  },
1626
2587
 
1627
2588
  bindClick: function (i) {
1628
- var self = this;
1629
-
1630
- this.on(this.elements[i], 'click', function () {
1631
- if (!self.options.disablePlaceholders) {
2589
+ if (!this.options.disablePlaceholders) {
2590
+ this.on(this.elements[i], 'click', function () {
1632
2591
  // Remove placeholder
1633
2592
  this.classList.remove('medium-editor-placeholder');
1634
- }
1635
-
1636
- if (self.options.staticToolbar) {
1637
- self.setToolbarPosition();
1638
- }
1639
- });
2593
+ });
2594
+ }
1640
2595
 
1641
2596
  return this;
1642
2597
  },
@@ -1707,6 +2662,32 @@ function MediumEditor(elements, options) {
1707
2662
  return extension;
1708
2663
  },
1709
2664
 
2665
+ shouldAddDefaultAnchorPreview: function () {
2666
+ var i,
2667
+ shouldAdd = false;
2668
+
2669
+ // If anchor-preview is disabled, don't add
2670
+ if (this.options.disableAnchorPreview) {
2671
+ return false;
2672
+ }
2673
+ // If anchor-preview extension has been overriden, don't add
2674
+ if (this.options.extensions['anchor-preview']) {
2675
+ return false;
2676
+ }
2677
+ // If toolbar is disabled, don't add
2678
+ if (this.options.disableToolbar) {
2679
+ return false;
2680
+ }
2681
+ // If all elements have 'data-disable-toolbar' attribute, don't add
2682
+ for (i = 0; i < this.elements.length; i += 1) {
2683
+ if (!this.elements[i].getAttribute('data-disable-toolbar')) {
2684
+ shouldAdd = true;
2685
+ }
2686
+ }
2687
+
2688
+ return shouldAdd;
2689
+ },
2690
+
1710
2691
  initCommands: function () {
1711
2692
  var buttons = this.options.buttons,
1712
2693
  extensions = this.options.extensions,
@@ -1733,6 +2714,11 @@ function MediumEditor(elements, options) {
1733
2714
  }
1734
2715
  }
1735
2716
 
2717
+ // Add AnchorPreview as extension if needed
2718
+ if (this.shouldAddDefaultAnchorPreview()) {
2719
+ this.commands.push(this.initExtension(new AnchorPreview(), 'anchor-preview'));
2720
+ }
2721
+
1736
2722
  return this;
1737
2723
  },
1738
2724
 
@@ -1794,7 +2780,10 @@ function MediumEditor(elements, options) {
1794
2780
  tagName,
1795
2781
  editorElement;
1796
2782
 
1797
- if (node && node.getAttribute('data-medium-element') && node.children.length === 0 && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
2783
+ if (node
2784
+ && node.getAttribute('data-medium-element')
2785
+ && node.children.length === 0
2786
+ && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
1798
2787
  self.options.ownerDocument.execCommand('formatBlock', false, 'p');
1799
2788
  }
1800
2789
  if (e.which === Util.keyCode.ENTER) {
@@ -1804,16 +2793,15 @@ function MediumEditor(elements, options) {
1804
2793
 
1805
2794
  if (!(self.options.disableReturn || editorElement.getAttribute('data-disable-return')) &&
1806
2795
  tagName !== 'li' && !Util.isListItemChild(node)) {
1807
- if (!e.shiftKey) {
1808
-
1809
- // paragraph creation should not be forced within a header tag
2796
+ // For anchor tags, unlink
2797
+ if (tagName === 'a') {
2798
+ self.options.ownerDocument.execCommand('unlink', false, null);
2799
+ } else if (!e.shiftKey) {
2800
+ // only format block if this is not a header tag
1810
2801
  if (!/h\d/.test(tagName)) {
1811
2802
  self.options.ownerDocument.execCommand('formatBlock', false, 'p');
1812
2803
  }
1813
2804
  }
1814
- if (tagName === 'a') {
1815
- self.options.ownerDocument.execCommand('unlink', false, null);
1816
- }
1817
2805
  }
1818
2806
  }
1819
2807
  });
@@ -1849,7 +2837,7 @@ function MediumEditor(elements, options) {
1849
2837
 
1850
2838
  if (tag === 'pre') {
1851
2839
  e.preventDefault();
1852
- self.options.ownerDocument.execCommand('insertHtml', null, ' ');
2840
+ Util.insertHTMLCommand(self.options.ownerDocument, ' ');
1853
2841
  }
1854
2842
 
1855
2843
  // Tab to indent list structures!
@@ -1858,9 +2846,9 @@ function MediumEditor(elements, options) {
1858
2846
 
1859
2847
  // If Shift is down, outdent, otherwise indent
1860
2848
  if (e.shiftKey) {
1861
- self.options.ownerDocument.execCommand('outdent', e);
2849
+ self.options.ownerDocument.execCommand('outdent', false, null);
1862
2850
  } else {
1863
- self.options.ownerDocument.execCommand('indent', e);
2851
+ self.options.ownerDocument.execCommand('indent', false, null);
1864
2852
  }
1865
2853
  }
1866
2854
  } else if (e.which === Util.keyCode.BACKSPACE || e.which === Util.keyCode.DELETE || e.which === Util.keyCode.ENTER) {
@@ -1871,7 +2859,7 @@ function MediumEditor(elements, options) {
1871
2859
  } else if (e.ctrlKey || e.metaKey) {
1872
2860
  key = String.fromCharCode(e.which || e.keyCode).toLowerCase();
1873
2861
  self.commands.forEach(function (extension) {
1874
- if (extension.options.key && extension.options.key === key) {
2862
+ if (extension.options && extension.options.key && extension.options.key === key) {
1875
2863
  extension.handleClick(e);
1876
2864
  }
1877
2865
  });
@@ -1922,455 +2910,102 @@ function MediumEditor(elements, options) {
1922
2910
 
1923
2911
  // remove node and move cursor to start of header
1924
2912
  range = document.createRange();
1925
- sel = window.getSelection();
2913
+ sel = this.options.contentWindow.getSelection();
1926
2914
 
1927
2915
  range.setStart(node.nextElementSibling, 0);
1928
2916
  range.collapse(true);
1929
2917
 
1930
- sel.removeAllRanges();
1931
- sel.addRange(range);
1932
-
1933
- node.previousElementSibling.parentNode.removeChild(node);
1934
-
1935
- e.preventDefault();
1936
- }
1937
- },
1938
-
1939
- initToolbar: function () {
1940
- if (this.toolbar) {
1941
- return this;
1942
- }
1943
- this.toolbar = this.createToolbar();
1944
- this.keepToolbarAlive = false;
1945
- this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions');
1946
- this.anchorPreview = this.createAnchorPreview();
1947
-
1948
- return this;
1949
- },
1950
-
1951
- createToolbar: function () {
1952
- var toolbar = this.options.ownerDocument.createElement('div');
1953
- toolbar.id = 'medium-editor-toolbar-' + this.id;
1954
- toolbar.className = 'medium-editor-toolbar';
1955
-
1956
- if (this.options.staticToolbar) {
1957
- toolbar.className += " static-toolbar";
1958
- } else {
1959
- toolbar.className += " stalker-toolbar";
1960
- }
1961
-
1962
- toolbar.appendChild(this.toolbarButtons());
1963
-
1964
- // Add any forms that extensions may have
1965
- this.commands.forEach(function (extension) {
1966
- if (extension.hasForm) {
1967
- toolbar.appendChild(extension.getForm());
1968
- }
1969
- });
1970
-
1971
- this.options.elementsContainer.appendChild(toolbar);
1972
- return toolbar;
1973
- },
1974
-
1975
- //TODO: actionTemplate
1976
- toolbarButtons: function () {
1977
- var ul = this.options.ownerDocument.createElement('ul'),
1978
- li,
1979
- btn;
1980
-
1981
- ul.id = 'medium-editor-toolbar-actions' + this.id;
1982
- ul.className = 'medium-editor-toolbar-actions clearfix';
1983
-
1984
- this.commands.forEach(function (extension) {
1985
- if (typeof extension.getButton === 'function') {
1986
- btn = extension.getButton(this);
1987
- li = this.options.ownerDocument.createElement('li');
1988
- if (Util.isElement(btn)) {
1989
- li.appendChild(btn);
1990
- } else {
1991
- li.innerHTML = btn;
1992
- }
1993
- ul.appendChild(li);
1994
- }
1995
- }.bind(this));
1996
-
1997
- return ul;
1998
- },
1999
-
2000
- bindSelect: function () {
2001
- var i,
2002
- blurHelper = function (event) {
2003
- // Do not close the toolbar when bluring the editable area and clicking into the anchor form
2004
- if (event &&
2005
- event.type &&
2006
- event.type.toLowerCase() === 'blur' &&
2007
- event.relatedTarget &&
2008
- Util.isDescendant(this.toolbar, event.relatedTarget)) {
2009
- return false;
2010
- }
2011
- this.checkSelection();
2012
- }.bind(this),
2013
- timeoutHelper = function () {
2014
- setTimeout(function () {
2015
- this.checkSelection();
2016
- }.bind(this), 0);
2017
- }.bind(this);
2018
-
2019
- this.on(this.options.ownerDocument.documentElement, 'mouseup', this.checkSelection.bind(this));
2020
-
2021
- for (i = 0; i < this.elements.length; i += 1) {
2022
- this.on(this.elements[i], 'keyup', this.checkSelection.bind(this));
2023
- this.on(this.elements[i], 'blur', blurHelper);
2024
- this.on(this.elements[i], 'click', timeoutHelper);
2025
- }
2026
-
2027
- return this;
2028
- },
2029
-
2030
- bindDragDrop: function () {
2031
- var self = this, i, className, onDrag, onDrop, element;
2032
-
2033
- if (!self.options.imageDragging) {
2034
- return this;
2035
- }
2036
-
2037
- className = 'medium-editor-dragover';
2038
-
2039
- onDrag = function (e) {
2040
- e.preventDefault();
2041
- e.dataTransfer.dropEffect = "copy";
2042
-
2043
- if (e.type === "dragover") {
2044
- this.classList.add(className);
2045
- } else {
2046
- this.classList.remove(className);
2047
- }
2048
- };
2049
-
2050
- onDrop = function (e) {
2051
- var files;
2052
- e.preventDefault();
2053
- e.stopPropagation();
2054
- files = Array.prototype.slice.call(e.dataTransfer.files, 0);
2055
- files.some(function (file) {
2056
- if (file.type.match("image")) {
2057
- var fileReader, id;
2058
- fileReader = new FileReader();
2059
- fileReader.readAsDataURL(file);
2060
-
2061
- id = 'medium-img-' + (+new Date());
2062
- Util.insertHTMLCommand(self.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
2063
-
2064
- fileReader.onload = function () {
2065
- var img = document.getElementById(id);
2066
- if (img) {
2067
- img.removeAttribute('id');
2068
- img.removeAttribute('class');
2069
- img.src = fileReader.result;
2070
- }
2071
- };
2072
- }
2073
- });
2074
- this.classList.remove(className);
2075
- };
2076
-
2077
- for (i = 0; i < this.elements.length; i += 1) {
2078
- element = this.elements[i];
2079
-
2080
-
2081
- this.on(element, 'dragover', onDrag);
2082
- this.on(element, 'dragleave', onDrag);
2083
- this.on(element, 'drop', onDrop);
2084
- }
2085
- return this;
2086
- },
2087
-
2088
- stopSelectionUpdates: function () {
2089
- this.preventSelectionUpdates = true;
2090
- },
2091
-
2092
- startSelectionUpdates: function () {
2093
- this.preventSelectionUpdates = false;
2094
- },
2095
-
2096
- checkSelection: function () {
2097
- var newSelection,
2098
- selectionElement;
2099
-
2100
- if (!this.preventSelectionUpdates &&
2101
- this.keepToolbarAlive !== true &&
2102
- !this.options.disableToolbar) {
2103
-
2104
- newSelection = this.options.contentWindow.getSelection();
2105
- if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
2106
- (this.options.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected()) ||
2107
- Selection.selectionInContentEditableFalse(this.options.contentWindow)) {
2108
- if (!this.options.staticToolbar) {
2109
- this.hideToolbarActions();
2110
- } else {
2111
- this.showAndUpdateToolbar();
2112
- }
2113
-
2114
- } else {
2115
- selectionElement = Selection.getSelectionElement(this.options.contentWindow);
2116
- if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
2117
- if (!this.options.staticToolbar) {
2118
- this.hideToolbarActions();
2119
- }
2120
- } else {
2121
- this.checkSelectionElement(newSelection, selectionElement);
2122
- }
2123
- }
2124
- }
2125
- return this;
2126
- },
2127
-
2128
- // Checks for existance of multiple block elements in the current selection
2129
- multipleBlockElementsSelected: function () {
2130
- /*jslint regexp: true*/
2131
- var selectionHtml = Selection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
2132
- hasMultiParagraphs = selectionHtml.match(/<(p|h[1-6]|blockquote)[^>]*>/g);
2133
- /*jslint regexp: false*/
2134
-
2135
- return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
2136
- },
2137
-
2138
- checkSelectionElement: function (newSelection, selectionElement) {
2139
- var i,
2140
- adjacentNode,
2141
- offset = 0,
2142
- newRange;
2143
- this.selection = newSelection;
2144
- this.selectionRange = this.selection.getRangeAt(0);
2145
-
2146
- /*
2147
- * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
2148
- * will be at the very end of an element. In other browsers, the selectionRange start
2149
- * would instead be at the very beginning of an element that actually has content.
2150
- * example:
2151
- * <span>foo</span><span>bar</span>
2152
- *
2153
- * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
2154
- * of the 'bar' span. However, there are cases where firefox will have the selectionRange start
2155
- * at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any
2156
- * properties on the 'bar' span, they won't be reflected accurately in the toolbar
2157
- * (ie 'Bold' button wouldn't be active)
2158
- *
2159
- * So, for cases where the selectionRange start is at the end of an element/node, find the next
2160
- * adjacent text node that actually has content in it, and move the selectionRange start there.
2161
- */
2162
- if (this.options.standardizeSelectionStart &&
2163
- this.selectionRange.startContainer.nodeValue &&
2164
- (this.selectionRange.startOffset === this.selectionRange.startContainer.nodeValue.length)) {
2165
- adjacentNode = Util.findAdjacentTextNodeWithContent(Selection.getSelectionElement(this.options.contentWindow), this.selectionRange.startContainer, this.options.ownerDocument);
2166
- if (adjacentNode) {
2167
- offset = 0;
2168
- while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
2169
- offset = offset + 1;
2170
- }
2171
- newRange = this.options.ownerDocument.createRange();
2172
- newRange.setStart(adjacentNode, offset);
2173
- newRange.setEnd(this.selectionRange.endContainer, this.selectionRange.endOffset);
2174
- this.selection.removeAllRanges();
2175
- this.selection.addRange(newRange);
2176
- this.selectionRange = newRange;
2177
- }
2178
- }
2179
-
2180
- for (i = 0; i < this.elements.length; i += 1) {
2181
- if (this.elements[i] === selectionElement) {
2182
- this.showAndUpdateToolbar();
2183
- return;
2184
- }
2185
- }
2186
-
2187
- if (!this.options.staticToolbar) {
2188
- this.hideToolbarActions();
2189
- }
2190
- },
2191
-
2192
- showAndUpdateToolbar: function () {
2193
- this.setToolbarButtonStates()
2194
- .setToolbarPosition()
2195
- .showToolbarDefaultActions();
2196
- },
2197
-
2198
- setToolbarPosition: function () {
2199
- // document.documentElement for IE 9
2200
- var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
2201
- selection = this.options.contentWindow.getSelection(),
2202
- windowWidth = this.options.contentWindow.innerWidth,
2203
- container = Selection.getSelectionElement(this.options.contentWindow),
2204
- buttonHeight = 50,
2205
- toolbarWidth,
2206
- toolbarHeight,
2207
- halfOffsetWidth,
2208
- defaultLeft,
2209
- containerRect,
2210
- containerTop,
2211
- containerCenter,
2212
- range,
2213
- boundary,
2214
- middleBoundary,
2215
- targetLeft;
2216
-
2217
- // If there isn't a valid selection, bail
2218
- if (!container || !this.options.contentWindow.getSelection().focusNode) {
2219
- return this;
2220
- }
2221
-
2222
- // If the container isn't part of this medium-editor instance, bail
2223
- if (this.elements.indexOf(container) === -1) {
2224
- return this;
2225
- }
2226
-
2227
- // Calculate container dimensions
2228
- containerRect = container.getBoundingClientRect();
2229
- containerTop = containerRect.top + scrollTop;
2230
- containerCenter = (containerRect.left + (containerRect.width / 2));
2231
-
2232
- // position the toolbar at left 0, so we can get the real width of the toolbar
2233
- this.toolbar.style.left = '0';
2234
- toolbarWidth = this.toolbar.offsetWidth;
2235
- toolbarHeight = this.toolbar.offsetHeight;
2236
- halfOffsetWidth = toolbarWidth / 2;
2237
- defaultLeft = this.options.diffLeft - halfOffsetWidth;
2238
-
2239
- if (this.options.staticToolbar) {
2240
- this.showToolbar();
2241
-
2242
- if (this.options.stickyToolbar) {
2243
- // If it's beyond the height of the editor, position it at the bottom of the editor
2244
- if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) {
2245
- this.toolbar.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
2246
- this.toolbar.classList.remove('sticky-toolbar');
2247
-
2248
- // Stick the toolbar to the top of the window
2249
- } else if (scrollTop > (containerTop - toolbarHeight)) {
2250
- this.toolbar.classList.add('sticky-toolbar');
2251
- this.toolbar.style.top = "0px";
2252
-
2253
- // Normal static toolbar position
2254
- } else {
2255
- this.toolbar.classList.remove('sticky-toolbar');
2256
- this.toolbar.style.top = containerTop - toolbarHeight + "px";
2257
- }
2258
- } else {
2259
- this.toolbar.style.top = containerTop - toolbarHeight + "px";
2260
- }
2261
-
2262
- if (this.options.toolbarAlign === 'left') {
2263
- targetLeft = containerRect.left;
2264
- } else if (this.options.toolbarAlign === 'center') {
2265
- targetLeft = containerCenter - halfOffsetWidth;
2266
- } else if (this.options.toolbarAlign === 'right') {
2267
- targetLeft = containerRect.right - toolbarWidth;
2268
- }
2269
-
2270
- if (targetLeft < 0) {
2271
- targetLeft = 0;
2272
- } else if ((targetLeft + toolbarWidth) > windowWidth) {
2273
- targetLeft = windowWidth - toolbarWidth;
2274
- }
2275
-
2276
- this.toolbar.style.left = targetLeft + 'px';
2277
-
2278
- } else if (!selection.isCollapsed) {
2279
- this.showToolbar();
2280
-
2281
- range = selection.getRangeAt(0);
2282
- boundary = range.getBoundingClientRect();
2283
- middleBoundary = (boundary.left + boundary.right) / 2;
2284
-
2285
- if (boundary.top < buttonHeight) {
2286
- this.toolbar.classList.add('medium-toolbar-arrow-over');
2287
- this.toolbar.classList.remove('medium-toolbar-arrow-under');
2288
- this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
2289
- } else {
2290
- this.toolbar.classList.add('medium-toolbar-arrow-under');
2291
- this.toolbar.classList.remove('medium-toolbar-arrow-over');
2292
- this.toolbar.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
2293
- }
2294
- if (middleBoundary < halfOffsetWidth) {
2295
- this.toolbar.style.left = defaultLeft + halfOffsetWidth + 'px';
2296
- } else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
2297
- this.toolbar.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px';
2298
- } else {
2299
- this.toolbar.style.left = defaultLeft + middleBoundary + 'px';
2300
- }
2301
- }
2918
+ sel.removeAllRanges();
2919
+ sel.addRange(range);
2302
2920
 
2303
- this.hideAnchorPreview();
2921
+ node.previousElementSibling.parentNode.removeChild(node);
2304
2922
 
2305
- return this;
2923
+ e.preventDefault();
2924
+ }
2306
2925
  },
2307
2926
 
2308
- setToolbarButtonStates: function () {
2309
- this.commands.forEach(function (extension) {
2310
- if (typeof extension.isActive === 'function') {
2311
- extension.setInactive();
2312
- }
2313
- }.bind(this));
2314
- this.checkActiveButtons();
2927
+ initToolbar: function () {
2928
+ if (this.toolbar) {
2929
+ return this;
2930
+ }
2931
+ this.toolbar = new Toolbar(this);
2932
+ this.options.elementsContainer.appendChild(this.toolbar.getToolbarElement());
2933
+
2315
2934
  return this;
2316
2935
  },
2317
2936
 
2318
- checkActiveButtons: function () {
2319
- var elements = Array.prototype.slice.call(this.elements),
2320
- manualStateChecks = [],
2321
- queryState = null,
2322
- parentNode,
2323
- checkExtension = function (extension) {
2324
- if (typeof extension.checkState === 'function') {
2325
- extension.checkState(parentNode);
2326
- } else if (typeof extension.isActive === 'function' &&
2327
- typeof extension.isAlreadyApplied === 'function') {
2328
- if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
2329
- extension.setActive();
2330
- }
2331
- }
2332
- };
2937
+ bindDragDrop: function () {
2938
+ var self = this, i, className, onDrag, onDrop, element;
2333
2939
 
2334
- if (!this.selectionRange) {
2335
- return;
2940
+ if (!self.options.imageDragging) {
2941
+ return this;
2336
2942
  }
2337
- parentNode = Selection.getSelectedParentElement(this.selectionRange);
2338
2943
 
2339
- // Loop through all commands
2340
- this.commands.forEach(function (command) {
2341
- // For those commands where we can use document.queryCommandState(), do so
2342
- if (typeof command.queryCommandState === 'function') {
2343
- queryState = command.queryCommandState();
2344
- // If queryCommandState returns a valid value, we can trust the browser
2345
- // and don't need to do our manual checks
2346
- if (queryState !== null) {
2347
- if (queryState) {
2348
- command.setActive();
2944
+ className = 'medium-editor-dragover';
2945
+
2946
+ onDrag = function (e) {
2947
+ e.preventDefault();
2948
+ e.dataTransfer.dropEffect = "copy";
2949
+
2950
+ if (e.type === "dragover") {
2951
+ this.classList.add(className);
2952
+ } else {
2953
+ this.classList.remove(className);
2954
+ }
2955
+ };
2956
+
2957
+ onDrop = function (e) {
2958
+ var files;
2959
+ e.preventDefault();
2960
+ e.stopPropagation();
2961
+ // IE9 does not support the File API, so prevent file from opening in a new window
2962
+ // but also don't try to actually get the file
2963
+ if (e.dataTransfer.files) {
2964
+ files = Array.prototype.slice.call(e.dataTransfer.files, 0);
2965
+ files.some(function (file) {
2966
+ if (file.type.match("image")) {
2967
+ var fileReader, id;
2968
+ fileReader = new FileReader();
2969
+ fileReader.readAsDataURL(file);
2970
+
2971
+ id = 'medium-img-' + (+new Date());
2972
+ Util.insertHTMLCommand(self.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
2973
+
2974
+ fileReader.onload = function () {
2975
+ var img = document.getElementById(id);
2976
+ if (img) {
2977
+ img.removeAttribute('id');
2978
+ img.removeAttribute('class');
2979
+ img.src = fileReader.result;
2980
+ }
2981
+ };
2349
2982
  }
2350
- return;
2351
- }
2983
+ });
2352
2984
  }
2353
- // We can't use queryCommandState for this command, so add to manualStateChecks
2354
- manualStateChecks.push(command);
2355
- });
2985
+ this.classList.remove(className);
2986
+ };
2356
2987
 
2357
- // Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node
2358
- while (parentNode.tagName !== undefined && Util.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
2359
- manualStateChecks.forEach(checkExtension.bind(this));
2988
+ for (i = 0; i < this.elements.length; i += 1) {
2989
+ element = this.elements[i];
2360
2990
 
2361
- // we can abort the search upwards if we leave the contentEditable element
2362
- if (elements.indexOf(parentNode) !== -1) {
2363
- break;
2364
- }
2365
- parentNode = parentNode.parentNode;
2991
+ this.on(element, 'dragover', onDrag);
2992
+ this.on(element, 'dragleave', onDrag);
2993
+ this.on(element, 'drop', onDrop);
2366
2994
  }
2995
+ return this;
2367
2996
  },
2368
2997
 
2369
- setFirstAndLastButtons: function () {
2370
- var buttons = this.toolbar.querySelectorAll('button');
2371
- if (buttons.length > 0) {
2372
- buttons[0].className += ' ' + this.options.firstButtonClass;
2373
- buttons[buttons.length - 1].className += ' ' + this.options.lastButtonClass;
2998
+ stopSelectionUpdates: function () {
2999
+ this.preventSelectionUpdates = true;
3000
+ },
3001
+
3002
+ startSelectionUpdates: function () {
3003
+ this.preventSelectionUpdates = false;
3004
+ },
3005
+
3006
+ checkSelection: function () {
3007
+ if (this.toolbar) {
3008
+ this.toolbar.checkState();
2374
3009
  }
2375
3010
  return this;
2376
3011
  },
@@ -2447,8 +3082,11 @@ function MediumEditor(elements, options) {
2447
3082
  return this.options.ownerDocument.execCommand(action, false, null);
2448
3083
  },
2449
3084
 
2450
- getSelectedParentElement: function () {
2451
- return Selection.getSelectedParentElement();
3085
+ getSelectedParentElement: function (range) {
3086
+ if (range === undefined) {
3087
+ range = this.options.contentWindow.getSelection().getRangeAt(0);
3088
+ }
3089
+ return Selection.getSelectedParentElement(range);
2452
3090
  },
2453
3091
 
2454
3092
  execFormatBlock: function (el) {
@@ -2476,79 +3114,19 @@ function MediumEditor(elements, options) {
2476
3114
  return this.options.ownerDocument.execCommand('formatBlock', false, el);
2477
3115
  },
2478
3116
 
2479
- isToolbarDefaultActionsShown: function () {
2480
- return !!this.toolbarActions && this.toolbarActions.style.display === 'block';
2481
- },
2482
-
2483
3117
  hideToolbarDefaultActions: function () {
2484
- if (this.toolbarActions && this.isToolbarDefaultActionsShown()) {
2485
- this.commands.forEach(function (extension) {
2486
- if (extension.onHide && typeof extension.onHide === 'function') {
2487
- extension.onHide();
2488
- }
2489
- });
2490
- this.toolbarActions.style.display = 'none';
2491
- }
2492
- },
2493
-
2494
- showToolbarDefaultActions: function () {
2495
- this.hideExtensionForms();
2496
-
2497
- if (this.toolbarActions && !this.isToolbarDefaultActionsShown()) {
2498
- this.toolbarActions.style.display = 'block';
3118
+ if (this.toolbar) {
3119
+ this.toolbar.hideToolbarDefaultActions();
2499
3120
  }
2500
-
2501
- this.keepToolbarAlive = false;
2502
- // Using setTimeout + options.delay because:
2503
- // We will actually be displaying the toolbar, which should be controlled by options.delay
2504
- this.delay(function () {
2505
- this.showToolbar();
2506
- }.bind(this));
2507
-
2508
3121
  return this;
2509
3122
  },
2510
3123
 
2511
- hideExtensionForms: function () {
2512
- // Hide all extension forms
2513
- this.commands.forEach(function (extension) {
2514
- if (extension.hasForm && extension.isDisplayed()) {
2515
- extension.hideForm();
2516
- }
2517
- });
2518
- },
2519
-
2520
- isToolbarShown: function () {
2521
- return this.toolbar && this.toolbar.classList.contains('medium-editor-toolbar-active');
2522
- },
2523
-
2524
- showToolbar: function () {
2525
- if (this.toolbar && !this.isToolbarShown()) {
2526
- this.toolbar.classList.add('medium-editor-toolbar-active');
2527
- if (typeof this.options.onShowToolbar === 'function') {
2528
- this.options.onShowToolbar();
2529
- }
2530
- }
2531
- },
2532
-
2533
- hideToolbar: function () {
2534
- if (this.isToolbarShown()) {
2535
- this.toolbar.classList.remove('medium-editor-toolbar-active');
2536
- if (typeof this.options.onHideToolbar === 'function') {
2537
- this.options.onHideToolbar();
2538
- }
3124
+ setToolbarPosition: function () {
3125
+ if (this.toolbar) {
3126
+ this.toolbar.setToolbarPosition();
2539
3127
  }
2540
3128
  },
2541
3129
 
2542
- hideToolbarActions: function () {
2543
- this.commands.forEach(function (extension) {
2544
- if (extension.onHide && typeof extension.onHide === 'function') {
2545
- extension.onHide();
2546
- }
2547
- });
2548
- this.keepToolbarAlive = false;
2549
- this.hideToolbar();
2550
- },
2551
-
2552
3130
  selectAllContents: function () {
2553
3131
  var range = this.options.ownerDocument.createRange(),
2554
3132
  sel = this.options.contentWindow.getSelection(),
@@ -2656,181 +3234,6 @@ function MediumEditor(elements, options) {
2656
3234
  sel.addRange(range);
2657
3235
  },
2658
3236
 
2659
- hideAnchorPreview: function () {
2660
- this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
2661
- },
2662
-
2663
- // TODO: break method
2664
- showAnchorPreview: function (anchorEl) {
2665
- if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')
2666
- || anchorEl.getAttribute('data-disable-preview')) {
2667
- return true;
2668
- }
2669
-
2670
- var self = this,
2671
- buttonHeight = 40,
2672
- boundary = anchorEl.getBoundingClientRect(),
2673
- middleBoundary = (boundary.left + boundary.right) / 2,
2674
- halfOffsetWidth,
2675
- defaultLeft;
2676
-
2677
- self.anchorPreview.querySelector('i').textContent = anchorEl.attributes.href.value;
2678
- halfOffsetWidth = self.anchorPreview.offsetWidth / 2;
2679
- defaultLeft = self.options.diffLeft - halfOffsetWidth;
2680
-
2681
- self.observeAnchorPreview(anchorEl);
2682
-
2683
- self.anchorPreview.classList.add('medium-toolbar-arrow-over');
2684
- self.anchorPreview.classList.remove('medium-toolbar-arrow-under');
2685
- self.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - self.options.diffTop + this.options.contentWindow.pageYOffset - self.anchorPreview.offsetHeight) + 'px';
2686
- if (middleBoundary < halfOffsetWidth) {
2687
- self.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
2688
- } else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
2689
- self.anchorPreview.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
2690
- } else {
2691
- self.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
2692
- }
2693
-
2694
- if (this.anchorPreview && !this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
2695
- this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
2696
- }
2697
-
2698
- return this;
2699
- },
2700
-
2701
- // TODO: break method
2702
- observeAnchorPreview: function (anchorEl) {
2703
- var self = this,
2704
- lastOver = (new Date()).getTime(),
2705
- over = true,
2706
- stamp = function () {
2707
- lastOver = (new Date()).getTime();
2708
- over = true;
2709
- },
2710
- unstamp = function (e) {
2711
- if (!e.relatedTarget || !/anchor-preview/.test(e.relatedTarget.className)) {
2712
- over = false;
2713
- }
2714
- },
2715
- interval_timer = setInterval(function () {
2716
- if (over) {
2717
- return true;
2718
- }
2719
- var durr = (new Date()).getTime() - lastOver;
2720
- if (durr > self.options.anchorPreviewHideDelay) {
2721
- // hide the preview 1/2 second after mouse leaves the link
2722
- self.hideAnchorPreview();
2723
-
2724
- // cleanup
2725
- clearInterval(interval_timer);
2726
- self.off(self.anchorPreview, 'mouseover', stamp);
2727
- self.off(self.anchorPreview, 'mouseout', unstamp);
2728
- self.off(anchorEl, 'mouseover', stamp);
2729
- self.off(anchorEl, 'mouseout', unstamp);
2730
-
2731
- }
2732
- }, 200);
2733
-
2734
- this.on(self.anchorPreview, 'mouseover', stamp);
2735
- this.on(self.anchorPreview, 'mouseout', unstamp);
2736
- this.on(anchorEl, 'mouseover', stamp);
2737
- this.on(anchorEl, 'mouseout', unstamp);
2738
- },
2739
-
2740
- createAnchorPreview: function () {
2741
- var self = this,
2742
- anchorPreview = this.options.ownerDocument.createElement('div');
2743
-
2744
- anchorPreview.id = 'medium-editor-anchor-preview-' + this.id;
2745
- anchorPreview.className = 'medium-editor-anchor-preview';
2746
- anchorPreview.innerHTML = this.anchorPreviewTemplate();
2747
- this.options.elementsContainer.appendChild(anchorPreview);
2748
-
2749
- this.on(anchorPreview, 'click', function () {
2750
- self.anchorPreviewClickHandler();
2751
- });
2752
-
2753
- return anchorPreview;
2754
- },
2755
-
2756
- anchorPreviewTemplate: function () {
2757
- return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
2758
- ' <i class="medium-editor-toolbar-anchor-preview-inner"></i>' +
2759
- '</div>';
2760
- },
2761
-
2762
- anchorPreviewClickHandler: function (event) {
2763
- var range,
2764
- sel,
2765
- anchorExtension = this.getExtensionByName('anchor');
2766
-
2767
- if (anchorExtension && this.activeAnchor) {
2768
- range = this.options.ownerDocument.createRange();
2769
- range.selectNodeContents(this.activeAnchor);
2770
-
2771
- sel = this.options.contentWindow.getSelection();
2772
- sel.removeAllRanges();
2773
- sel.addRange(range);
2774
- // Using setTimeout + options.delay because:
2775
- // We may actually be displaying the anchor form, which should be controlled by options.delay
2776
- this.delay(function () {
2777
- if (this.activeAnchor) {
2778
- anchorExtension.showForm(this.activeAnchor.attributes.href.value);
2779
- }
2780
- this.keepToolbarAlive = false;
2781
- }.bind(this));
2782
- }
2783
-
2784
- this.hideAnchorPreview();
2785
- },
2786
-
2787
- editorAnchorObserver: function (e) {
2788
- var self = this,
2789
- overAnchor = true,
2790
- leaveAnchor = function () {
2791
- // mark the anchor as no longer hovered, and stop listening
2792
- overAnchor = false;
2793
- self.off(self.activeAnchor, 'mouseout', leaveAnchor);
2794
- };
2795
-
2796
- if (e.target && e.target.tagName.toLowerCase() === 'a') {
2797
-
2798
- // Detect empty href attributes
2799
- // The browser will make href="" or href="#top"
2800
- // into absolute urls when accessed as e.targed.href, so check the html
2801
- if (!/href=["']\S+["']/.test(e.target.outerHTML) || /href=["']#\S+["']/.test(e.target.outerHTML)) {
2802
- return true;
2803
- }
2804
-
2805
- // only show when hovering on anchors
2806
- if (this.isToolbarShown()) {
2807
- // only show when toolbar is not present
2808
- return true;
2809
- }
2810
- this.activeAnchor = e.target;
2811
- this.on(this.activeAnchor, 'mouseout', leaveAnchor);
2812
- // Using setTimeout + options.delay because:
2813
- // - We're going to show the anchor preview according to the configured delay
2814
- // if the mouse has not left the anchor tag in that time
2815
- this.delay(function () {
2816
- if (overAnchor) {
2817
- self.showAnchorPreview(e.target);
2818
- }
2819
- });
2820
- }
2821
- },
2822
-
2823
- bindAnchorPreview: function (index) {
2824
- var i, self = this;
2825
- this.editorAnchorObserverWrapper = function (e) {
2826
- self.editorAnchorObserver(e);
2827
- };
2828
- for (i = 0; i < this.elements.length; i += 1) {
2829
- this.on(this.elements[i], 'mouseover', this.editorAnchorObserverWrapper);
2830
- }
2831
- return this;
2832
- },
2833
-
2834
3237
  createLink: function (opts) {
2835
3238
  var customEvent,
2836
3239
  i;
@@ -2875,32 +3278,6 @@ function MediumEditor(elements, options) {
2875
3278
  }
2876
3279
  },
2877
3280
 
2878
- positionToolbarIfShown: function () {
2879
- if (this.isToolbarShown()) {
2880
- this.setToolbarPosition();
2881
- }
2882
- },
2883
-
2884
- bindWindowActions: function () {
2885
- var self = this;
2886
-
2887
- // Add a scroll event for sticky toolbar
2888
- if (this.options.staticToolbar && this.options.stickyToolbar) {
2889
- // On scroll, re-position the toolbar
2890
- this.on(this.options.contentWindow, 'scroll', function () {
2891
- self.positionToolbarIfShown();
2892
- }, true);
2893
- }
2894
-
2895
- this.on(this.options.contentWindow, 'resize', function () {
2896
- self.handleResize();
2897
- });
2898
-
2899
- this.bindBlur();
2900
-
2901
- return this;
2902
- },
2903
-
2904
3281
  activate: function () {
2905
3282
  if (this.isActive) {
2906
3283
  return;
@@ -2918,10 +3295,8 @@ function MediumEditor(elements, options) {
2918
3295
  this.isActive = false;
2919
3296
 
2920
3297
  if (this.toolbar !== undefined) {
2921
- this.options.elementsContainer.removeChild(this.anchorPreview);
2922
- this.options.elementsContainer.removeChild(this.toolbar);
3298
+ this.toolbar.deactivate();
2923
3299
  delete this.toolbar;
2924
- delete this.anchorPreview;
2925
3300
  }
2926
3301
 
2927
3302
  for (i = 0; i < this.elements.length; i += 1) {