scrivito_editors 1.3.1 → 1.4.0.rc1

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