medium-editor-rails 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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;