medium-editor-rails 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7dde5397c49e9ca83dbc11bf66e79b5b4345b057
4
- data.tar.gz: 4488ea4ce630bbc1d36ed1619575901c8320ac7f
3
+ metadata.gz: 0241fca73542f5f5867edc588778dbcb10ad3f28
4
+ data.tar.gz: 842d1ca2ac98a5c6a45336d1f35816d6017d090a
5
5
  SHA512:
6
- metadata.gz: d8a38f3ba2fccfee0bb0ff99c535b79fe7b424d6500cba782a6620d437ad9bfb1b3b75a355d91606a9f83a8a9e0dbe3c6f627288669a81fb87b69d8bec2cc064
7
- data.tar.gz: 155235c9e6b70b6ef7ff2138f29fda2fc98146b3dc66bae2904bc3a0f373cb44697a8dcb5313201fc6dd7e2e55f1ccef34bf71f52b17b3814c274dd50bf29850
6
+ metadata.gz: e7a183c756bba1a1ac215348813f707b15e748cf94d8476fd214540dec9fdeefd89e98b8619f866845f1b22dd2368223d0bd1620cf32d758e843d06090f287cd
7
+ data.tar.gz: 4f03baf7dc8dbc0e08f5aa6e65273df20d0122160101901e7c34694913c482517b5599fae11ab9cbdb5f55e76646bdc09df89e3a28ba0ef02e670564a01e9463
data/README.md CHANGED
@@ -8,7 +8,7 @@ This gem integrates [Medium Editor](https://github.com/yabwe/medium-editor) with
8
8
 
9
9
  ## Version
10
10
 
11
- The latest version of Medium Editor bundled by this gem is [5.14.4](https://github.com/yabwe/medium-editor/releases)
11
+ The latest version of Medium Editor bundled by this gem is [5.15.0](https://github.com/yabwe/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.2.0'
4
- MEDIUM_EDITOR_VERSION = '5.15.0'
3
+ VERSION = '2.3.0'
4
+ MEDIUM_EDITOR_VERSION = '5.22.0'
5
5
  end
6
6
  end
@@ -385,7 +385,8 @@ if (!("classList" in document.createElement("_"))) {
385
385
 
386
386
  (function (root, factory) {
387
387
  'use strict';
388
- if (typeof module === 'object') {
388
+ var isElectron = typeof module === 'object' && process && process.versions && process.versions.electron;
389
+ if (!isElectron && typeof module === 'object') {
389
390
  module.exports = factory;
390
391
  } else if (typeof define === 'function' && define.amd) {
391
392
  define(function () {
@@ -454,6 +455,7 @@ MediumEditor.extensions = {};
454
455
  isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),
455
456
 
456
457
  // https://github.com/jashkenas/underscore
458
+ // Lonely letter MUST USE the uppercase code
457
459
  keyCode: {
458
460
  BACKSPACE: 8,
459
461
  TAB: 9,
@@ -462,7 +464,8 @@ MediumEditor.extensions = {};
462
464
  SPACE: 32,
463
465
  DELETE: 46,
464
466
  K: 75, // K keycode, and not k
465
- M: 77
467
+ M: 77,
468
+ V: 86
466
469
  },
467
470
 
468
471
  /**
@@ -897,8 +900,7 @@ MediumEditor.extensions = {};
897
900
  range = range.cloneRange();
898
901
  range.setStartAfter(lastNode);
899
902
  range.collapse(true);
900
- selection.removeAllRanges();
901
- selection.addRange(range);
903
+ MediumEditor.selection.selectRange(doc, range);
902
904
  }
903
905
  res = true;
904
906
  }
@@ -1428,11 +1430,15 @@ MediumEditor.extensions = {};
1428
1430
  },
1429
1431
 
1430
1432
  cleanupTags: function (el, tags) {
1431
- tags.forEach(function (tag) {
1432
- if (el.nodeName.toLowerCase() === tag) {
1433
- el.parentNode.removeChild(el);
1434
- }
1435
- });
1433
+ if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
1434
+ el.parentNode.removeChild(el);
1435
+ }
1436
+ },
1437
+
1438
+ unwrapTags: function (el, tags) {
1439
+ if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
1440
+ MediumEditor.util.unwrap(el, document);
1441
+ }
1436
1442
  },
1437
1443
 
1438
1444
  // get the closest parent
@@ -1457,6 +1463,17 @@ MediumEditor.extensions = {};
1457
1463
  } else {
1458
1464
  el.parentNode.removeChild(el);
1459
1465
  }
1466
+ },
1467
+
1468
+ guid: function () {
1469
+ function _s4() {
1470
+ return Math
1471
+ .floor((1 + Math.random()) * 0x10000)
1472
+ .toString(16)
1473
+ .substring(1);
1474
+ }
1475
+
1476
+ return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4();
1460
1477
  }
1461
1478
  };
1462
1479
 
@@ -1641,6 +1658,19 @@ MediumEditor.extensions = {};
1641
1658
  */
1642
1659
  setInactive: undefined,
1643
1660
 
1661
+ /* getInteractionElements: [function ()]
1662
+ *
1663
+ * If the extension renders any elements that the user can interact with,
1664
+ * this method should be implemented and return the root element or an array
1665
+ * containing all of the root elements. MediumEditor will call this function
1666
+ * during interaction to see if the user clicked on something outside of the editor.
1667
+ * The elements are used to check if the target element of a click or
1668
+ * other user event is a descendant of any extension elements.
1669
+ * This way, the editor can also count user interaction within editor elements as
1670
+ * interactions with the editor, and thus not trigger 'blur'
1671
+ */
1672
+ getInteractionElements: undefined,
1673
+
1644
1674
  /************************ Helpers ************************
1645
1675
  * The following are helpers that are either set by MediumEditor
1646
1676
  * during initialization, or are helper methods which either
@@ -1938,9 +1968,7 @@ MediumEditor.extensions = {};
1938
1968
  range = this.importSelectionMoveCursorPastAnchor(selectionState, range);
1939
1969
  }
1940
1970
 
1941
- var sel = doc.getSelection();
1942
- sel.removeAllRanges();
1943
- sel.addRange(range);
1971
+ this.selectRange(doc, range);
1944
1972
  },
1945
1973
 
1946
1974
  // Utility method called from importSelection only
@@ -2333,16 +2361,12 @@ MediumEditor.extensions = {};
2333
2361
  },
2334
2362
 
2335
2363
  selectNode: function (node, doc) {
2336
- var range = doc.createRange(),
2337
- sel = doc.getSelection();
2338
-
2364
+ var range = doc.createRange();
2339
2365
  range.selectNodeContents(node);
2340
- sel.removeAllRanges();
2341
- sel.addRange(range);
2366
+ this.selectRange(doc, range);
2342
2367
  },
2343
2368
 
2344
2369
  select: function (doc, startNode, startOffset, endNode, endOffset) {
2345
- doc.getSelection().removeAllRanges();
2346
2370
  var range = doc.createRange();
2347
2371
  range.setStart(startNode, startOffset);
2348
2372
  if (endNode) {
@@ -2350,7 +2374,7 @@ MediumEditor.extensions = {};
2350
2374
  } else {
2351
2375
  range.collapse(true);
2352
2376
  }
2353
- doc.getSelection().addRange(range);
2377
+ this.selectRange(doc, range);
2354
2378
  return range;
2355
2379
  },
2356
2380
 
@@ -2387,6 +2411,13 @@ MediumEditor.extensions = {};
2387
2411
  return selection.getRangeAt(0);
2388
2412
  },
2389
2413
 
2414
+ selectRange: function (ownerDocument, range) {
2415
+ var selection = ownerDocument.getSelection();
2416
+
2417
+ selection.removeAllRanges();
2418
+ selection.addRange(range);
2419
+ },
2420
+
2390
2421
  // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
2391
2422
  // by You
2392
2423
  getSelectionStart: function (ownerDocument) {
@@ -2403,6 +2434,26 @@ MediumEditor.extensions = {};
2403
2434
  (function () {
2404
2435
  'use strict';
2405
2436
 
2437
+ function isElementDescendantOfExtension(extensions, element) {
2438
+ return extensions.some(function (extension) {
2439
+ if (typeof extension.getInteractionElements !== 'function') {
2440
+ return false;
2441
+ }
2442
+
2443
+ var extensionElements = extension.getInteractionElements();
2444
+ if (!extensionElements) {
2445
+ return false;
2446
+ }
2447
+
2448
+ if (!Array.isArray(extensionElements)) {
2449
+ extensionElements = [extensionElements];
2450
+ }
2451
+ return extensionElements.some(function (el) {
2452
+ return MediumEditor.util.isDescendant(el, element, true);
2453
+ });
2454
+ });
2455
+ }
2456
+
2406
2457
  var Events = function (instance) {
2407
2458
  this.base = instance;
2408
2459
  this.options = this.base.options;
@@ -2417,18 +2468,32 @@ MediumEditor.extensions = {};
2417
2468
 
2418
2469
  // Helpers for event handling
2419
2470
 
2420
- attachDOMEvent: function (target, event, listener, useCapture) {
2421
- target.addEventListener(event, listener, useCapture);
2422
- this.events.push([target, event, listener, useCapture]);
2471
+ attachDOMEvent: function (targets, event, listener, useCapture) {
2472
+ var win = this.base.options.contentWindow,
2473
+ doc = this.base.options.ownerDocument;
2474
+
2475
+ targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;
2476
+
2477
+ Array.prototype.forEach.call(targets, function (target) {
2478
+ target.addEventListener(event, listener, useCapture);
2479
+ this.events.push([target, event, listener, useCapture]);
2480
+ }.bind(this));
2423
2481
  },
2424
2482
 
2425
- detachDOMEvent: function (target, event, listener, useCapture) {
2426
- var index = this.indexOfListener(target, event, listener, useCapture),
2427
- e;
2428
- if (index !== -1) {
2429
- e = this.events.splice(index, 1)[0];
2430
- e[0].removeEventListener(e[1], e[2], e[3]);
2431
- }
2483
+ detachDOMEvent: function (targets, event, listener, useCapture) {
2484
+ var index, e,
2485
+ win = this.base.options.contentWindow,
2486
+ doc = this.base.options.ownerDocument;
2487
+
2488
+ targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;
2489
+
2490
+ Array.prototype.forEach.call(targets, function (target) {
2491
+ index = this.indexOfListener(target, event, listener, useCapture);
2492
+ if (index !== -1) {
2493
+ e = this.events.splice(index, 1)[0];
2494
+ e[0].removeEventListener(e[1], e[2], e[3]);
2495
+ }
2496
+ }.bind(this));
2432
2497
  },
2433
2498
 
2434
2499
  indexOfListener: function (target, event, listener, useCapture) {
@@ -2450,6 +2515,30 @@ MediumEditor.extensions = {};
2450
2515
  }
2451
2516
  },
2452
2517
 
2518
+ detachAllEventsFromElement: function (element) {
2519
+ var filtered = this.events.filter(function (e) {
2520
+ return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index');
2521
+ });
2522
+
2523
+ for (var i = 0, len = filtered.length; i < len; i++) {
2524
+ var e = filtered[i];
2525
+ this.detachDOMEvent(e[0], e[1], e[2], e[3]);
2526
+ }
2527
+ },
2528
+
2529
+ // Attach all existing handlers to a new element
2530
+ attachAllEventsToElement: function (element) {
2531
+ if (this.listeners['editableInput']) {
2532
+ this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
2533
+ }
2534
+
2535
+ if (this.eventsCache) {
2536
+ this.eventsCache.forEach(function (e) {
2537
+ this.attachDOMEvent(element, e['name'], e['handler'].bind(this));
2538
+ }, this);
2539
+ }
2540
+ },
2541
+
2453
2542
  enableCustomEvent: function (event) {
2454
2543
  if (this.disabledEvents[event] !== undefined) {
2455
2544
  delete this.disabledEvents[event];
@@ -2564,23 +2653,23 @@ MediumEditor.extensions = {};
2564
2653
 
2565
2654
  // Helper method to call all listeners to execCommand
2566
2655
  var callListeners = function (args, result) {
2567
- if (doc.execCommand.listeners) {
2568
- doc.execCommand.listeners.forEach(function (listener) {
2569
- listener({
2570
- command: args[0],
2571
- value: args[2],
2572
- args: args,
2573
- result: result
2574
- });
2656
+ if (doc.execCommand.listeners) {
2657
+ doc.execCommand.listeners.forEach(function (listener) {
2658
+ listener({
2659
+ command: args[0],
2660
+ value: args[2],
2661
+ args: args,
2662
+ result: result
2575
2663
  });
2576
- }
2577
- },
2664
+ });
2665
+ }
2666
+ },
2578
2667
 
2579
- // Create a wrapper method for execCommand which will:
2580
- // 1) Call document.execCommand with the correct arguments
2581
- // 2) Loop through any listeners and notify them that execCommand was called
2582
- // passing extra info on the call
2583
- // 3) Return the result
2668
+ // Create a wrapper method for execCommand which will:
2669
+ // 1) Call document.execCommand with the correct arguments
2670
+ // 2) Loop through any listeners and notify them that execCommand was called
2671
+ // passing extra info on the call
2672
+ // 3) Return the result
2584
2673
  wrapper = function () {
2585
2674
  var result = doc.execCommand.orig.apply(this, arguments);
2586
2675
 
@@ -2641,15 +2730,15 @@ MediumEditor.extensions = {};
2641
2730
  break;
2642
2731
  case 'editableInput':
2643
2732
  // setup cache for knowing when the content has changed
2644
- this.contentCache = [];
2733
+ this.contentCache = {};
2645
2734
  this.base.elements.forEach(function (element) {
2646
2735
  this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
2736
+ }, this);
2647
2737
 
2648
- // Attach to the 'oninput' event, handled correctly by most browsers
2649
- if (this.InputEventOnContenteditableSupported) {
2650
- this.attachDOMEvent(element, 'input', this.handleInput.bind(this));
2651
- }
2652
- }.bind(this));
2738
+ // Attach to the 'oninput' event, handled correctly by most browsers
2739
+ if (this.InputEventOnContenteditableSupported) {
2740
+ this.attachToEachElement('input', this.handleInput);
2741
+ }
2653
2742
 
2654
2743
  // For browsers which don't support the input event on contenteditable (IE)
2655
2744
  // we'll attach to 'selectionchange' on the document and 'keypress' on the editables
@@ -2710,6 +2799,8 @@ MediumEditor.extensions = {};
2710
2799
  // Detecting drop on the contenteditables
2711
2800
  this.attachToEachElement('drop', this.handleDrop);
2712
2801
  break;
2802
+ // TODO: We need to have a custom 'paste' event separate from 'editablePaste'
2803
+ // Need to think about the way to introduce this without breaking folks
2713
2804
  case 'editablePaste':
2714
2805
  // Detecting paste on the contenteditables
2715
2806
  this.attachToEachElement('paste', this.handlePaste);
@@ -2719,9 +2810,26 @@ MediumEditor.extensions = {};
2719
2810
  },
2720
2811
 
2721
2812
  attachToEachElement: function (name, handler) {
2813
+ // build our internal cache to know which element got already what handler attached
2814
+ if (!this.eventsCache) {
2815
+ this.eventsCache = [];
2816
+ }
2817
+
2722
2818
  this.base.elements.forEach(function (element) {
2723
2819
  this.attachDOMEvent(element, name, handler.bind(this));
2724
2820
  }, this);
2821
+
2822
+ this.eventsCache.push({ 'name': name, 'handler': handler });
2823
+ },
2824
+
2825
+ cleanupElement: function (element) {
2826
+ var index = element.getAttribute('medium-editor-index');
2827
+ if (index) {
2828
+ this.detachAllEventsFromElement(element);
2829
+ if (this.contentCache) {
2830
+ delete this.contentCache[index];
2831
+ }
2832
+ }
2725
2833
  },
2726
2834
 
2727
2835
  focusElement: function (element) {
@@ -2730,21 +2838,16 @@ MediumEditor.extensions = {};
2730
2838
  },
2731
2839
 
2732
2840
  updateFocus: function (target, eventObj) {
2733
- var toolbar = this.base.getExtensionByName('toolbar'),
2734
- toolbarEl = toolbar ? toolbar.getToolbarElement() : null,
2735
- anchorPreview = this.base.getExtensionByName('anchor-preview'),
2736
- previewEl = (anchorPreview && anchorPreview.getPreviewElement) ? anchorPreview.getPreviewElement() : null,
2737
- hadFocus = this.base.getFocusedElement(),
2841
+ var hadFocus = this.base.getFocusedElement(),
2738
2842
  toFocus;
2739
2843
 
2740
- // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element.
2741
- // If so, we don't want to focus another element
2844
+ // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element
2845
+ // or one of the extension elements. If so, we don't want to focus another element
2742
2846
  if (hadFocus &&
2743
- eventObj.type === 'click' &&
2744
- this.lastMousedownTarget &&
2745
- (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
2746
- MediumEditor.util.isDescendant(toolbarEl, this.lastMousedownTarget, true) ||
2747
- MediumEditor.util.isDescendant(previewEl, this.lastMousedownTarget, true))) {
2847
+ eventObj.type === 'click' &&
2848
+ this.lastMousedownTarget &&
2849
+ (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
2850
+ isElementDescendantOfExtension(this.base.extensions, this.lastMousedownTarget))) {
2748
2851
  toFocus = hadFocus;
2749
2852
  }
2750
2853
 
@@ -2760,10 +2863,9 @@ MediumEditor.extensions = {};
2760
2863
  }, this);
2761
2864
  }
2762
2865
 
2763
- // Check if the target is external (not part of the editor, toolbar, or anchorpreview)
2866
+ // Check if the target is external (not part of the editor, toolbar, or any other extension)
2764
2867
  var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) &&
2765
- !MediumEditor.util.isDescendant(toolbarEl, target, true) &&
2766
- !MediumEditor.util.isDescendant(previewEl, target, true);
2868
+ !isElementDescendantOfExtension(this.base.extensions, target);
2767
2869
 
2768
2870
  if (toFocus !== hadFocus) {
2769
2871
  // If element has focus, and focus is going outside of editor
@@ -2793,12 +2895,14 @@ MediumEditor.extensions = {};
2793
2895
  }
2794
2896
  // An event triggered which signifies that the user may have changed someting
2795
2897
  // Look in our cache of input for the contenteditables to see if something changed
2796
- var index = target.getAttribute('medium-editor-index');
2797
- if (target.innerHTML !== this.contentCache[index]) {
2898
+ var index = target.getAttribute('medium-editor-index'),
2899
+ html = target.innerHTML;
2900
+
2901
+ if (html !== this.contentCache[index]) {
2798
2902
  // The content has changed since the last time we checked, fire the event
2799
2903
  this.triggerCustomEvent('editableInput', eventObj, target);
2800
2904
  }
2801
- this.contentCache[index] = target.innerHTML;
2905
+ this.contentCache[index] = html;
2802
2906
  },
2803
2907
 
2804
2908
  handleDocumentSelectionChange: function (event) {
@@ -3679,12 +3783,12 @@ MediumEditor.extensions = {};
3679
3783
  targetCheckbox = this.getAnchorTargetCheckbox(),
3680
3784
  buttonCheckbox = this.getAnchorButtonCheckbox();
3681
3785
 
3682
- opts = opts || { url: '' };
3786
+ opts = opts || { value: '' };
3683
3787
  // TODO: This is for backwards compatability
3684
3788
  // We don't need to support the 'string' argument in 6.0.0
3685
3789
  if (typeof opts === 'string') {
3686
3790
  opts = {
3687
- url: opts
3791
+ value: opts
3688
3792
  };
3689
3793
  }
3690
3794
 
@@ -3693,7 +3797,7 @@ MediumEditor.extensions = {};
3693
3797
  MediumEditor.extensions.form.prototype.showForm.apply(this);
3694
3798
  this.setToolbarPosition();
3695
3799
 
3696
- input.value = opts.url;
3800
+ input.value = opts.value;
3697
3801
  input.focus();
3698
3802
 
3699
3803
  // If we have a target checkbox, we want it to be checked/unchecked
@@ -3730,11 +3834,11 @@ MediumEditor.extensions = {};
3730
3834
  var targetCheckbox = this.getAnchorTargetCheckbox(),
3731
3835
  buttonCheckbox = this.getAnchorButtonCheckbox(),
3732
3836
  opts = {
3733
- url: this.getInput().value.trim()
3837
+ value: this.getInput().value.trim()
3734
3838
  };
3735
3839
 
3736
3840
  if (this.linkValidation) {
3737
- opts.url = this.checkLinkFormat(opts.url);
3841
+ opts.value = this.checkLinkFormat(opts.value);
3738
3842
  }
3739
3843
 
3740
3844
  opts.target = '_self';
@@ -3764,14 +3868,15 @@ MediumEditor.extensions = {};
3764
3868
  // Matches any alphabetical characters followed by ://
3765
3869
  // Matches protocol relative "//"
3766
3870
  // Matches common external protocols "mailto:" "tel:" "maps:"
3767
- var urlSchemeRegex = /^([a-z]+:)?\/\/|^(mailto|tel|maps):/i,
3871
+ // Matches relative hash link, begins with "#"
3872
+ var urlSchemeRegex = /^([a-z]+:)?\/\/|^(mailto|tel|maps):|^\#/i,
3768
3873
  // var te is a regex for checking if the string is a telephone number
3769
3874
  telRegex = /^\+?\s?\(?(?:\d\s?\-?\)?){3,20}$/;
3770
3875
  if (telRegex.test(value)) {
3771
3876
  return 'tel:' + value;
3772
3877
  } else {
3773
3878
  // Check for URL scheme and default to http:// if none found
3774
- return (urlSchemeRegex.test(value) ? '' : 'http://') + value;
3879
+ return (urlSchemeRegex.test(value) ? '' : 'http://') + encodeURI(value);
3775
3880
  }
3776
3881
  },
3777
3882
 
@@ -3884,6 +3989,11 @@ MediumEditor.extensions = {};
3884
3989
  */
3885
3990
  showWhenToolbarIsVisible: false,
3886
3991
 
3992
+ /* showOnEmptyLinks: [boolean]
3993
+ * determines whether the anchor tag preview shows up on links with href="" or href="#something"
3994
+ */
3995
+ showOnEmptyLinks: true,
3996
+
3887
3997
  init: function () {
3888
3998
  this.anchorPreview = this.createPreview();
3889
3999
 
@@ -3892,6 +4002,11 @@ MediumEditor.extensions = {};
3892
4002
  this.attachToEditables();
3893
4003
  },
3894
4004
 
4005
+ getInteractionElements: function () {
4006
+ return this.getPreviewElement();
4007
+ },
4008
+
4009
+ // TODO: Remove this function in 6.0.0
3895
4010
  getPreviewElement: function () {
3896
4011
  return this.anchorPreview;
3897
4012
  },
@@ -3956,13 +4071,15 @@ MediumEditor.extensions = {};
3956
4071
 
3957
4072
  positionPreview: function (activeAnchor) {
3958
4073
  activeAnchor = activeAnchor || this.activeAnchor;
3959
- var buttonHeight = this.anchorPreview.offsetHeight,
4074
+ var containerWidth = this.window.innerWidth,
4075
+ buttonHeight = this.anchorPreview.offsetHeight,
3960
4076
  boundary = activeAnchor.getBoundingClientRect(),
3961
- middleBoundary = (boundary.left + boundary.right) / 2,
3962
4077
  diffLeft = this.diffLeft,
3963
4078
  diffTop = this.diffTop,
3964
- halfOffsetWidth,
3965
- defaultLeft;
4079
+ elementsContainer = this.getEditorOption('elementsContainer'),
4080
+ elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
4081
+ relativeBoundary = {},
4082
+ halfOffsetWidth, defaultLeft, middleBoundary, elementsContainerBoundary, top;
3966
4083
 
3967
4084
  halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
3968
4085
  var toolbarExtension = this.base.getExtensionByName('toolbar');
@@ -3972,12 +4089,35 @@ MediumEditor.extensions = {};
3972
4089
  }
3973
4090
  defaultLeft = diffLeft - halfOffsetWidth;
3974
4091
 
3975
- this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - diffTop + this.window.pageYOffset - this.anchorPreview.offsetHeight) + 'px';
4092
+ // If container element is absolute / fixed, recalculate boundaries to be relative to the container
4093
+ if (elementsContainerAbsolute) {
4094
+ elementsContainerBoundary = elementsContainer.getBoundingClientRect();
4095
+ ['top', 'left'].forEach(function (key) {
4096
+ relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
4097
+ });
4098
+
4099
+ relativeBoundary.width = boundary.width;
4100
+ relativeBoundary.height = boundary.height;
4101
+ boundary = relativeBoundary;
4102
+
4103
+ containerWidth = elementsContainerBoundary.width;
4104
+
4105
+ // Adjust top position according to container scroll position
4106
+ top = elementsContainer.scrollTop;
4107
+ } else {
4108
+ // Adjust top position according to window scroll position
4109
+ top = this.window.pageYOffset;
4110
+ }
4111
+
4112
+ middleBoundary = boundary.left + boundary.width / 2;
4113
+ top += buttonHeight + boundary.top + boundary.height - diffTop - this.anchorPreview.offsetHeight;
4114
+
4115
+ this.anchorPreview.style.top = Math.round(top) + 'px';
3976
4116
  this.anchorPreview.style.right = 'initial';
3977
4117
  if (middleBoundary < halfOffsetWidth) {
3978
4118
  this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
3979
4119
  this.anchorPreview.style.right = 'initial';
3980
- } else if ((this.window.innerWidth - middleBoundary) < halfOffsetWidth) {
4120
+ } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
3981
4121
  this.anchorPreview.style.left = 'auto';
3982
4122
  this.anchorPreview.style.right = 0;
3983
4123
  } else {
@@ -3988,6 +4128,15 @@ MediumEditor.extensions = {};
3988
4128
 
3989
4129
  attachToEditables: function () {
3990
4130
  this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
4131
+ this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this));
4132
+ },
4133
+
4134
+ handlePositionedToolbar: function () {
4135
+ // If the toolbar is visible and positioned, we don't need to hide the preview
4136
+ // when showWhenToolbarIsVisible is true
4137
+ if (!this.showWhenToolbarIsVisible) {
4138
+ this.hidePreview();
4139
+ }
3991
4140
  },
3992
4141
 
3993
4142
  handleClick: function (event) {
@@ -4004,7 +4153,7 @@ MediumEditor.extensions = {};
4004
4153
  this.base.delay(function () {
4005
4154
  if (activeAnchor) {
4006
4155
  var opts = {
4007
- url: activeAnchor.attributes.href.value,
4156
+ value: activeAnchor.attributes.href.value,
4008
4157
  target: activeAnchor.getAttribute('target'),
4009
4158
  buttonClass: activeAnchor.getAttribute('class')
4010
4159
  };
@@ -4033,7 +4182,8 @@ MediumEditor.extensions = {};
4033
4182
  // Detect empty href attributes
4034
4183
  // The browser will make href="" or href="#top"
4035
4184
  // into absolute urls when accessed as event.target.href, so check the html
4036
- if (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML)) {
4185
+ if (!this.showOnEmptyLinks &&
4186
+ (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML))) {
4037
4187
  return true;
4038
4188
  }
4039
4189
 
@@ -4537,8 +4687,12 @@ MediumEditor.extensions = {};
4537
4687
  event.preventDefault();
4538
4688
  event.stopPropagation();
4539
4689
 
4690
+ // command can be a function to execute
4691
+ if (typeof data.command === 'function') {
4692
+ data.command.apply(this);
4693
+ }
4540
4694
  // command can be false so the shortcut is just disabled
4541
- if (false !== data.command) {
4695
+ else if (false !== data.command) {
4542
4696
  this.execAction(data.command);
4543
4697
  }
4544
4698
  }
@@ -4709,7 +4863,7 @@ MediumEditor.extensions = {};
4709
4863
  if (font === '') {
4710
4864
  this.clearFontName();
4711
4865
  } else {
4712
- this.execAction('fontName', { name: font });
4866
+ this.execAction('fontName', { value: font });
4713
4867
  }
4714
4868
  },
4715
4869
 
@@ -4887,7 +5041,7 @@ MediumEditor.extensions = {};
4887
5041
  if (size === '4') {
4888
5042
  this.clearFontSize();
4889
5043
  } else {
4890
- this.execAction('fontSize', { size: size });
5044
+ this.execAction('fontSize', { value: size });
4891
5045
  }
4892
5046
  },
4893
5047
 
@@ -4913,6 +5067,16 @@ MediumEditor.extensions = {};
4913
5067
  }());
4914
5068
  (function () {
4915
5069
  'use strict';
5070
+
5071
+ /* Helpers and internal variables that don't need to be members of actual paste handler */
5072
+
5073
+ var pasteBinDefaultContent = '%ME_PASTEBIN%',
5074
+ lastRange = null,
5075
+ keyboardPasteEditable = null,
5076
+ stopProp = function (event) {
5077
+ event.stopPropagation();
5078
+ };
5079
+
4916
5080
  /*jslint regexp: true*/
4917
5081
  /*
4918
5082
  jslint does not allow character negation, because the negation
@@ -4922,6 +5086,15 @@ MediumEditor.extensions = {};
4922
5086
  */
4923
5087
  function createReplacements() {
4924
5088
  return [
5089
+ // Remove anything but the contents within the BODY element
5090
+ [new RegExp(/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/g), ''],
5091
+
5092
+ // cleanup comments added by Chrome when pasting html
5093
+ [new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''],
5094
+
5095
+ // Trailing BR elements
5096
+ [new RegExp(/<br>$/i), ''],
5097
+
4925
5098
  // replace two bogus tags that begin pastes from google docs
4926
5099
  [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],
4927
5100
  [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''],
@@ -4931,13 +5104,13 @@ MediumEditor.extensions = {};
4931
5104
  [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
4932
5105
 
4933
5106
  // replace google docs italics+bold with a span to be replaced once the html is inserted
4934
- [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
5107
+ [new RegExp(/<span[^>]*(font-style:italic;font-weight:(bold|700)|font-weight:(bold|700);font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
4935
5108
 
4936
5109
  // replace google docs italics with a span to be replaced once the html is inserted
4937
5110
  [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
4938
5111
 
4939
5112
  //[replace google docs bolds with a span to be replaced once the html is inserted
4940
- [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
5113
+ [new RegExp(/<span[^>]*font-weight:(bold|700)[^>]*>/gi), '<span class="replace-with bold">'],
4941
5114
 
4942
5115
  // replace manually entered b/i/a tags with real ones
4943
5116
  [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
@@ -4953,13 +5126,47 @@ MediumEditor.extensions = {};
4953
5126
  // Microsoft Word makes these odd tags, like <o:p></o:p>
4954
5127
  [new RegExp(/<\/?o:[a-z]*>/gi), ''],
4955
5128
 
4956
- // cleanup comments added by Chrome when pasting html
4957
- ['<!--EndFragment-->', ''],
4958
- ['<!--StartFragment-->', '']
5129
+ // Microsoft Word adds some special elements around list items
5130
+ [new RegExp(/<!\[if !supportLists\]>(((?!<!).)*)<!\[endif]\>/gi), '$1']
4959
5131
  ];
4960
5132
  }
4961
5133
  /*jslint regexp: false*/
4962
5134
 
5135
+ /**
5136
+ * Gets various content types out of the Clipboard API. It will also get the
5137
+ * plain text using older IE and WebKit API.
5138
+ *
5139
+ * @param {event} event Event fired on paste.
5140
+ * @param {win} reference to window
5141
+ * @param {doc} reference to document
5142
+ * @return {Object} Object with mime types and data for those mime types.
5143
+ */
5144
+ function getClipboardContent(event, win, doc) {
5145
+ var dataTransfer = event.clipboardData || win.clipboardData || doc.dataTransfer,
5146
+ data = {};
5147
+
5148
+ if (!dataTransfer) {
5149
+ return data;
5150
+ }
5151
+
5152
+ // Use old WebKit/IE API
5153
+ if (dataTransfer.getData) {
5154
+ var legacyText = dataTransfer.getData('Text');
5155
+ if (legacyText && legacyText.length > 0) {
5156
+ data['text/plain'] = legacyText;
5157
+ }
5158
+ }
5159
+
5160
+ if (dataTransfer.types) {
5161
+ for (var i = 0; i < dataTransfer.types.length; i++) {
5162
+ var contentType = dataTransfer.types[i];
5163
+ data[contentType] = dataTransfer.getData(contentType);
5164
+ }
5165
+ }
5166
+
5167
+ return data;
5168
+ }
5169
+
4963
5170
  var PasteHandler = MediumEditor.Extension.extend({
4964
5171
  /* Paste Options */
4965
5172
 
@@ -4999,65 +5206,247 @@ MediumEditor.extensions = {};
4999
5206
  */
5000
5207
  cleanTags: ['meta'],
5001
5208
 
5209
+ /* unwrapTags: [Array]
5210
+ * list of element tag names to unwrap (remove the element tag but retain its child elements)
5211
+ * during paste when __cleanPastedHTML__ is `true` or when
5212
+ * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
5213
+ */
5214
+ unwrapTags: [],
5215
+
5002
5216
  init: function () {
5003
5217
  MediumEditor.Extension.prototype.init.apply(this, arguments);
5004
5218
 
5005
5219
  if (this.forcePlainText || this.cleanPastedHTML) {
5006
- this.subscribe('editablePaste', this.handlePaste.bind(this));
5220
+ this.subscribe('editableKeydown', this.handleKeydown.bind(this));
5221
+ // We need access to the full event data in paste
5222
+ // so we can't use the editablePaste event here
5223
+ this.getEditorElements().forEach(function (element) {
5224
+ this.on(element, 'paste', this.handlePaste.bind(this));
5225
+ }, this);
5226
+ this.subscribe('addElement', this.handleAddElement.bind(this));
5007
5227
  }
5008
5228
  },
5009
5229
 
5010
- handlePaste: function (event, element) {
5011
- var paragraphs,
5012
- html = '',
5013
- p,
5014
- dataFormatHTML = 'text/html',
5015
- dataFormatPlain = 'text/plain',
5016
- pastedHTML,
5017
- pastedPlain;
5018
-
5019
- if (this.window.clipboardData && event.clipboardData === undefined) {
5020
- event.clipboardData = this.window.clipboardData;
5230
+ handleAddElement: function (event, editable) {
5231
+ this.on(editable, 'paste', this.handlePaste.bind(this));
5232
+ },
5233
+
5234
+ destroy: function () {
5235
+ // Make sure pastebin is destroyed in case it's still around for some reason
5236
+ if (this.forcePlainText || this.cleanPastedHTML) {
5237
+ this.removePasteBin();
5238
+ }
5239
+ },
5240
+
5241
+ handlePaste: function (event, editable) {
5242
+ if (event.defaultPrevented) {
5243
+ return;
5244
+ }
5245
+
5246
+ var clipboardContent = getClipboardContent(event, this.window, this.document),
5247
+ pastedHTML = clipboardContent['text/html'],
5248
+ pastedPlain = clipboardContent['text/plain'];
5249
+
5250
+ if (this.window.clipboardData && event.clipboardData === undefined && !pastedHTML) {
5021
5251
  // If window.clipboardData exists, but event.clipboardData doesn't exist,
5022
5252
  // we're probably in IE. IE only has two possibilities for clipboard
5023
5253
  // data format: 'Text' and 'URL'.
5024
5254
  //
5025
- // Of the two, we want 'Text':
5026
- dataFormatHTML = 'Text';
5027
- dataFormatPlain = 'Text';
5255
+ // For IE, we'll fallback to 'Text' for text/html
5256
+ pastedHTML = pastedPlain;
5028
5257
  }
5029
5258
 
5030
- if (event.clipboardData &&
5031
- event.clipboardData.getData &&
5032
- !event.defaultPrevented) {
5259
+ if (pastedHTML || pastedPlain) {
5033
5260
  event.preventDefault();
5034
5261
 
5035
- pastedHTML = event.clipboardData.getData(dataFormatHTML);
5036
- pastedPlain = event.clipboardData.getData(dataFormatPlain);
5262
+ this.doPaste(pastedHTML, pastedPlain, editable);
5263
+ }
5264
+ },
5037
5265
 
5038
- if (this.cleanPastedHTML && pastedHTML) {
5039
- return this.cleanPaste(pastedHTML);
5040
- }
5266
+ doPaste: function (pastedHTML, pastedPlain, editable) {
5267
+ var paragraphs,
5268
+ html = '',
5269
+ p;
5041
5270
 
5042
- if (!(this.getEditorOption('disableReturn') || element.getAttribute('data-disable-return'))) {
5043
- paragraphs = pastedPlain.split(/[\r\n]+/g);
5044
- // If there are no \r\n in data, don't wrap in <p>
5045
- if (paragraphs.length > 1) {
5046
- for (p = 0; p < paragraphs.length; p += 1) {
5047
- if (paragraphs[p] !== '') {
5048
- html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
5049
- }
5271
+ if (this.cleanPastedHTML && pastedHTML) {
5272
+ return this.cleanPaste(pastedHTML);
5273
+ }
5274
+
5275
+ if (!(this.getEditorOption('disableReturn') || (editable && editable.getAttribute('data-disable-return')))) {
5276
+ paragraphs = pastedPlain.split(/[\r\n]+/g);
5277
+ // If there are no \r\n in data, don't wrap in <p>
5278
+ if (paragraphs.length > 1) {
5279
+ for (p = 0; p < paragraphs.length; p += 1) {
5280
+ if (paragraphs[p] !== '') {
5281
+ html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
5050
5282
  }
5051
- } else {
5052
- html = MediumEditor.util.htmlEntities(paragraphs[0]);
5053
5283
  }
5054
5284
  } else {
5055
- html = MediumEditor.util.htmlEntities(pastedPlain);
5285
+ html = MediumEditor.util.htmlEntities(paragraphs[0]);
5056
5286
  }
5057
- MediumEditor.util.insertHTMLCommand(this.document, html);
5287
+ } else {
5288
+ html = MediumEditor.util.htmlEntities(pastedPlain);
5289
+ }
5290
+ MediumEditor.util.insertHTMLCommand(this.document, html);
5291
+ },
5292
+
5293
+ handlePasteBinPaste: function (event) {
5294
+ if (event.defaultPrevented) {
5295
+ this.removePasteBin();
5296
+ return;
5297
+ }
5298
+
5299
+ var clipboardContent = getClipboardContent(event, this.window, this.document),
5300
+ pastedHTML = clipboardContent['text/html'],
5301
+ pastedPlain = clipboardContent['text/plain'],
5302
+ editable = keyboardPasteEditable;
5303
+
5304
+ // If we have valid html already, or we're not in cleanPastedHTML mode
5305
+ // we can ignore the paste bin and just paste now
5306
+ if (!this.cleanPastedHTML || pastedHTML) {
5307
+ event.preventDefault();
5308
+ this.removePasteBin();
5309
+ this.doPaste(pastedHTML, pastedPlain, editable);
5310
+
5311
+ // The event handling code listens for paste on the editable element
5312
+ // in order to trigger the editablePaste event. Since this paste event
5313
+ // is happening on the pastebin, the event handling code never knows about it
5314
+ // So, we have to trigger editablePaste manually
5315
+ this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);
5316
+ return;
5317
+ }
5318
+
5319
+ // We need to look at the paste bin, so do a setTimeout to let the paste
5320
+ // fall through into the paste bin
5321
+ setTimeout(function () {
5322
+ // Only look for HTML if we're in cleanPastedHTML mode
5323
+ if (this.cleanPastedHTML) {
5324
+ // If clipboard didn't have HTML, try the paste bin
5325
+ pastedHTML = this.getPasteBinHtml();
5326
+ }
5327
+
5328
+ // If we needed the paste bin, we're done with it now, remove it
5329
+ this.removePasteBin();
5330
+
5331
+ // Handle the paste with the html from the paste bin
5332
+ this.doPaste(pastedHTML, pastedPlain, editable);
5333
+
5334
+ // The event handling code listens for paste on the editable element
5335
+ // in order to trigger the editablePaste event. Since this paste event
5336
+ // is happening on the pastebin, the event handling code never knows about it
5337
+ // So, we have to trigger editablePaste manually
5338
+ this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);
5339
+ }.bind(this), 0);
5340
+ },
5341
+
5342
+ handleKeydown: function (event, editable) {
5343
+ // if it's not Ctrl+V, do nothing
5344
+ if (!(MediumEditor.util.isKey(event, MediumEditor.util.keyCode.V) && MediumEditor.util.isMetaCtrlKey(event))) {
5345
+ return;
5346
+ }
5347
+
5348
+ event.stopImmediatePropagation();
5349
+
5350
+ this.removePasteBin();
5351
+ this.createPasteBin(editable);
5352
+ },
5353
+
5354
+ createPasteBin: function (editable) {
5355
+ var rects,
5356
+ range = MediumEditor.selection.getSelectionRange(this.document),
5357
+ top = this.window.pageYOffset;
5358
+
5359
+ keyboardPasteEditable = editable;
5360
+
5361
+ if (range) {
5362
+ rects = range.getClientRects();
5363
+
5364
+ // on empty line, rects is empty so we grab information from the first container of the range
5365
+ if (rects.length) {
5366
+ top += rects[0].top;
5367
+ } else {
5368
+ top += range.startContainer.getBoundingClientRect().top;
5369
+ }
5370
+ }
5371
+
5372
+ lastRange = range;
5373
+
5374
+ var pasteBinElm = this.document.createElement('div');
5375
+ pasteBinElm.id = this.pasteBinId = 'medium-editor-pastebin-' + (+Date.now());
5376
+ pasteBinElm.setAttribute('style', 'border: 1px red solid; position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0');
5377
+ pasteBinElm.setAttribute('contentEditable', true);
5378
+ pasteBinElm.innerHTML = pasteBinDefaultContent;
5379
+
5380
+ this.document.body.appendChild(pasteBinElm);
5381
+
5382
+ // avoid .focus() to stop other event (actually the paste event)
5383
+ this.on(pasteBinElm, 'focus', stopProp);
5384
+ this.on(pasteBinElm, 'focusin', stopProp);
5385
+ this.on(pasteBinElm, 'focusout', stopProp);
5386
+
5387
+ pasteBinElm.focus();
5388
+
5389
+ MediumEditor.selection.selectNode(pasteBinElm, this.document);
5390
+
5391
+ if (!this.boundHandlePaste) {
5392
+ this.boundHandlePaste = this.handlePasteBinPaste.bind(this);
5393
+ }
5394
+
5395
+ this.on(pasteBinElm, 'paste', this.boundHandlePaste);
5396
+ },
5397
+
5398
+ removePasteBin: function () {
5399
+ if (null !== lastRange) {
5400
+ MediumEditor.selection.selectRange(this.document, lastRange);
5401
+ lastRange = null;
5402
+ }
5403
+
5404
+ if (null !== keyboardPasteEditable) {
5405
+ keyboardPasteEditable = null;
5406
+ }
5407
+
5408
+ var pasteBinElm = this.getPasteBin();
5409
+ if (!pasteBinElm) {
5410
+ return;
5411
+ }
5412
+
5413
+ if (pasteBinElm) {
5414
+ this.off(pasteBinElm, 'focus', stopProp);
5415
+ this.off(pasteBinElm, 'focusin', stopProp);
5416
+ this.off(pasteBinElm, 'focusout', stopProp);
5417
+ this.off(pasteBinElm, 'paste', this.boundHandlePaste);
5418
+ pasteBinElm.parentElement.removeChild(pasteBinElm);
5058
5419
  }
5059
5420
  },
5060
5421
 
5422
+ getPasteBin: function () {
5423
+ return this.document.getElementById(this.pasteBinId);
5424
+ },
5425
+
5426
+ getPasteBinHtml: function () {
5427
+ var pasteBinElm = this.getPasteBin();
5428
+
5429
+ if (!pasteBinElm) {
5430
+ return false;
5431
+ }
5432
+
5433
+ // WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
5434
+ // so we need to force plain text mode in this case
5435
+ if (pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
5436
+ return false;
5437
+ }
5438
+
5439
+ var pasteBinHtml = pasteBinElm.innerHTML;
5440
+
5441
+ // If paste bin is empty try using plain text mode
5442
+ // since that is better than nothing right
5443
+ if (!pasteBinHtml || pasteBinHtml === pasteBinDefaultContent) {
5444
+ return false;
5445
+ }
5446
+
5447
+ return pasteBinHtml;
5448
+ },
5449
+
5061
5450
  cleanPaste: function (text) {
5062
5451
  var i, elList, tmp, workEl,
5063
5452
  multiline = /<p|<br|<div/.test(text),
@@ -5107,7 +5496,8 @@ MediumEditor.extensions = {};
5107
5496
  pasteHTML: function (html, options) {
5108
5497
  options = MediumEditor.util.defaults({}, options, {
5109
5498
  cleanAttrs: this.cleanAttrs,
5110
- cleanTags: this.cleanTags
5499
+ cleanTags: this.cleanTags,
5500
+ unwrapTags: this.unwrapTags
5111
5501
  });
5112
5502
 
5113
5503
  var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();
@@ -5129,21 +5519,25 @@ MediumEditor.extensions = {};
5129
5519
 
5130
5520
  MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);
5131
5521
  MediumEditor.util.cleanupTags(workEl, options.cleanTags);
5522
+ MediumEditor.util.unwrapTags(workEl, options.unwrapTags);
5132
5523
  }
5133
5524
 
5134
5525
  MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
5135
5526
  },
5136
5527
 
5528
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5137
5529
  isCommonBlock: function (el) {
5138
5530
  return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
5139
5531
  },
5140
5532
 
5533
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5141
5534
  filterCommonBlocks: function (el) {
5142
5535
  if (/^\s*$/.test(el.textContent) && el.parentNode) {
5143
5536
  el.parentNode.removeChild(el);
5144
5537
  }
5145
5538
  },
5146
5539
 
5540
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5147
5541
  filterLineBreak: function (el) {
5148
5542
  if (this.isCommonBlock(el.previousElementSibling)) {
5149
5543
  // remove stray br's following common block elements
@@ -5157,6 +5551,7 @@ MediumEditor.extensions = {};
5157
5551
  }
5158
5552
  },
5159
5553
 
5554
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5160
5555
  // remove an element, including its parent, if it is the only element within its parent
5161
5556
  removeWithParent: function (el) {
5162
5557
  if (el && el.parentNode) {
@@ -5168,6 +5563,7 @@ MediumEditor.extensions = {};
5168
5563
  }
5169
5564
  },
5170
5565
 
5566
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5171
5567
  cleanupSpans: function (containerEl) {
5172
5568
  var i,
5173
5569
  el,
@@ -5234,37 +5630,61 @@ MediumEditor.extensions = {};
5234
5630
  },
5235
5631
 
5236
5632
  initPlaceholders: function () {
5237
- this.getEditorElements().forEach(function (el) {
5238
- if (!el.getAttribute('data-placeholder')) {
5239
- el.setAttribute('data-placeholder', this.text);
5240
- }
5241
- this.updatePlaceholder(el);
5242
- }, this);
5633
+ this.getEditorElements().forEach(this.initElement, this);
5634
+ },
5635
+
5636
+ handleAddElement: function (event, editable) {
5637
+ this.initElement(editable);
5638
+ },
5639
+
5640
+ initElement: function (el) {
5641
+ if (!el.getAttribute('data-placeholder')) {
5642
+ el.setAttribute('data-placeholder', this.text);
5643
+ }
5644
+ this.updatePlaceholder(el);
5243
5645
  },
5244
5646
 
5245
5647
  destroy: function () {
5246
- this.getEditorElements().forEach(function (el) {
5247
- if (el.getAttribute('data-placeholder') === this.text) {
5248
- el.removeAttribute('data-placeholder');
5249
- }
5250
- }, this);
5648
+ this.getEditorElements().forEach(this.cleanupElement, this);
5649
+ },
5650
+
5651
+ handleRemoveElement: function (event, editable) {
5652
+ this.cleanupElement(editable);
5653
+ },
5654
+
5655
+ cleanupElement: function (el) {
5656
+ if (el.getAttribute('data-placeholder') === this.text) {
5657
+ el.removeAttribute('data-placeholder');
5658
+ }
5251
5659
  },
5252
5660
 
5253
5661
  showPlaceholder: function (el) {
5254
5662
  if (el) {
5255
- el.classList.add('medium-editor-placeholder');
5663
+ // https://github.com/yabwe/medium-editor/issues/234
5664
+ // In firefox, styling the placeholder with an absolutely positioned
5665
+ // pseudo element causes the cursor to appear in a bad location
5666
+ // when the element is completely empty, so apply a different class to
5667
+ // style it with a relatively positioned pseudo element
5668
+ if (MediumEditor.util.isFF && el.childNodes.length === 0) {
5669
+ el.classList.add('medium-editor-placeholder-relative');
5670
+ el.classList.remove('medium-editor-placeholder');
5671
+ } else {
5672
+ el.classList.add('medium-editor-placeholder');
5673
+ el.classList.remove('medium-editor-placeholder-relative');
5674
+ }
5256
5675
  }
5257
5676
  },
5258
5677
 
5259
5678
  hidePlaceholder: function (el) {
5260
5679
  if (el) {
5261
5680
  el.classList.remove('medium-editor-placeholder');
5681
+ el.classList.remove('medium-editor-placeholder-relative');
5262
5682
  }
5263
5683
  },
5264
5684
 
5265
5685
  updatePlaceholder: function (el, dontShow) {
5266
5686
  // If the element has content, hide the placeholder
5267
- if (el.querySelector('img, blockquote, ul, ol') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) {
5687
+ if (el.querySelector('img, blockquote, ul, ol, table') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) {
5268
5688
  return this.hidePlaceholder(el);
5269
5689
  }
5270
5690
 
@@ -5284,6 +5704,10 @@ MediumEditor.extensions = {};
5284
5704
 
5285
5705
  // When the editor loses focus, check if the placeholder should be visible
5286
5706
  this.subscribe('blur', this.handleBlur.bind(this));
5707
+
5708
+ // Need to know when elements are added/removed from the editor
5709
+ this.subscribe('addElement', this.handleAddElement.bind(this));
5710
+ this.subscribe('removeElement', this.handleRemoveElement.bind(this));
5287
5711
  },
5288
5712
 
5289
5713
  handleInput: function (event, element) {
@@ -5500,6 +5924,10 @@ MediumEditor.extensions = {};
5500
5924
 
5501
5925
  // Toolbar accessors
5502
5926
 
5927
+ getInteractionElements: function () {
5928
+ return this.getToolbarElement();
5929
+ },
5930
+
5503
5931
  getToolbarElement: function () {
5504
5932
  if (!this.toolbar) {
5505
5933
  this.toolbar = this.createToolbar();
@@ -5825,30 +6253,26 @@ MediumEditor.extensions = {};
5825
6253
 
5826
6254
  setToolbarPosition: function () {
5827
6255
  var container = this.base.getFocusedElement(),
5828
- selection = this.window.getSelection(),
5829
- anchorPreview;
6256
+ selection = this.window.getSelection();
5830
6257
 
5831
6258
  // If there isn't a valid selection, bail
5832
6259
  if (!container) {
5833
6260
  return this;
5834
6261
  }
5835
6262
 
5836
- if (this.static && !this.relativeContainer) {
5837
- this.showToolbar();
5838
- this.positionStaticToolbar(container);
5839
- } else if (!selection.isCollapsed) {
6263
+ if (this.static || !selection.isCollapsed) {
5840
6264
  this.showToolbar();
5841
6265
 
5842
6266
  // we don't need any absolute positioning if relativeContainer is set
5843
6267
  if (!this.relativeContainer) {
5844
- this.positionToolbar(selection);
6268
+ if (this.static) {
6269
+ this.positionStaticToolbar(container);
6270
+ } else {
6271
+ this.positionToolbar(selection);
6272
+ }
5845
6273
  }
5846
- }
5847
-
5848
- anchorPreview = this.base.getExtensionByName('anchor-preview');
5849
6274
 
5850
- if (anchorPreview && typeof anchorPreview.hidePreview === 'function') {
5851
- anchorPreview.hidePreview();
6275
+ this.trigger('positionedToolbar', {}, this.base.getFocusedElement());
5852
6276
  }
5853
6277
  },
5854
6278
 
@@ -5927,35 +6351,66 @@ MediumEditor.extensions = {};
5927
6351
  }
5928
6352
  }
5929
6353
 
5930
- var windowWidth = this.window.innerWidth,
5931
- middleBoundary = (boundary.left + boundary.right) / 2,
6354
+ var containerWidth = this.window.innerWidth,
5932
6355
  toolbarElement = this.getToolbarElement(),
5933
6356
  toolbarHeight = toolbarElement.offsetHeight,
5934
6357
  toolbarWidth = toolbarElement.offsetWidth,
5935
6358
  halfOffsetWidth = toolbarWidth / 2,
5936
6359
  buttonHeight = 50,
5937
- defaultLeft = this.diffLeft - halfOffsetWidth;
6360
+ defaultLeft = this.diffLeft - halfOffsetWidth,
6361
+ elementsContainer = this.getEditorOption('elementsContainer'),
6362
+ elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
6363
+ positions = {},
6364
+ relativeBoundary = {},
6365
+ middleBoundary, elementsContainerBoundary;
6366
+
6367
+ // If container element is absolute / fixed, recalculate boundaries to be relative to the container
6368
+ if (elementsContainerAbsolute) {
6369
+ elementsContainerBoundary = elementsContainer.getBoundingClientRect();
6370
+ ['top', 'left'].forEach(function (key) {
6371
+ relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
6372
+ });
6373
+
6374
+ relativeBoundary.width = boundary.width;
6375
+ relativeBoundary.height = boundary.height;
6376
+ boundary = relativeBoundary;
6377
+
6378
+ containerWidth = elementsContainerBoundary.width;
6379
+
6380
+ // Adjust top position according to container scroll position
6381
+ positions.top = elementsContainer.scrollTop;
6382
+ } else {
6383
+ // Adjust top position according to window scroll position
6384
+ positions.top = this.window.pageYOffset;
6385
+ }
6386
+
6387
+ middleBoundary = boundary.left + boundary.width / 2;
6388
+ positions.top += boundary.top - toolbarHeight;
5938
6389
 
5939
6390
  if (boundary.top < buttonHeight) {
5940
6391
  toolbarElement.classList.add('medium-toolbar-arrow-over');
5941
6392
  toolbarElement.classList.remove('medium-toolbar-arrow-under');
5942
- toolbarElement.style.top = buttonHeight + boundary.bottom - this.diffTop + this.window.pageYOffset - toolbarHeight + 'px';
6393
+ positions.top += buttonHeight + boundary.height - this.diffTop;
5943
6394
  } else {
5944
6395
  toolbarElement.classList.add('medium-toolbar-arrow-under');
5945
6396
  toolbarElement.classList.remove('medium-toolbar-arrow-over');
5946
- toolbarElement.style.top = boundary.top + this.diffTop + this.window.pageYOffset - toolbarHeight + 'px';
6397
+ positions.top += this.diffTop;
5947
6398
  }
5948
6399
 
5949
6400
  if (middleBoundary < halfOffsetWidth) {
5950
- toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px';
5951
- toolbarElement.style.right = 'initial';
5952
- } else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
5953
- toolbarElement.style.left = 'auto';
5954
- toolbarElement.style.right = 0;
6401
+ positions.left = defaultLeft + halfOffsetWidth;
6402
+ positions.right = 'initial';
6403
+ } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
6404
+ positions.left = 'auto';
6405
+ positions.right = 0;
5955
6406
  } else {
5956
- toolbarElement.style.left = defaultLeft + middleBoundary + 'px';
5957
- toolbarElement.style.right = 'initial';
6407
+ positions.left = defaultLeft + middleBoundary;
6408
+ positions.right = 'initial';
5958
6409
  }
6410
+
6411
+ ['top', 'left', 'right'].forEach(function (key) {
6412
+ toolbarElement.style[key] = positions[key] + (isNaN(positions[key]) ? '' : 'px');
6413
+ });
5959
6414
  }
5960
6415
  });
5961
6416
 
@@ -6162,6 +6617,31 @@ MediumEditor.extensions = {};
6162
6617
  // then pressing backspace key should change the <blockquote> to a <p> tag
6163
6618
  event.preventDefault();
6164
6619
  MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');
6620
+ } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&
6621
+ (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
6622
+ MediumEditor.selection.getCaretOffsets(node).right === 0) {
6623
+
6624
+ // when cursor is at the end of <blockquote>,
6625
+ // then pressing enter key should create <p> tag, not <blockquote>
6626
+ p = this.options.ownerDocument.createElement('p');
6627
+ p.innerHTML = '<br>';
6628
+ node.parentElement.insertBefore(p, node.nextSibling);
6629
+
6630
+ // move the cursor into the new paragraph
6631
+ MediumEditor.selection.moveCursor(this.options.ownerDocument, p);
6632
+
6633
+ event.preventDefault();
6634
+ } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
6635
+ MediumEditor.util.isMediumEditorElement(node.parentElement) &&
6636
+ !node.previousElementSibling &&
6637
+ node.nextElementSibling &&
6638
+ isEmpty.test(node.innerHTML)) {
6639
+
6640
+ // when cursor is in the first element, it's empty and user presses backspace,
6641
+ // do delete action instead to get rid of the first element and move caret to 2nd
6642
+ event.preventDefault();
6643
+ MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextSibling);
6644
+ node.parentElement.removeChild(node);
6165
6645
  }
6166
6646
  }
6167
6647
 
@@ -6173,7 +6653,9 @@ MediumEditor.extensions = {};
6173
6653
  return;
6174
6654
  }
6175
6655
 
6176
- if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0) {
6656
+ // https://github.com/yabwe/medium-editor/issues/994
6657
+ // Firefox thrown an error when calling `formatBlock` on an empty editable blockContainer that's not a <div>
6658
+ if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0 && !MediumEditor.util.isBlockContainer(node)) {
6177
6659
  this.options.ownerDocument.execCommand('formatBlock', false, 'p');
6178
6660
  }
6179
6661
 
@@ -6194,6 +6676,13 @@ MediumEditor.extensions = {};
6194
6676
  }
6195
6677
  }
6196
6678
 
6679
+ function handleEditableInput(event, editable) {
6680
+ var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id="' + editable.getAttribute('medium-editor-textarea-id') + '"]');
6681
+ if (textarea) {
6682
+ textarea.value = editable.innerHTML.trim();
6683
+ }
6684
+ }
6685
+
6197
6686
  // Internal helper methods which shouldn't be exposed externally
6198
6687
 
6199
6688
  function addToEditors(win) {
@@ -6227,30 +6716,50 @@ MediumEditor.extensions = {};
6227
6716
  win._mediumEditors[this.id] = null;
6228
6717
  }
6229
6718
 
6230
- function createElementsArray(selector) {
6719
+ function createElementsArray(selector, doc, filterEditorElements) {
6720
+ var elements = [];
6721
+
6231
6722
  if (!selector) {
6232
6723
  selector = [];
6233
6724
  }
6234
6725
  // If string, use as query selector
6235
6726
  if (typeof selector === 'string') {
6236
- selector = this.options.ownerDocument.querySelectorAll(selector);
6727
+ selector = doc.querySelectorAll(selector);
6237
6728
  }
6238
6729
  // If element, put into array
6239
6730
  if (MediumEditor.util.isElement(selector)) {
6240
6731
  selector = [selector];
6241
6732
  }
6242
- // Convert NodeList (or other array like object) into an array
6243
- var elements = Array.prototype.slice.apply(selector);
6244
6733
 
6245
- // Loop through elements and convert textarea's into divs
6246
- this.elements = [];
6247
- elements.forEach(function (element, index) {
6248
- if (element.nodeName.toLowerCase() === 'textarea') {
6249
- this.elements.push(createContentEditable.call(this, element, index));
6250
- } else {
6251
- this.elements.push(element);
6734
+ if (filterEditorElements) {
6735
+ // Remove elements that have already been initialized by the editor
6736
+ // selecotr might not be an array (ie NodeList) so use for loop
6737
+ for (var i = 0; i < selector.length; i++) {
6738
+ var el = selector[i];
6739
+ if (MediumEditor.util.isElement(el) &&
6740
+ !el.getAttribute('data-medium-editor-element') &&
6741
+ !el.getAttribute('medium-editor-textarea-id')) {
6742
+ elements.push(el);
6743
+ }
6252
6744
  }
6253
- }, this);
6745
+ } else {
6746
+ // Convert NodeList (or other array like object) into an array
6747
+ elements = Array.prototype.slice.apply(selector);
6748
+ }
6749
+
6750
+ return elements;
6751
+ }
6752
+
6753
+ function cleanupTextareaElement(element) {
6754
+ var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]');
6755
+ if (textarea) {
6756
+ // Un-hide the textarea
6757
+ textarea.classList.remove('medium-editor-hidden');
6758
+ textarea.removeAttribute('medium-editor-textarea-id');
6759
+ }
6760
+ if (element.parentNode) {
6761
+ element.parentNode.removeChild(element);
6762
+ }
6254
6763
  }
6255
6764
 
6256
6765
  function setExtensionDefaults(extension, defaults) {
@@ -6328,17 +6837,17 @@ MediumEditor.extensions = {};
6328
6837
  return !this.options.extensions['imageDragging'];
6329
6838
  }
6330
6839
 
6331
- function createContentEditable(textarea, id) {
6840
+ function createContentEditable(textarea) {
6332
6841
  var div = this.options.ownerDocument.createElement('div'),
6333
6842
  now = Date.now(),
6334
- uniqueId = 'medium-editor-' + now + '-' + id,
6843
+ uniqueId = 'medium-editor-' + now,
6335
6844
  atts = textarea.attributes;
6336
6845
 
6337
6846
  // Some browsers can move pretty fast, since we're using a timestamp
6338
6847
  // to make a unique-id, ensure that the id is actually unique on the page
6339
6848
  while (this.options.ownerDocument.getElementById(uniqueId)) {
6340
6849
  now++;
6341
- uniqueId = 'medium-editor-' + now + '-' + id;
6850
+ uniqueId = 'medium-editor-' + now;
6342
6851
  }
6343
6852
 
6344
6853
  div.className = textarea.className;
@@ -6355,6 +6864,16 @@ MediumEditor.extensions = {};
6355
6864
  }
6356
6865
  }
6357
6866
 
6867
+ // If textarea has a form, listen for reset on the form to clear
6868
+ // the content of the created div
6869
+ if (textarea.form) {
6870
+ this.on(textarea.form, 'reset', function (event) {
6871
+ if (!event.defaultPrevented) {
6872
+ this.resetContent(this.options.ownerDocument.getElementById(uniqueId));
6873
+ }
6874
+ }.bind(this));
6875
+ }
6876
+
6358
6877
  textarea.classList.add('medium-editor-hidden');
6359
6878
  textarea.parentNode.insertBefore(
6360
6879
  div,
@@ -6364,37 +6883,57 @@ MediumEditor.extensions = {};
6364
6883
  return div;
6365
6884
  }
6366
6885
 
6367
- function initElements() {
6368
- var isTextareaUsed = false;
6886
+ function initElement(element, editorId) {
6887
+ if (!element.getAttribute('data-medium-editor-element')) {
6888
+ if (element.nodeName.toLowerCase() === 'textarea') {
6889
+ element = createContentEditable.call(this, element);
6890
+
6891
+ // Make sure we only attach to editableInput once for <textarea> elements
6892
+ if (!this.instanceHandleEditableInput) {
6893
+ this.instanceHandleEditableInput = handleEditableInput.bind(this);
6894
+ this.subscribe('editableInput', this.instanceHandleEditableInput);
6895
+ }
6896
+ }
6369
6897
 
6370
- this.elements.forEach(function (element, index) {
6371
6898
  if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {
6372
6899
  element.setAttribute('contentEditable', true);
6373
6900
  element.setAttribute('spellcheck', this.options.spellcheck);
6374
6901
  }
6375
- element.setAttribute('data-medium-editor-element', true);
6376
- element.setAttribute('role', 'textbox');
6377
- element.setAttribute('aria-multiline', true);
6378
- element.setAttribute('medium-editor-index', index);
6379
6902
 
6380
- if (element.hasAttribute('medium-editor-textarea-id')) {
6381
- isTextareaUsed = true;
6903
+ // Make sure we only attach to editableKeydownEnter once for disable-return options
6904
+ if (!this.instanceHandleEditableKeydownEnter) {
6905
+ if (element.getAttribute('data-disable-return') || element.getAttribute('data-disable-double-return')) {
6906
+ this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
6907
+ this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
6908
+ }
6382
6909
  }
6383
- }, this);
6384
6910
 
6385
- if (isTextareaUsed) {
6386
- this.subscribe('editableInput', function (event, editable) {
6387
- var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id="' + editable.getAttribute('medium-editor-textarea-id') + '"]');
6388
- if (textarea) {
6389
- textarea.value = this.serialize()[editable.id].value;
6390
- }
6391
- }.bind(this));
6911
+ // if we're not disabling return, add a handler to help handle cleanup
6912
+ // for certain cases when enter is pressed
6913
+ if (!this.options.disableReturn && !element.getAttribute('data-disable-return')) {
6914
+ this.on(element, 'keyup', handleKeyup.bind(this));
6915
+ }
6916
+
6917
+ var elementId = MediumEditor.util.guid();
6918
+
6919
+ element.setAttribute('data-medium-editor-element', true);
6920
+ element.classList.add('medium-editor-element');
6921
+ element.setAttribute('role', 'textbox');
6922
+ element.setAttribute('aria-multiline', true);
6923
+ element.setAttribute('data-medium-editor-editor-index', editorId);
6924
+ // TODO: Merge data-medium-editor-element and medium-editor-index attributes for 6.0.0
6925
+ // medium-editor-index is not named correctly anymore and can be re-purposed to signify
6926
+ // whether the element has been initialized or not
6927
+ element.setAttribute('medium-editor-index', elementId);
6928
+ initialContent[elementId] = element.innerHTML;
6929
+
6930
+ this.events.attachAllEventsToElement(element);
6392
6931
  }
6932
+
6933
+ return element;
6393
6934
  }
6394
6935
 
6395
6936
  function attachHandlers() {
6396
- var i;
6397
-
6398
6937
  // attach to tabs
6399
6938
  this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));
6400
6939
 
@@ -6407,27 +6946,14 @@ MediumEditor.extensions = {};
6407
6946
  this.subscribe('editableKeydownSpace', handleDisableExtraSpaces.bind(this));
6408
6947
  }
6409
6948
 
6410
- // disabling return or double return
6411
- if (this.options.disableReturn || this.options.disableDoubleReturn) {
6412
- this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
6413
- } else {
6414
- for (i = 0; i < this.elements.length; i += 1) {
6415
- if (this.elements[i].getAttribute('data-disable-return') || this.elements[i].getAttribute('data-disable-double-return')) {
6416
- this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
6417
- break;
6418
- }
6949
+ // Make sure we only attach to editableKeydownEnter once for disable-return options
6950
+ if (!this.instanceHandleEditableKeydownEnter) {
6951
+ // disabling return or double return
6952
+ if (this.options.disableReturn || this.options.disableDoubleReturn) {
6953
+ this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
6954
+ this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
6419
6955
  }
6420
6956
  }
6421
-
6422
- // if we're not disabling return, add a handler to help handle cleanup
6423
- // for certain cases when enter is pressed
6424
- if (!this.options.disableReturn) {
6425
- this.elements.forEach(function (element) {
6426
- if (!element.getAttribute('data-disable-return')) {
6427
- this.on(element, 'keyup', handleKeyup.bind(this));
6428
- }
6429
- }, this);
6430
- }
6431
6957
  }
6432
6958
 
6433
6959
  function initExtensions() {
@@ -6519,7 +7045,8 @@ MediumEditor.extensions = {};
6519
7045
  /*jslint regexp: true*/
6520
7046
  var appendAction = /^append-(.+)$/gi,
6521
7047
  justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */
6522
- match;
7048
+ match,
7049
+ cmdValueArgument;
6523
7050
  /*jslint regexp: false*/
6524
7051
 
6525
7052
  // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
@@ -6530,11 +7057,21 @@ MediumEditor.extensions = {};
6530
7057
  }
6531
7058
 
6532
7059
  if (action === 'fontSize') {
6533
- return this.options.ownerDocument.execCommand('fontSize', false, opts.size);
7060
+ // TODO: Deprecate support for opts.size in 6.0.0
7061
+ if (opts.size) {
7062
+ MediumEditor.util.deprecated('.size option for fontSize command', '.value', '6.0.0');
7063
+ }
7064
+ cmdValueArgument = opts.value || opts.size;
7065
+ return this.options.ownerDocument.execCommand('fontSize', false, cmdValueArgument);
6534
7066
  }
6535
7067
 
6536
7068
  if (action === 'fontName') {
6537
- return this.options.ownerDocument.execCommand('fontName', false, opts.name);
7069
+ // TODO: Deprecate support for opts.name in 6.0.0
7070
+ if (opts.name) {
7071
+ MediumEditor.util.deprecated('.name option for fontName command', '.value', '6.0.0');
7072
+ }
7073
+ cmdValueArgument = opts.value || opts.name;
7074
+ return this.options.ownerDocument.execCommand('fontName', false, cmdValueArgument);
6538
7075
  }
6539
7076
 
6540
7077
  if (action === 'createLink') {
@@ -6558,7 +7095,8 @@ MediumEditor.extensions = {};
6558
7095
  return result;
6559
7096
  }
6560
7097
 
6561
- return this.options.ownerDocument.execCommand(action, false, null);
7098
+ cmdValueArgument = opts && opts.value;
7099
+ return this.options.ownerDocument.execCommand(action, false, cmdValueArgument);
6562
7100
  }
6563
7101
 
6564
7102
  /* If we've just justified text within a container block
@@ -6606,6 +7144,8 @@ MediumEditor.extensions = {};
6606
7144
  }
6607
7145
  }
6608
7146
 
7147
+ var initialContent = {};
7148
+
6609
7149
  MediumEditor.prototype = {
6610
7150
  // NOT DOCUMENTED - exposed for backwards compatability
6611
7151
  init: function (elements, options) {
@@ -6624,19 +7164,19 @@ MediumEditor.extensions = {};
6624
7164
  return;
6625
7165
  }
6626
7166
 
6627
- createElementsArray.call(this, this.origElements);
7167
+ addToEditors.call(this, this.options.contentWindow);
7168
+ this.events = new MediumEditor.Events(this);
7169
+ this.elements = [];
7170
+
7171
+ this.addElements(this.origElements);
6628
7172
 
6629
7173
  if (this.elements.length === 0) {
6630
7174
  return;
6631
7175
  }
6632
7176
 
6633
7177
  this.isActive = true;
6634
- addToEditors.call(this, this.options.contentWindow);
6635
-
6636
- this.events = new MediumEditor.Events(this);
6637
7178
 
6638
7179
  // Call initialization helpers
6639
- initElements.call(this);
6640
7180
  initExtensions.call(this);
6641
7181
  attachHandlers.call(this);
6642
7182
  },
@@ -6666,45 +7206,52 @@ MediumEditor.extensions = {};
6666
7206
  element.removeAttribute('contentEditable');
6667
7207
  element.removeAttribute('spellcheck');
6668
7208
  element.removeAttribute('data-medium-editor-element');
7209
+ element.classList.remove('medium-editor-element');
6669
7210
  element.removeAttribute('role');
6670
7211
  element.removeAttribute('aria-multiline');
6671
7212
  element.removeAttribute('medium-editor-index');
7213
+ element.removeAttribute('data-medium-editor-editor-index');
6672
7214
 
6673
7215
  // Remove any elements created for textareas
6674
- if (element.hasAttribute('medium-editor-textarea-id')) {
6675
- var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]');
6676
- if (textarea) {
6677
- // Un-hide the textarea
6678
- textarea.classList.remove('medium-editor-hidden');
6679
- }
6680
- if (element.parentNode) {
6681
- element.parentNode.removeChild(element);
6682
- }
7216
+ if (element.getAttribute('medium-editor-textarea-id')) {
7217
+ cleanupTextareaElement(element);
6683
7218
  }
6684
7219
  }, this);
6685
7220
  this.elements = [];
7221
+ this.instanceHandleEditableKeydownEnter = null;
7222
+ this.instanceHandleEditableInput = null;
6686
7223
 
6687
7224
  removeFromEditors.call(this, this.options.contentWindow);
6688
7225
  },
6689
7226
 
6690
7227
  on: function (target, event, listener, useCapture) {
6691
7228
  this.events.attachDOMEvent(target, event, listener, useCapture);
7229
+
7230
+ return this;
6692
7231
  },
6693
7232
 
6694
7233
  off: function (target, event, listener, useCapture) {
6695
7234
  this.events.detachDOMEvent(target, event, listener, useCapture);
7235
+
7236
+ return this;
6696
7237
  },
6697
7238
 
6698
7239
  subscribe: function (event, listener) {
6699
7240
  this.events.attachCustomEvent(event, listener);
7241
+
7242
+ return this;
6700
7243
  },
6701
7244
 
6702
7245
  unsubscribe: function (event, listener) {
6703
7246
  this.events.detachCustomEvent(event, listener);
7247
+
7248
+ return this;
6704
7249
  },
6705
7250
 
6706
7251
  trigger: function (name, data, editable) {
6707
7252
  this.events.triggerCustomEvent(name, data, editable);
7253
+
7254
+ return this;
6708
7255
  },
6709
7256
 
6710
7257
  delay: function (fn) {
@@ -6719,8 +7266,10 @@ MediumEditor.extensions = {};
6719
7266
  serialize: function () {
6720
7267
  var i,
6721
7268
  elementid,
6722
- content = {};
6723
- for (i = 0; i < this.elements.length; i += 1) {
7269
+ content = {},
7270
+ len = this.elements.length;
7271
+
7272
+ for (i = 0; i < len; i += 1) {
6724
7273
  elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
6725
7274
  content[elementid] = {
6726
7275
  value: this.elements[i].innerHTML.trim()
@@ -6954,7 +7503,8 @@ MediumEditor.extensions = {};
6954
7503
 
6955
7504
  createLink: function (opts) {
6956
7505
  var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
6957
- customEvent = {};
7506
+ customEvent = {},
7507
+ targetUrl;
6958
7508
 
6959
7509
  // Make sure the selection is within an element this editor is tracking
6960
7510
  if (this.elements.indexOf(currentEditor) === -1) {
@@ -6963,7 +7513,12 @@ MediumEditor.extensions = {};
6963
7513
 
6964
7514
  try {
6965
7515
  this.events.disableCustomEvent('editableInput');
6966
- if (opts.url && opts.url.trim().length > 0) {
7516
+ // TODO: Deprecate support for opts.url in 6.0.0
7517
+ if (opts.url) {
7518
+ MediumEditor.util.deprecated('.url option for createLink', '.value', '6.0.0');
7519
+ }
7520
+ targetUrl = opts.url || opts.value;
7521
+ if (targetUrl && targetUrl.trim().length > 0) {
6967
7522
  var currentSelection = this.options.contentWindow.getSelection();
6968
7523
  if (currentSelection) {
6969
7524
  var currRange = currentSelection.getRangeAt(0),
@@ -7055,7 +7610,7 @@ MediumEditor.extensions = {};
7055
7610
  }
7056
7611
 
7057
7612
  // Creates the link in the document fragment
7058
- MediumEditor.util.createLink(this.options.ownerDocument, textNodes, opts.url.trim());
7613
+ MediumEditor.util.createLink(this.options.ownerDocument, textNodes, targetUrl.trim());
7059
7614
 
7060
7615
  // Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.
7061
7616
  var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length;
@@ -7067,13 +7622,13 @@ MediumEditor.extensions = {};
7067
7622
 
7068
7623
  this.importSelection(exportedSelection);
7069
7624
  } else {
7070
- this.options.ownerDocument.execCommand('createLink', false, opts.url);
7625
+ this.options.ownerDocument.execCommand('createLink', false, targetUrl);
7071
7626
  }
7072
7627
 
7073
7628
  if (this.options.targetBlank || opts.target === '_blank') {
7074
- MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url);
7629
+ MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
7075
7630
  } else {
7076
- MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url);
7631
+ MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
7077
7632
  }
7078
7633
 
7079
7634
  if (opts.buttonClass) {
@@ -7085,7 +7640,7 @@ MediumEditor.extensions = {};
7085
7640
  if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {
7086
7641
  customEvent = this.options.ownerDocument.createEvent('HTMLEvents');
7087
7642
  customEvent.initEvent('input', true, true, this.options.contentWindow);
7088
- for (var i = 0; i < this.elements.length; i += 1) {
7643
+ for (var i = 0, len = this.elements.length; i < len; i += 1) {
7089
7644
  this.elements[i].dispatchEvent(customEvent);
7090
7645
  }
7091
7646
  }
@@ -7110,9 +7665,98 @@ MediumEditor.extensions = {};
7110
7665
  if (this.elements[index]) {
7111
7666
  var target = this.elements[index];
7112
7667
  target.innerHTML = html;
7113
- this.events.updateInput(target, { target: target, currentTarget: target });
7668
+ this.checkContentChanged(target);
7669
+ }
7670
+ },
7671
+
7672
+ getContent: function (index) {
7673
+ index = index || 0;
7674
+
7675
+ if (this.elements[index]) {
7676
+ return this.elements[index].innerHTML.trim();
7677
+ }
7678
+ return null;
7679
+ },
7680
+
7681
+ checkContentChanged: function (editable) {
7682
+ editable = editable || MediumEditor.selection.getSelectionElement(this.options.contentWindow);
7683
+ this.events.updateInput(editable, { target: editable, currentTarget: editable });
7684
+ },
7685
+
7686
+ resetContent: function (element) {
7687
+ // For all elements that exist in the this.elements array, we can assume:
7688
+ // - Its initial content has been set in the initialContent object
7689
+ // - It has a medium-editor-index attribute which is the key value in the initialContent object
7690
+
7691
+ if (element) {
7692
+ var index = this.elements.indexOf(element);
7693
+ if (index !== -1) {
7694
+ this.setContent(initialContent[element.getAttribute('medium-editor-index')], index);
7695
+ }
7696
+ return;
7697
+ }
7698
+
7699
+ this.elements.forEach(function (el, idx) {
7700
+ this.setContent(initialContent[el.getAttribute('medium-editor-index')], idx);
7701
+ }, this);
7702
+ },
7703
+
7704
+ addElements: function (selector) {
7705
+ // Convert elements into an array
7706
+ var elements = createElementsArray(selector, this.options.ownerDocument, true);
7707
+
7708
+ // Do we have elements to add now?
7709
+ if (elements.length === 0) {
7710
+ return false;
7114
7711
  }
7712
+
7713
+ elements.forEach(function (element) {
7714
+ // Initialize all new elements (we check that in those functions don't worry)
7715
+ element = initElement.call(this, element, this.id);
7716
+
7717
+ // Add new elements to our internal elements array
7718
+ this.elements.push(element);
7719
+
7720
+ // Trigger event so extensions can know when an element has been added
7721
+ this.trigger('addElement', { target: element, currentTarget: element }, element);
7722
+ }, this);
7723
+ },
7724
+
7725
+ removeElements: function (selector) {
7726
+ // Convert elements into an array
7727
+ var elements = createElementsArray(selector, this.options.ownerDocument),
7728
+ toRemove = elements.map(function (el) {
7729
+ // For textareas, make sure we're looking at the editor div and not the textarea itself
7730
+ if (el.getAttribute('medium-editor-textarea-id') && el.parentNode) {
7731
+ return el.parentNode.querySelector('div[medium-editor-textarea-id="' + el.getAttribute('medium-editor-textarea-id') + '"]');
7732
+ } else {
7733
+ return el;
7734
+ }
7735
+ });
7736
+
7737
+ this.elements = this.elements.filter(function (element) {
7738
+ // If this is an element we want to remove
7739
+ if (toRemove.indexOf(element) !== -1) {
7740
+ this.events.cleanupElement(element);
7741
+ if (element.getAttribute('medium-editor-textarea-id')) {
7742
+ cleanupTextareaElement(element);
7743
+ }
7744
+ // Trigger event so extensions can clean-up elements that are being removed
7745
+ this.trigger('removeElement', { target: element, currentTarget: element }, element);
7746
+ return false;
7747
+ }
7748
+ return true;
7749
+ }, this);
7750
+ }
7751
+ };
7752
+
7753
+ MediumEditor.getEditorFromElement = function (element) {
7754
+ var index = element.getAttribute('data-medium-editor-editor-index'),
7755
+ win = element && element.ownerDocument && (element.ownerDocument.defaultView || element.ownerDocument.parentWindow);
7756
+ if (win && win._mediumEditors && win._mediumEditors[index]) {
7757
+ return win._mediumEditors[index];
7115
7758
  }
7759
+ return null;
7116
7760
  };
7117
7761
  }());
7118
7762
 
@@ -7154,7 +7798,7 @@ MediumEditor.parseVersionString = function (release) {
7154
7798
 
7155
7799
  MediumEditor.version = MediumEditor.parseVersionString.call(this, ({
7156
7800
  // grunt-bump looks for this:
7157
- 'version': '5.15.0'
7801
+ 'version': '5.22.0'
7158
7802
  }).version);
7159
7803
 
7160
7804
  return MediumEditor;