scrivito_editors 1.3.1 → 1.4.0.rc1

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: 473f8bed0ce2f9361a182be0bcbe8208256cdeae
4
- data.tar.gz: 1190dbc1b2b80804419175d16d42457a8ebf1629
3
+ metadata.gz: 1c20b548f95ef5b6cc55ae5d0f84bc1630983ff1
4
+ data.tar.gz: 0f80d14e64a84c337fd5c24e44ac47ec53032e5a
5
5
  SHA512:
6
- metadata.gz: 64a3129af3b1f3673609398e30a7c7d4d4ab813faa924f2e10d89556e5258ff0eb58d1ec27964144f721d1b05268dd97b8de61a5273b6194d0af2200af1107e3
7
- data.tar.gz: 106f0ce6b5455af37822175efd556a61338d0d1b100e043b7ef3da5c4738b67bbc1d1ee8bd28e1e61bc41e40c89790e84015672098016b3572f4199b81f87bcb
6
+ metadata.gz: 6e8058e3038e79dc4fcb5f2a439d1efac42e490707b8724aa29b1fab597cba69721ef5afcb737ea92ca2ecfc545b02e3cb2dc3b73e7d5a8b0f14ca1323f71c12
7
+ data.tar.gz: 994a4ef961e50e17b0c7e39fa45a52286ac4e0d3c999bf38ed8e8ffb65b2e40991f5f5e58e1f74c3132ea1a81f20d7713322dc01398a621dd1d7a92fb3238cd4
@@ -55,7 +55,7 @@
55
55
  var container, image;
56
56
  container = $(".image_kit_start");
57
57
  image = new Image();
58
- return element.scrivito('content').no_cache_url().then(function(no_cache_url) {
58
+ return element.scrivito('content').noCacheUrl().then(function(no_cache_url) {
59
59
  image.crossOrigin = "Anonymous";
60
60
  image.src = no_cache_url;
61
61
  image.onerror = function() {
@@ -49,7 +49,7 @@
49
49
  },
50
50
  completeFormSave: function(opts) {
51
51
  this.base.restoreSelection();
52
- if (opts.url) {
52
+ if (opts.value) {
53
53
  this.execAction(this.action, opts);
54
54
  } else {
55
55
  this.execAction('unlink');
@@ -75,8 +75,8 @@
75
75
  var i, len, objId, objIds, ul;
76
76
  cmsField.empty();
77
77
  objIds = cmsField.scrivito('content');
78
+ ul = $('<ul></ul>').appendTo(cmsField);
78
79
  if (objIds.length) {
79
- ul = $('<ul></ul>').appendTo(cmsField);
80
80
  for (i = 0, len = objIds.length; i < len; i++) {
81
81
  objId = objIds[i];
82
82
  ul.append(renderItem(objId));
@@ -454,6 +454,7 @@ MediumEditor.extensions = {};
454
454
  isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),
455
455
 
456
456
  // https://github.com/jashkenas/underscore
457
+ // Lonely letter MUST USE the uppercase code
457
458
  keyCode: {
458
459
  BACKSPACE: 8,
459
460
  TAB: 9,
@@ -462,7 +463,8 @@ MediumEditor.extensions = {};
462
463
  SPACE: 32,
463
464
  DELETE: 46,
464
465
  K: 75, // K keycode, and not k
465
- M: 77
466
+ M: 77,
467
+ V: 86
466
468
  },
467
469
 
468
470
  /**
@@ -897,8 +899,7 @@ MediumEditor.extensions = {};
897
899
  range = range.cloneRange();
898
900
  range.setStartAfter(lastNode);
899
901
  range.collapse(true);
900
- selection.removeAllRanges();
901
- selection.addRange(range);
902
+ MediumEditor.selection.selectRange(doc, range);
902
903
  }
903
904
  res = true;
904
905
  }
@@ -1457,6 +1458,17 @@ MediumEditor.extensions = {};
1457
1458
  } else {
1458
1459
  el.parentNode.removeChild(el);
1459
1460
  }
1461
+ },
1462
+
1463
+ guid: function () {
1464
+ function _s4() {
1465
+ return Math
1466
+ .floor((1 + Math.random()) * 0x10000)
1467
+ .toString(16)
1468
+ .substring(1);
1469
+ }
1470
+
1471
+ return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4();
1460
1472
  }
1461
1473
  };
1462
1474
 
@@ -1938,9 +1950,7 @@ MediumEditor.extensions = {};
1938
1950
  range = this.importSelectionMoveCursorPastAnchor(selectionState, range);
1939
1951
  }
1940
1952
 
1941
- var sel = doc.getSelection();
1942
- sel.removeAllRanges();
1943
- sel.addRange(range);
1953
+ this.selectRange(doc, range);
1944
1954
  },
1945
1955
 
1946
1956
  // Utility method called from importSelection only
@@ -2333,16 +2343,12 @@ MediumEditor.extensions = {};
2333
2343
  },
2334
2344
 
2335
2345
  selectNode: function (node, doc) {
2336
- var range = doc.createRange(),
2337
- sel = doc.getSelection();
2338
-
2346
+ var range = doc.createRange();
2339
2347
  range.selectNodeContents(node);
2340
- sel.removeAllRanges();
2341
- sel.addRange(range);
2348
+ this.selectRange(doc, range);
2342
2349
  },
2343
2350
 
2344
2351
  select: function (doc, startNode, startOffset, endNode, endOffset) {
2345
- doc.getSelection().removeAllRanges();
2346
2352
  var range = doc.createRange();
2347
2353
  range.setStart(startNode, startOffset);
2348
2354
  if (endNode) {
@@ -2350,7 +2356,7 @@ MediumEditor.extensions = {};
2350
2356
  } else {
2351
2357
  range.collapse(true);
2352
2358
  }
2353
- doc.getSelection().addRange(range);
2359
+ this.selectRange(doc, range);
2354
2360
  return range;
2355
2361
  },
2356
2362
 
@@ -2387,6 +2393,13 @@ MediumEditor.extensions = {};
2387
2393
  return selection.getRangeAt(0);
2388
2394
  },
2389
2395
 
2396
+ selectRange: function (ownerDocument, range) {
2397
+ var selection = ownerDocument.getSelection();
2398
+
2399
+ selection.removeAllRanges();
2400
+ selection.addRange(range);
2401
+ },
2402
+
2390
2403
  // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
2391
2404
  // by You
2392
2405
  getSelectionStart: function (ownerDocument) {
@@ -2417,18 +2430,26 @@ MediumEditor.extensions = {};
2417
2430
 
2418
2431
  // Helpers for event handling
2419
2432
 
2420
- attachDOMEvent: function (target, event, listener, useCapture) {
2421
- target.addEventListener(event, listener, useCapture);
2422
- this.events.push([target, event, listener, useCapture]);
2433
+ attachDOMEvent: function (targets, event, listener, useCapture) {
2434
+ targets = MediumEditor.util.isElement(targets) || [window, document].indexOf(targets) > -1 ? [targets] : targets;
2435
+
2436
+ Array.prototype.forEach.call(targets, function (target) {
2437
+ target.addEventListener(event, listener, useCapture);
2438
+ this.events.push([target, event, listener, useCapture]);
2439
+ }.bind(this));
2423
2440
  },
2424
2441
 
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
- }
2442
+ detachDOMEvent: function (targets, event, listener, useCapture) {
2443
+ var index, e;
2444
+ targets = MediumEditor.util.isElement(targets) || [window, document].indexOf(targets) > -1 ? [targets] : targets;
2445
+
2446
+ Array.prototype.forEach.call(targets, function (target) {
2447
+ index = this.indexOfListener(target, event, listener, useCapture);
2448
+ if (index !== -1) {
2449
+ e = this.events.splice(index, 1)[0];
2450
+ e[0].removeEventListener(e[1], e[2], e[3]);
2451
+ }
2452
+ }.bind(this));
2432
2453
  },
2433
2454
 
2434
2455
  indexOfListener: function (target, event, listener, useCapture) {
@@ -2450,6 +2471,30 @@ MediumEditor.extensions = {};
2450
2471
  }
2451
2472
  },
2452
2473
 
2474
+ detachAllEventsFromElement: function (element) {
2475
+ var filtered = this.events.filter(function (e) {
2476
+ return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index');
2477
+ });
2478
+
2479
+ for (var i = 0, len = filtered.length; i < len; i++) {
2480
+ var e = filtered[i];
2481
+ this.detachDOMEvent(e[0], e[1], e[2], e[3]);
2482
+ }
2483
+ },
2484
+
2485
+ // Attach all existing handlers to a new element
2486
+ attachAllEventsToElement: function (element) {
2487
+ if (this.listeners['editableInput']) {
2488
+ this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
2489
+ }
2490
+
2491
+ if (this.eventsCache) {
2492
+ this.eventsCache.forEach(function (e) {
2493
+ this.attachDOMEvent(element, e['name'], e['handler'].bind(this));
2494
+ }, this);
2495
+ }
2496
+ },
2497
+
2453
2498
  enableCustomEvent: function (event) {
2454
2499
  if (this.disabledEvents[event] !== undefined) {
2455
2500
  delete this.disabledEvents[event];
@@ -2641,15 +2686,15 @@ MediumEditor.extensions = {};
2641
2686
  break;
2642
2687
  case 'editableInput':
2643
2688
  // setup cache for knowing when the content has changed
2644
- this.contentCache = [];
2689
+ this.contentCache = {};
2645
2690
  this.base.elements.forEach(function (element) {
2646
2691
  this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
2692
+ }, this);
2647
2693
 
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));
2694
+ // Attach to the 'oninput' event, handled correctly by most browsers
2695
+ if (this.InputEventOnContenteditableSupported) {
2696
+ this.attachToEachElement('input', this.handleInput);
2697
+ }
2653
2698
 
2654
2699
  // For browsers which don't support the input event on contenteditable (IE)
2655
2700
  // we'll attach to 'selectionchange' on the document and 'keypress' on the editables
@@ -2719,9 +2764,26 @@ MediumEditor.extensions = {};
2719
2764
  },
2720
2765
 
2721
2766
  attachToEachElement: function (name, handler) {
2767
+ // build our internal cache to know which element got already what handler attached
2768
+ if (!this.eventsCache) {
2769
+ this.eventsCache = [];
2770
+ }
2771
+
2722
2772
  this.base.elements.forEach(function (element) {
2723
2773
  this.attachDOMEvent(element, name, handler.bind(this));
2724
2774
  }, this);
2775
+
2776
+ this.eventsCache.push({ 'name': name, 'handler': handler });
2777
+ },
2778
+
2779
+ cleanupElement: function (element) {
2780
+ var index = element.getAttribute('medium-editor-index');
2781
+ if (index) {
2782
+ this.detachAllEventsFromElement(element);
2783
+ if (this.contentCache) {
2784
+ delete this.contentCache[index];
2785
+ }
2786
+ }
2725
2787
  },
2726
2788
 
2727
2789
  focusElement: function (element) {
@@ -2793,12 +2855,14 @@ MediumEditor.extensions = {};
2793
2855
  }
2794
2856
  // An event triggered which signifies that the user may have changed someting
2795
2857
  // 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]) {
2858
+ var index = target.getAttribute('medium-editor-index'),
2859
+ html = target.innerHTML;
2860
+
2861
+ if (html !== this.contentCache[index]) {
2798
2862
  // The content has changed since the last time we checked, fire the event
2799
2863
  this.triggerCustomEvent('editableInput', eventObj, target);
2800
2864
  }
2801
- this.contentCache[index] = target.innerHTML;
2865
+ this.contentCache[index] = html;
2802
2866
  },
2803
2867
 
2804
2868
  handleDocumentSelectionChange: function (event) {
@@ -3679,12 +3743,12 @@ MediumEditor.extensions = {};
3679
3743
  targetCheckbox = this.getAnchorTargetCheckbox(),
3680
3744
  buttonCheckbox = this.getAnchorButtonCheckbox();
3681
3745
 
3682
- opts = opts || { url: '' };
3746
+ opts = opts || { value: '' };
3683
3747
  // TODO: This is for backwards compatability
3684
3748
  // We don't need to support the 'string' argument in 6.0.0
3685
3749
  if (typeof opts === 'string') {
3686
3750
  opts = {
3687
- url: opts
3751
+ value: opts
3688
3752
  };
3689
3753
  }
3690
3754
 
@@ -3693,7 +3757,7 @@ MediumEditor.extensions = {};
3693
3757
  MediumEditor.extensions.form.prototype.showForm.apply(this);
3694
3758
  this.setToolbarPosition();
3695
3759
 
3696
- input.value = opts.url;
3760
+ input.value = opts.value;
3697
3761
  input.focus();
3698
3762
 
3699
3763
  // If we have a target checkbox, we want it to be checked/unchecked
@@ -3730,11 +3794,11 @@ MediumEditor.extensions = {};
3730
3794
  var targetCheckbox = this.getAnchorTargetCheckbox(),
3731
3795
  buttonCheckbox = this.getAnchorButtonCheckbox(),
3732
3796
  opts = {
3733
- url: this.getInput().value.trim()
3797
+ value: this.getInput().value.trim()
3734
3798
  };
3735
3799
 
3736
3800
  if (this.linkValidation) {
3737
- opts.url = this.checkLinkFormat(opts.url);
3801
+ opts.value = this.checkLinkFormat(opts.value);
3738
3802
  }
3739
3803
 
3740
3804
  opts.target = '_self';
@@ -3771,7 +3835,7 @@ MediumEditor.extensions = {};
3771
3835
  return 'tel:' + value;
3772
3836
  } else {
3773
3837
  // Check for URL scheme and default to http:// if none found
3774
- return (urlSchemeRegex.test(value) ? '' : 'http://') + value;
3838
+ return (urlSchemeRegex.test(value) ? '' : 'http://') + encodeURI(value);
3775
3839
  }
3776
3840
  },
3777
3841
 
@@ -3988,6 +4052,15 @@ MediumEditor.extensions = {};
3988
4052
 
3989
4053
  attachToEditables: function () {
3990
4054
  this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
4055
+ this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this));
4056
+ },
4057
+
4058
+ handlePositionedToolbar: function () {
4059
+ // If the toolbar is visible and positioned, we don't need to hide the preview
4060
+ // when showWhenToolbarIsVisible is true
4061
+ if (!this.showWhenToolbarIsVisible) {
4062
+ this.hidePreview();
4063
+ }
3991
4064
  },
3992
4065
 
3993
4066
  handleClick: function (event) {
@@ -4537,8 +4610,12 @@ MediumEditor.extensions = {};
4537
4610
  event.preventDefault();
4538
4611
  event.stopPropagation();
4539
4612
 
4613
+ // command can be a function to execute
4614
+ if (typeof data.command === 'function') {
4615
+ data.command.apply(this);
4616
+ }
4540
4617
  // command can be false so the shortcut is just disabled
4541
- if (false !== data.command) {
4618
+ else if (false !== data.command) {
4542
4619
  this.execAction(data.command);
4543
4620
  }
4544
4621
  }
@@ -4709,7 +4786,7 @@ MediumEditor.extensions = {};
4709
4786
  if (font === '') {
4710
4787
  this.clearFontName();
4711
4788
  } else {
4712
- this.execAction('fontName', { name: font });
4789
+ this.execAction('fontName', { value: font });
4713
4790
  }
4714
4791
  },
4715
4792
 
@@ -4887,7 +4964,7 @@ MediumEditor.extensions = {};
4887
4964
  if (size === '4') {
4888
4965
  this.clearFontSize();
4889
4966
  } else {
4890
- this.execAction('fontSize', { size: size });
4967
+ this.execAction('fontSize', { value: size });
4891
4968
  }
4892
4969
  },
4893
4970
 
@@ -4913,6 +4990,16 @@ MediumEditor.extensions = {};
4913
4990
  }());
4914
4991
  (function () {
4915
4992
  'use strict';
4993
+
4994
+ /* Helpers and internal variables that don't need to be members of actual paste handler */
4995
+
4996
+ var pasteBinDefaultContent = '%ME_PASTEBIN%',
4997
+ lastRange = null,
4998
+ keyboardPasteEditable = null,
4999
+ stopProp = function (event) {
5000
+ event.stopPropagation();
5001
+ };
5002
+
4916
5003
  /*jslint regexp: true*/
4917
5004
  /*
4918
5005
  jslint does not allow character negation, because the negation
@@ -4922,6 +5009,15 @@ MediumEditor.extensions = {};
4922
5009
  */
4923
5010
  function createReplacements() {
4924
5011
  return [
5012
+ // Remove anything but the contents within the BODY element
5013
+ [new RegExp(/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/g), ''],
5014
+
5015
+ // cleanup comments added by Chrome when pasting html
5016
+ [new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''],
5017
+
5018
+ // Trailing BR elements
5019
+ [new RegExp(/<br>$/i), ''],
5020
+
4925
5021
  // replace two bogus tags that begin pastes from google docs
4926
5022
  [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],
4927
5023
  [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''],
@@ -4953,13 +5049,47 @@ MediumEditor.extensions = {};
4953
5049
  // Microsoft Word makes these odd tags, like <o:p></o:p>
4954
5050
  [new RegExp(/<\/?o:[a-z]*>/gi), ''],
4955
5051
 
4956
- // cleanup comments added by Chrome when pasting html
4957
- ['<!--EndFragment-->', ''],
4958
- ['<!--StartFragment-->', '']
5052
+ // Microsoft Word adds some special elements around list items
5053
+ [new RegExp(/<!\[if !supportLists\]>(((?!<!).)*)<!\[endif]\>/gi), '$1']
4959
5054
  ];
4960
5055
  }
4961
5056
  /*jslint regexp: false*/
4962
5057
 
5058
+ /**
5059
+ * Gets various content types out of the Clipboard API. It will also get the
5060
+ * plain text using older IE and WebKit API.
5061
+ *
5062
+ * @param {event} event Event fired on paste.
5063
+ * @param {win} reference to window
5064
+ * @param {doc} reference to document
5065
+ * @return {Object} Object with mime types and data for those mime types.
5066
+ */
5067
+ function getClipboardContent(event, win, doc) {
5068
+ var dataTransfer = event.clipboardData || win.clipboardData || doc.dataTransfer,
5069
+ data = {};
5070
+
5071
+ if (!dataTransfer) {
5072
+ return data;
5073
+ }
5074
+
5075
+ // Use old WebKit/IE API
5076
+ if (dataTransfer.getData) {
5077
+ var legacyText = dataTransfer.getData('Text');
5078
+ if (legacyText && legacyText.length > 0) {
5079
+ data['text/plain'] = legacyText;
5080
+ }
5081
+ }
5082
+
5083
+ if (dataTransfer.types) {
5084
+ for (var i = 0; i < dataTransfer.types.length; i++) {
5085
+ var contentType = dataTransfer.types[i];
5086
+ data[contentType] = dataTransfer.getData(contentType);
5087
+ }
5088
+ }
5089
+
5090
+ return data;
5091
+ }
5092
+
4963
5093
  var PasteHandler = MediumEditor.Extension.extend({
4964
5094
  /* Paste Options */
4965
5095
 
@@ -5004,58 +5134,212 @@ MediumEditor.extensions = {};
5004
5134
 
5005
5135
  if (this.forcePlainText || this.cleanPastedHTML) {
5006
5136
  this.subscribe('editablePaste', this.handlePaste.bind(this));
5137
+ this.subscribe('editableKeydown', this.handleKeydown.bind(this));
5007
5138
  }
5008
5139
  },
5009
5140
 
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;
5141
+ destroy: function () {
5142
+ // Make sure pastebin is destroyed in case it's still around for some reason
5143
+ if (this.forcePlainText || this.cleanPastedHTML) {
5144
+ this.removePasteBin();
5145
+ }
5146
+ },
5147
+
5148
+ handlePaste: function (event, editable) {
5149
+ if (event.defaultPrevented) {
5150
+ return;
5151
+ }
5152
+
5153
+ var clipboardContent = getClipboardContent(event, this.window, this.document),
5154
+ pastedHTML = clipboardContent['text/html'],
5155
+ pastedPlain = clipboardContent['text/plain'];
5156
+
5157
+ if (this.window.clipboardData && event.clipboardData === undefined && !pastedHTML) {
5021
5158
  // If window.clipboardData exists, but event.clipboardData doesn't exist,
5022
5159
  // we're probably in IE. IE only has two possibilities for clipboard
5023
5160
  // data format: 'Text' and 'URL'.
5024
5161
  //
5025
- // Of the two, we want 'Text':
5026
- dataFormatHTML = 'Text';
5027
- dataFormatPlain = 'Text';
5162
+ // For IE, we'll fallback to 'Text' for text/html
5163
+ pastedHTML = pastedPlain;
5028
5164
  }
5029
5165
 
5030
- if (event.clipboardData &&
5031
- event.clipboardData.getData &&
5032
- !event.defaultPrevented) {
5166
+ if (pastedHTML || pastedPlain) {
5033
5167
  event.preventDefault();
5034
5168
 
5035
- pastedHTML = event.clipboardData.getData(dataFormatHTML);
5036
- pastedPlain = event.clipboardData.getData(dataFormatPlain);
5169
+ this.doPaste(pastedHTML, pastedPlain, editable);
5170
+ }
5171
+ },
5037
5172
 
5038
- if (this.cleanPastedHTML && pastedHTML) {
5039
- return this.cleanPaste(pastedHTML);
5040
- }
5173
+ doPaste: function (pastedHTML, pastedPlain, editable) {
5174
+ var paragraphs,
5175
+ html = '',
5176
+ p;
5041
5177
 
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
- }
5178
+ if (this.cleanPastedHTML && pastedHTML) {
5179
+ return this.cleanPaste(pastedHTML);
5180
+ }
5181
+
5182
+ if (!(this.getEditorOption('disableReturn') || (editable && editable.getAttribute('data-disable-return')))) {
5183
+ paragraphs = pastedPlain.split(/[\r\n]+/g);
5184
+ // If there are no \r\n in data, don't wrap in <p>
5185
+ if (paragraphs.length > 1) {
5186
+ for (p = 0; p < paragraphs.length; p += 1) {
5187
+ if (paragraphs[p] !== '') {
5188
+ html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
5050
5189
  }
5051
- } else {
5052
- html = MediumEditor.util.htmlEntities(paragraphs[0]);
5053
5190
  }
5054
5191
  } else {
5055
- html = MediumEditor.util.htmlEntities(pastedPlain);
5192
+ html = MediumEditor.util.htmlEntities(paragraphs[0]);
5193
+ }
5194
+ } else {
5195
+ html = MediumEditor.util.htmlEntities(pastedPlain);
5196
+ }
5197
+ MediumEditor.util.insertHTMLCommand(this.document, html);
5198
+ },
5199
+
5200
+ handlePasteBinPaste: function (event) {
5201
+ if (event.defaultPrevented) {
5202
+ this.removePasteBin();
5203
+ return;
5204
+ }
5205
+
5206
+ var clipboardContent = getClipboardContent(event, this.window, this.document),
5207
+ pastedHTML = clipboardContent['text/html'],
5208
+ pastedPlain = clipboardContent['text/plain'],
5209
+ editable = keyboardPasteEditable;
5210
+
5211
+ // If we have valid html already, or we're not in cleanPastedHTML mode
5212
+ // we can ignore the paste bin and just paste now
5213
+ if (!this.cleanPastedHTML || pastedHTML) {
5214
+ event.preventDefault();
5215
+ this.removePasteBin();
5216
+ this.doPaste(pastedHTML, pastedPlain, editable);
5217
+ return;
5218
+ }
5219
+
5220
+ // We need to look at the paste bin, so do a setTimeout to let the paste
5221
+ // fall through into the paste bin
5222
+ setTimeout(function () {
5223
+ // Only look for HTML if we're in cleanPastedHTML mode
5224
+ if (this.cleanPastedHTML) {
5225
+ // If clipboard didn't have HTML, try the paste bin
5226
+ pastedHTML = this.getPasteBinHtml();
5227
+ }
5228
+
5229
+ // If we needed the paste bin, we're done with it now, remove it
5230
+ this.removePasteBin();
5231
+
5232
+ // Handle the paste with the html from the paste bin
5233
+ this.doPaste(pastedHTML, pastedPlain, editable);
5234
+ }.bind(this), 0);
5235
+ },
5236
+
5237
+ handleKeydown: function (event, editable) {
5238
+ // if it's not Ctrl+V, do nothing
5239
+ if (!(MediumEditor.util.isKey(event, MediumEditor.util.keyCode.V) && MediumEditor.util.isMetaCtrlKey(event))) {
5240
+ return;
5241
+ }
5242
+
5243
+ event.stopImmediatePropagation();
5244
+
5245
+ this.removePasteBin();
5246
+ this.createPasteBin(editable);
5247
+ },
5248
+
5249
+ createPasteBin: function (editable) {
5250
+ var rects,
5251
+ range = MediumEditor.selection.getSelectionRange(this.document),
5252
+ top = this.window.pageYOffset;
5253
+
5254
+ keyboardPasteEditable = editable;
5255
+
5256
+ if (range) {
5257
+ rects = range.getClientRects();
5258
+
5259
+ // on empty line, rects is empty so we grab information from the first container of the range
5260
+ if (rects.length) {
5261
+ top += rects[0].top;
5262
+ } else {
5263
+ top += range.startContainer.getBoundingClientRect().top;
5056
5264
  }
5057
- MediumEditor.util.insertHTMLCommand(this.document, html);
5058
5265
  }
5266
+
5267
+ lastRange = range;
5268
+
5269
+ var pasteBinElm = this.document.createElement('div');
5270
+ pasteBinElm.id = this.pasteBinId = 'medium-editor-pastebin-' + (+Date.now());
5271
+ pasteBinElm.setAttribute('style', 'border: 1px red solid; position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0');
5272
+ pasteBinElm.setAttribute('contentEditable', true);
5273
+ pasteBinElm.innerHTML = pasteBinDefaultContent;
5274
+
5275
+ this.document.body.appendChild(pasteBinElm);
5276
+
5277
+ // avoid .focus() to stop other event (actually the paste event)
5278
+ this.on(pasteBinElm, 'focus', stopProp);
5279
+ this.on(pasteBinElm, 'focusin', stopProp);
5280
+ this.on(pasteBinElm, 'focusout', stopProp);
5281
+
5282
+ pasteBinElm.focus();
5283
+
5284
+ MediumEditor.selection.selectNode(pasteBinElm, this.document);
5285
+
5286
+ if (!this.boundHandlePaste) {
5287
+ this.boundHandlePaste = this.handlePasteBinPaste.bind(this);
5288
+ }
5289
+
5290
+ this.on(pasteBinElm, 'paste', this.boundHandlePaste);
5291
+ },
5292
+
5293
+ removePasteBin: function () {
5294
+ if (null !== lastRange) {
5295
+ MediumEditor.selection.selectRange(this.document, lastRange);
5296
+ lastRange = null;
5297
+ }
5298
+
5299
+ if (null !== keyboardPasteEditable) {
5300
+ keyboardPasteEditable = null;
5301
+ }
5302
+
5303
+ var pasteBinElm = this.getPasteBin();
5304
+ if (!pasteBinElm) {
5305
+ return;
5306
+ }
5307
+
5308
+ if (pasteBinElm) {
5309
+ this.off(pasteBinElm, 'focus', stopProp);
5310
+ this.off(pasteBinElm, 'focusin', stopProp);
5311
+ this.off(pasteBinElm, 'focusout', stopProp);
5312
+ this.off(pasteBinElm, 'paste', this.boundHandlePaste);
5313
+ pasteBinElm.parentElement.removeChild(pasteBinElm);
5314
+ }
5315
+ },
5316
+
5317
+ getPasteBin: function () {
5318
+ return this.document.getElementById(this.pasteBinId);
5319
+ },
5320
+
5321
+ getPasteBinHtml: function () {
5322
+ var pasteBinElm = this.getPasteBin();
5323
+
5324
+ if (!pasteBinElm) {
5325
+ return false;
5326
+ }
5327
+
5328
+ // WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
5329
+ // so we need to force plain text mode in this case
5330
+ if (pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
5331
+ return false;
5332
+ }
5333
+
5334
+ var pasteBinHtml = pasteBinElm.innerHTML;
5335
+
5336
+ // If paste bin is empty try using plain text mode
5337
+ // since that is better than nothing right
5338
+ if (!pasteBinHtml || pasteBinHtml === pasteBinDefaultContent) {
5339
+ return false;
5340
+ }
5341
+
5342
+ return pasteBinHtml;
5059
5343
  },
5060
5344
 
5061
5345
  cleanPaste: function (text) {
@@ -5134,16 +5418,19 @@ MediumEditor.extensions = {};
5134
5418
  MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
5135
5419
  },
5136
5420
 
5421
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5137
5422
  isCommonBlock: function (el) {
5138
5423
  return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
5139
5424
  },
5140
5425
 
5426
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5141
5427
  filterCommonBlocks: function (el) {
5142
5428
  if (/^\s*$/.test(el.textContent) && el.parentNode) {
5143
5429
  el.parentNode.removeChild(el);
5144
5430
  }
5145
5431
  },
5146
5432
 
5433
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5147
5434
  filterLineBreak: function (el) {
5148
5435
  if (this.isCommonBlock(el.previousElementSibling)) {
5149
5436
  // remove stray br's following common block elements
@@ -5157,6 +5444,7 @@ MediumEditor.extensions = {};
5157
5444
  }
5158
5445
  },
5159
5446
 
5447
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5160
5448
  // remove an element, including its parent, if it is the only element within its parent
5161
5449
  removeWithParent: function (el) {
5162
5450
  if (el && el.parentNode) {
@@ -5168,6 +5456,7 @@ MediumEditor.extensions = {};
5168
5456
  }
5169
5457
  },
5170
5458
 
5459
+ // TODO (6.0): Make this an internal helper instead of member of paste handler
5171
5460
  cleanupSpans: function (containerEl) {
5172
5461
  var i,
5173
5462
  el,
@@ -5825,30 +6114,26 @@ MediumEditor.extensions = {};
5825
6114
 
5826
6115
  setToolbarPosition: function () {
5827
6116
  var container = this.base.getFocusedElement(),
5828
- selection = this.window.getSelection(),
5829
- anchorPreview;
6117
+ selection = this.window.getSelection();
5830
6118
 
5831
6119
  // If there isn't a valid selection, bail
5832
6120
  if (!container) {
5833
6121
  return this;
5834
6122
  }
5835
6123
 
5836
- if (this.static && !this.relativeContainer) {
5837
- this.showToolbar();
5838
- this.positionStaticToolbar(container);
5839
- } else if (!selection.isCollapsed) {
6124
+ if (this.static || !selection.isCollapsed) {
5840
6125
  this.showToolbar();
5841
6126
 
5842
6127
  // we don't need any absolute positioning if relativeContainer is set
5843
6128
  if (!this.relativeContainer) {
5844
- this.positionToolbar(selection);
6129
+ if (this.static) {
6130
+ this.positionStaticToolbar(container);
6131
+ } else {
6132
+ this.positionToolbar(selection);
6133
+ }
5845
6134
  }
5846
- }
5847
-
5848
- anchorPreview = this.base.getExtensionByName('anchor-preview');
5849
6135
 
5850
- if (anchorPreview && typeof anchorPreview.hidePreview === 'function') {
5851
- anchorPreview.hidePreview();
6136
+ this.trigger('positionedToolbar', {}, this.base.getFocusedElement());
5852
6137
  }
5853
6138
  },
5854
6139
 
@@ -6173,7 +6458,9 @@ MediumEditor.extensions = {};
6173
6458
  return;
6174
6459
  }
6175
6460
 
6176
- if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0) {
6461
+ // https://github.com/yabwe/medium-editor/issues/994
6462
+ // Firefox thrown an error when calling `formatBlock` on an empty editable blockContainer that's not a <div>
6463
+ if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0 && !MediumEditor.util.isBlockContainer(node)) {
6177
6464
  this.options.ownerDocument.execCommand('formatBlock', false, 'p');
6178
6465
  }
6179
6466
 
@@ -6194,6 +6481,13 @@ MediumEditor.extensions = {};
6194
6481
  }
6195
6482
  }
6196
6483
 
6484
+ function handleEditableInput(event, editable) {
6485
+ var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id="' + editable.getAttribute('medium-editor-textarea-id') + '"]');
6486
+ if (textarea) {
6487
+ textarea.value = editable.innerHTML.trim();
6488
+ }
6489
+ }
6490
+
6197
6491
  // Internal helper methods which shouldn't be exposed externally
6198
6492
 
6199
6493
  function addToEditors(win) {
@@ -6227,30 +6521,50 @@ MediumEditor.extensions = {};
6227
6521
  win._mediumEditors[this.id] = null;
6228
6522
  }
6229
6523
 
6230
- function createElementsArray(selector) {
6524
+ function createElementsArray(selector, doc, filterEditorElements) {
6525
+ var elements = [];
6526
+
6231
6527
  if (!selector) {
6232
6528
  selector = [];
6233
6529
  }
6234
6530
  // If string, use as query selector
6235
6531
  if (typeof selector === 'string') {
6236
- selector = this.options.ownerDocument.querySelectorAll(selector);
6532
+ selector = doc.querySelectorAll(selector);
6237
6533
  }
6238
6534
  // If element, put into array
6239
6535
  if (MediumEditor.util.isElement(selector)) {
6240
6536
  selector = [selector];
6241
6537
  }
6242
- // Convert NodeList (or other array like object) into an array
6243
- var elements = Array.prototype.slice.apply(selector);
6244
6538
 
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);
6539
+ if (filterEditorElements) {
6540
+ // Remove elements that have already been initialized by the editor
6541
+ // selecotr might not be an array (ie NodeList) so use for loop
6542
+ for (var i = 0; i < selector.length; i++) {
6543
+ var el = selector[i];
6544
+ if (MediumEditor.util.isElement(el) &&
6545
+ !el.getAttribute('data-medium-editor-element') &&
6546
+ !el.getAttribute('medium-editor-textarea-id')) {
6547
+ elements.push(el);
6548
+ }
6252
6549
  }
6253
- }, this);
6550
+ } else {
6551
+ // Convert NodeList (or other array like object) into an array
6552
+ elements = Array.prototype.slice.apply(selector);
6553
+ }
6554
+
6555
+ return elements;
6556
+ }
6557
+
6558
+ function cleanupTextareaElement(element) {
6559
+ var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]');
6560
+ if (textarea) {
6561
+ // Un-hide the textarea
6562
+ textarea.classList.remove('medium-editor-hidden');
6563
+ textarea.removeAttribute('medium-editor-textarea-id');
6564
+ }
6565
+ if (element.parentNode) {
6566
+ element.parentNode.removeChild(element);
6567
+ }
6254
6568
  }
6255
6569
 
6256
6570
  function setExtensionDefaults(extension, defaults) {
@@ -6328,15 +6642,15 @@ MediumEditor.extensions = {};
6328
6642
  return !this.options.extensions['imageDragging'];
6329
6643
  }
6330
6644
 
6331
- function createContentEditable(textarea, id) {
6332
- var div = this.options.ownerDocument.createElement('div'),
6645
+ function createContentEditable(textarea, id, doc) {
6646
+ var div = doc.createElement('div'),
6333
6647
  now = Date.now(),
6334
6648
  uniqueId = 'medium-editor-' + now + '-' + id,
6335
6649
  atts = textarea.attributes;
6336
6650
 
6337
6651
  // Some browsers can move pretty fast, since we're using a timestamp
6338
6652
  // to make a unique-id, ensure that the id is actually unique on the page
6339
- while (this.options.ownerDocument.getElementById(uniqueId)) {
6653
+ while (doc.getElementById(uniqueId)) {
6340
6654
  now++;
6341
6655
  uniqueId = 'medium-editor-' + now + '-' + id;
6342
6656
  }
@@ -6364,37 +6678,49 @@ MediumEditor.extensions = {};
6364
6678
  return div;
6365
6679
  }
6366
6680
 
6367
- function initElements() {
6368
- var isTextareaUsed = false;
6681
+ function initElement(element, id) {
6682
+ if (!element.getAttribute('data-medium-editor-element')) {
6683
+ if (element.nodeName.toLowerCase() === 'textarea') {
6684
+ element = createContentEditable(element, id, this.options.ownerDocument);
6685
+
6686
+ // Make sure we only attach to editableInput once for <textarea> elements
6687
+ if (!this.instanceHandleEditableInput) {
6688
+ this.instanceHandleEditableInput = handleEditableInput.bind(this);
6689
+ this.subscribe('editableInput', this.instanceHandleEditableInput);
6690
+ }
6691
+ }
6369
6692
 
6370
- this.elements.forEach(function (element, index) {
6371
6693
  if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {
6372
6694
  element.setAttribute('contentEditable', true);
6373
6695
  element.setAttribute('spellcheck', this.options.spellcheck);
6374
6696
  }
6697
+
6698
+ // Make sure we only attach to editableKeydownEnter once for disable-return options
6699
+ if (!this.instanceHandleEditableKeydownEnter) {
6700
+ if (element.getAttribute('data-disable-return') || element.getAttribute('data-disable-double-return')) {
6701
+ this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
6702
+ this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
6703
+ }
6704
+ }
6705
+
6706
+ // if we're not disabling return, add a handler to help handle cleanup
6707
+ // for certain cases when enter is pressed
6708
+ if (!this.options.disableReturn && !element.getAttribute('data-disable-return')) {
6709
+ this.on(element, 'keyup', handleKeyup.bind(this));
6710
+ }
6711
+
6375
6712
  element.setAttribute('data-medium-editor-element', true);
6376
6713
  element.setAttribute('role', 'textbox');
6377
6714
  element.setAttribute('aria-multiline', true);
6378
- element.setAttribute('medium-editor-index', index);
6379
-
6380
- if (element.hasAttribute('medium-editor-textarea-id')) {
6381
- isTextareaUsed = true;
6382
- }
6383
- }, this);
6715
+ element.setAttribute('medium-editor-index', MediumEditor.util.guid());
6384
6716
 
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));
6717
+ this.events.attachAllEventsToElement(element);
6392
6718
  }
6719
+
6720
+ return element;
6393
6721
  }
6394
6722
 
6395
6723
  function attachHandlers() {
6396
- var i;
6397
-
6398
6724
  // attach to tabs
6399
6725
  this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));
6400
6726
 
@@ -6407,27 +6733,14 @@ MediumEditor.extensions = {};
6407
6733
  this.subscribe('editableKeydownSpace', handleDisableExtraSpaces.bind(this));
6408
6734
  }
6409
6735
 
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
- }
6736
+ // Make sure we only attach to editableKeydownEnter once for disable-return options
6737
+ if (!this.instanceHandleEditableKeydownEnter) {
6738
+ // disabling return or double return
6739
+ if (this.options.disableReturn || this.options.disableDoubleReturn) {
6740
+ this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
6741
+ this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
6419
6742
  }
6420
6743
  }
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
6744
  }
6432
6745
 
6433
6746
  function initExtensions() {
@@ -6519,7 +6832,8 @@ MediumEditor.extensions = {};
6519
6832
  /*jslint regexp: true*/
6520
6833
  var appendAction = /^append-(.+)$/gi,
6521
6834
  justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */
6522
- match;
6835
+ match,
6836
+ cmdValueArgument;
6523
6837
  /*jslint regexp: false*/
6524
6838
 
6525
6839
  // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
@@ -6530,11 +6844,21 @@ MediumEditor.extensions = {};
6530
6844
  }
6531
6845
 
6532
6846
  if (action === 'fontSize') {
6533
- return this.options.ownerDocument.execCommand('fontSize', false, opts.size);
6847
+ // TODO: Deprecate support for opts.size in 6.0.0
6848
+ if (opts.size) {
6849
+ MediumEditor.util.deprecated('.size option for fontSize command', '.value', '6.0.0');
6850
+ }
6851
+ cmdValueArgument = opts.value || opts.size;
6852
+ return this.options.ownerDocument.execCommand('fontSize', false, cmdValueArgument);
6534
6853
  }
6535
6854
 
6536
6855
  if (action === 'fontName') {
6537
- return this.options.ownerDocument.execCommand('fontName', false, opts.name);
6856
+ // TODO: Deprecate support for opts.name in 6.0.0
6857
+ if (opts.name) {
6858
+ MediumEditor.util.deprecated('.name option for fontName command', '.value', '6.0.0');
6859
+ }
6860
+ cmdValueArgument = opts.value || opts.name;
6861
+ return this.options.ownerDocument.execCommand('fontName', false, cmdValueArgument);
6538
6862
  }
6539
6863
 
6540
6864
  if (action === 'createLink') {
@@ -6558,7 +6882,8 @@ MediumEditor.extensions = {};
6558
6882
  return result;
6559
6883
  }
6560
6884
 
6561
- return this.options.ownerDocument.execCommand(action, false, null);
6885
+ cmdValueArgument = opts && opts.value;
6886
+ return this.options.ownerDocument.execCommand(action, false, cmdValueArgument);
6562
6887
  }
6563
6888
 
6564
6889
  /* If we've just justified text within a container block
@@ -6624,7 +6949,10 @@ MediumEditor.extensions = {};
6624
6949
  return;
6625
6950
  }
6626
6951
 
6627
- createElementsArray.call(this, this.origElements);
6952
+ this.events = new MediumEditor.Events(this);
6953
+ this.elements = [];
6954
+
6955
+ this.addElements(this.origElements);
6628
6956
 
6629
6957
  if (this.elements.length === 0) {
6630
6958
  return;
@@ -6633,10 +6961,7 @@ MediumEditor.extensions = {};
6633
6961
  this.isActive = true;
6634
6962
  addToEditors.call(this, this.options.contentWindow);
6635
6963
 
6636
- this.events = new MediumEditor.Events(this);
6637
-
6638
6964
  // Call initialization helpers
6639
- initElements.call(this);
6640
6965
  initExtensions.call(this);
6641
6966
  attachHandlers.call(this);
6642
6967
  },
@@ -6671,40 +6996,45 @@ MediumEditor.extensions = {};
6671
6996
  element.removeAttribute('medium-editor-index');
6672
6997
 
6673
6998
  // 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
- }
6999
+ if (element.getAttribute('medium-editor-textarea-id')) {
7000
+ cleanupTextareaElement(element);
6683
7001
  }
6684
7002
  }, this);
6685
7003
  this.elements = [];
7004
+ this.instanceHandleEditableKeydownEnter = null;
7005
+ this.instanceHandleEditableInput = null;
6686
7006
 
6687
7007
  removeFromEditors.call(this, this.options.contentWindow);
6688
7008
  },
6689
7009
 
6690
7010
  on: function (target, event, listener, useCapture) {
6691
7011
  this.events.attachDOMEvent(target, event, listener, useCapture);
7012
+
7013
+ return this;
6692
7014
  },
6693
7015
 
6694
7016
  off: function (target, event, listener, useCapture) {
6695
7017
  this.events.detachDOMEvent(target, event, listener, useCapture);
7018
+
7019
+ return this;
6696
7020
  },
6697
7021
 
6698
7022
  subscribe: function (event, listener) {
6699
7023
  this.events.attachCustomEvent(event, listener);
7024
+
7025
+ return this;
6700
7026
  },
6701
7027
 
6702
7028
  unsubscribe: function (event, listener) {
6703
7029
  this.events.detachCustomEvent(event, listener);
7030
+
7031
+ return this;
6704
7032
  },
6705
7033
 
6706
7034
  trigger: function (name, data, editable) {
6707
7035
  this.events.triggerCustomEvent(name, data, editable);
7036
+
7037
+ return this;
6708
7038
  },
6709
7039
 
6710
7040
  delay: function (fn) {
@@ -6719,8 +7049,10 @@ MediumEditor.extensions = {};
6719
7049
  serialize: function () {
6720
7050
  var i,
6721
7051
  elementid,
6722
- content = {};
6723
- for (i = 0; i < this.elements.length; i += 1) {
7052
+ content = {},
7053
+ len = this.elements.length;
7054
+
7055
+ for (i = 0; i < len; i += 1) {
6724
7056
  elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
6725
7057
  content[elementid] = {
6726
7058
  value: this.elements[i].innerHTML.trim()
@@ -6954,7 +7286,8 @@ MediumEditor.extensions = {};
6954
7286
 
6955
7287
  createLink: function (opts) {
6956
7288
  var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
6957
- customEvent = {};
7289
+ customEvent = {},
7290
+ targetUrl;
6958
7291
 
6959
7292
  // Make sure the selection is within an element this editor is tracking
6960
7293
  if (this.elements.indexOf(currentEditor) === -1) {
@@ -6963,7 +7296,12 @@ MediumEditor.extensions = {};
6963
7296
 
6964
7297
  try {
6965
7298
  this.events.disableCustomEvent('editableInput');
6966
- if (opts.url && opts.url.trim().length > 0) {
7299
+ // TODO: Deprecate support for opts.url in 6.0.0
7300
+ if (opts.url) {
7301
+ MediumEditor.util.deprecated('.url option for createLink', '.value', '6.0.0');
7302
+ }
7303
+ targetUrl = opts.url || opts.value;
7304
+ if (targetUrl && targetUrl.trim().length > 0) {
6967
7305
  var currentSelection = this.options.contentWindow.getSelection();
6968
7306
  if (currentSelection) {
6969
7307
  var currRange = currentSelection.getRangeAt(0),
@@ -7055,7 +7393,7 @@ MediumEditor.extensions = {};
7055
7393
  }
7056
7394
 
7057
7395
  // Creates the link in the document fragment
7058
- MediumEditor.util.createLink(this.options.ownerDocument, textNodes, opts.url.trim());
7396
+ MediumEditor.util.createLink(this.options.ownerDocument, textNodes, targetUrl.trim());
7059
7397
 
7060
7398
  // Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.
7061
7399
  var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length;
@@ -7067,13 +7405,13 @@ MediumEditor.extensions = {};
7067
7405
 
7068
7406
  this.importSelection(exportedSelection);
7069
7407
  } else {
7070
- this.options.ownerDocument.execCommand('createLink', false, opts.url);
7408
+ this.options.ownerDocument.execCommand('createLink', false, targetUrl);
7071
7409
  }
7072
7410
 
7073
7411
  if (this.options.targetBlank || opts.target === '_blank') {
7074
- MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url);
7412
+ MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
7075
7413
  } else {
7076
- MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url);
7414
+ MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
7077
7415
  }
7078
7416
 
7079
7417
  if (opts.buttonClass) {
@@ -7085,7 +7423,7 @@ MediumEditor.extensions = {};
7085
7423
  if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {
7086
7424
  customEvent = this.options.ownerDocument.createEvent('HTMLEvents');
7087
7425
  customEvent.initEvent('input', true, true, this.options.contentWindow);
7088
- for (var i = 0; i < this.elements.length; i += 1) {
7426
+ for (var i = 0, len = this.elements.length; i < len; i += 1) {
7089
7427
  this.elements[i].dispatchEvent(customEvent);
7090
7428
  }
7091
7429
  }
@@ -7110,8 +7448,56 @@ MediumEditor.extensions = {};
7110
7448
  if (this.elements[index]) {
7111
7449
  var target = this.elements[index];
7112
7450
  target.innerHTML = html;
7113
- this.events.updateInput(target, { target: target, currentTarget: target });
7451
+ this.checkContentChanged(target);
7452
+ }
7453
+ },
7454
+
7455
+ checkContentChanged: function (editable) {
7456
+ editable = editable || MediumEditor.selection.getSelectionElement(this.options.contentWindow);
7457
+ this.events.updateInput(editable, { target: editable, currentTarget: editable });
7458
+ },
7459
+
7460
+ addElements: function (selector) {
7461
+ // Convert elements into an array
7462
+ var elements = createElementsArray(selector, this.options.ownerDocument, true);
7463
+
7464
+ // Do we have elements to add now?
7465
+ if (elements.length === 0) {
7466
+ return false;
7114
7467
  }
7468
+
7469
+ elements.forEach(function (element) {
7470
+ // Initialize all new elements (we check that in those functions don't worry)
7471
+ element = initElement.call(this, element);
7472
+
7473
+ // Add new elements to our internal elements array
7474
+ this.elements.push(element);
7475
+ }, this);
7476
+ },
7477
+
7478
+ removeElements: function (selector) {
7479
+ // Convert elements into an array
7480
+ var elements = createElementsArray(selector, this.options.ownerDocument),
7481
+ toRemove = elements.map(function (el) {
7482
+ // For textareas, make sure we're looking at the editor div and not the textarea itself
7483
+ if (el.getAttribute('medium-editor-textarea-id') && el.parentNode) {
7484
+ return el.parentNode.querySelector('div[medium-editor-textarea-id="' + el.getAttribute('medium-editor-textarea-id') + '"]');
7485
+ } else {
7486
+ return el;
7487
+ }
7488
+ });
7489
+
7490
+ this.elements = this.elements.filter(function (element) {
7491
+ // If this is an element we want to remove
7492
+ if (toRemove.indexOf(element) !== -1) {
7493
+ this.events.cleanupElement(element);
7494
+ if (element.getAttribute('medium-editor-textarea-id')) {
7495
+ cleanupTextareaElement(element);
7496
+ }
7497
+ return false;
7498
+ }
7499
+ return true;
7500
+ }, this);
7115
7501
  }
7116
7502
  };
7117
7503
  }());
@@ -7154,7 +7540,7 @@ MediumEditor.parseVersionString = function (release) {
7154
7540
 
7155
7541
  MediumEditor.version = MediumEditor.parseVersionString.call(this, ({
7156
7542
  // grunt-bump looks for this:
7157
- 'version': '5.15.0'
7543
+ 'version': '5.18.0'
7158
7544
  }).version);
7159
7545
 
7160
7546
  return MediumEditor;
@@ -206,14 +206,15 @@
206
206
  content: "";
207
207
  display: table; }
208
208
 
209
- [data-medium-editor-element] img {
210
- max-width: 100%; }
211
-
212
- [data-medium-editor-element] sub {
213
- vertical-align: sub; }
214
-
215
- [data-medium-editor-element] sup {
216
- vertical-align: super; }
209
+ [data-medium-editor-element] {
210
+ word-wrap: break-word;
211
+ min-height: 30px; }
212
+ [data-medium-editor-element] img {
213
+ max-width: 100%; }
214
+ [data-medium-editor-element] sub {
215
+ vertical-align: sub; }
216
+ [data-medium-editor-element] sup {
217
+ vertical-align: super; }
217
218
 
218
219
  .medium-editor-hidden {
219
220
  display: none; }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scrivito_editors
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scrivito
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-01 00:00:00.000000000 Z
11
+ date: 2016-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jquery-ui-rails
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: 1.3.1
47
+ version: 1.4.0.rc1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: 1.3.1
54
+ version: 1.4.0.rc1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: scrivito_sdk
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - '='
60
60
  - !ruby/object:Gem::Version
61
- version: 1.3.1
61
+ version: 1.4.0.rc1
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - '='
67
67
  - !ruby/object:Gem::Version
68
- version: 1.3.1
68
+ version: 1.4.0.rc1
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: coffee-rails
71
71
  requirement: !ruby/object:Gem::Requirement