medium-editor-rails 2.0.7 → 2.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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) {