scrivito_editors 1.2.0 → 1.3.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: a0a7830c14e8fa7edae49655b70760ef7204c5ec
4
- data.tar.gz: 9d83a494684ca1e403464dbbcc5f55314df9db75
3
+ metadata.gz: 1e6fb4fb773e3fffc14b9b3b7df14336507aace3
4
+ data.tar.gz: a315c79989d708d0e9b3f936eb56044ba82319c2
5
5
  SHA512:
6
- metadata.gz: 01efcfe8598b71be0dea504e347e8ca66c0352addb6a37dbfbed5e6317b9c82092a6758320bd68b2877cb44926d7ee2424963597f35ed8138034b68108925d2b
7
- data.tar.gz: 61f3d9f52f7be9f5fadc58efb07c183c971b1d65f236a392df570d21587c3e8ca154ff1199aa3cbccd5f6d9d1e03dbca59f951d7c5654620754cb758da224ada
6
+ metadata.gz: 705a8a34d6c744073fa6ce5f20ea948a737bc1414170b61f827471130e1bdc617248c15c62370303f006fdae5f3d7f297c8aa26f495b74f78563d62119261fad
7
+ data.tar.gz: a7386bdc6a991955e484b1f996b41d1e81459efa3a07fef743606989cc40fcce59557a117edd6c9205525a8c03144e2682c7378110ab95a5edc4da33c6ec7499
@@ -66,7 +66,8 @@
66
66
  return {
67
67
  anchorPreview: false,
68
68
  extensions: {
69
- scrivito_anchor: new ScrivitoAnchor
69
+ scrivito_anchor: new ScrivitoAnchor,
70
+ imageDragging: {}
70
71
  },
71
72
  placeholder: false,
72
73
  toolbar: {
@@ -99,6 +100,7 @@
99
100
  activate: function(element) {
100
101
  return activate(element);
101
102
  },
103
+ default_options: defaultOptions,
102
104
  options: function() {
103
105
  return {};
104
106
  }
@@ -4,7 +4,8 @@
4
4
  */
5
5
 
6
6
  [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-delete { padding-right: 10px; }
7
- [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-delete i {
7
+ [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-delete i,
8
+ [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-delete i:before {
8
9
  -moz-osx-font-smoothing: grayscale;
9
10
  -webkit-font-smoothing: antialiased;
10
11
  color: #666;
@@ -17,9 +18,9 @@
17
18
  vertical-align: middle;
18
19
  }
19
20
  [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-delete i { background: none; }
20
- [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-delete i:after { content: "\F03c"; }
21
+ [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-delete i:before { content: "\F03c"; }
21
22
  [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-tag.active+.tag-editor-delete,
22
- [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-tag.active+.tag-editor-delete i:after {
23
+ [data-scrivito-field-type=stringlist]+.tag-editor .tag-editor-tag.active+.tag-editor-delete i:before {
23
24
  content: "";
24
25
  cursor: text;
25
26
  }
@@ -445,6 +445,11 @@ MediumEditor.extensions = {};
445
445
  // by rg89
446
446
  isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
447
447
 
448
+ isEdge: (/Edge\/\d+/).exec(navigator.userAgent) !== null,
449
+
450
+ // if firefox
451
+ isFF: (navigator.userAgent.toLowerCase().indexOf('firefox') > -1),
452
+
448
453
  // http://stackoverflow.com/a/11752084/569101
449
454
  isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),
450
455
 
@@ -551,7 +556,7 @@ MediumEditor.extensions = {};
551
556
  * not affected in any way.
552
557
  */
553
558
  findOrCreateMatchingTextNodes: function (document, element, match) {
554
- var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),
559
+ var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, null, false),
555
560
  matchedNodes = [],
556
561
  currentTextIndex = 0,
557
562
  startReached = false,
@@ -559,30 +564,41 @@ MediumEditor.extensions = {};
559
564
  newNode = null;
560
565
 
561
566
  while ((currentNode = treeWalker.nextNode()) !== null) {
562
- if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) {
563
- startReached = true;
564
- newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex);
565
- }
566
- if (startReached) {
567
- Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex);
568
- }
569
- if (startReached && currentTextIndex === match.end) {
570
- break; // Found the node(s) corresponding to the link. Break out and move on to the next.
571
- } else if (startReached && currentTextIndex > (match.end + 1)) {
572
- throw new Error('PerformLinking overshot the target!'); // should never happen...
573
- }
567
+ if (currentNode.nodeType > 3) {
568
+ continue;
569
+ } else if (currentNode.nodeType === 3) {
570
+ if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) {
571
+ startReached = true;
572
+ newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex);
573
+ }
574
+ if (startReached) {
575
+ Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex);
576
+ }
577
+ if (startReached && currentTextIndex === match.end) {
578
+ break; // Found the node(s) corresponding to the link. Break out and move on to the next.
579
+ } else if (startReached && currentTextIndex > (match.end + 1)) {
580
+ throw new Error('PerformLinking overshot the target!'); // should never happen...
581
+ }
574
582
 
575
- if (startReached) {
576
- matchedNodes.push(newNode || currentNode);
577
- }
583
+ if (startReached) {
584
+ matchedNodes.push(newNode || currentNode);
585
+ }
578
586
 
579
- currentTextIndex += currentNode.nodeValue.length;
580
- if (newNode !== null) {
581
- currentTextIndex += newNode.nodeValue.length;
582
- // Skip the newNode as we'll already have pushed it to the matches
583
- treeWalker.nextNode();
587
+ currentTextIndex += currentNode.nodeValue.length;
588
+ if (newNode !== null) {
589
+ currentTextIndex += newNode.nodeValue.length;
590
+ // Skip the newNode as we'll already have pushed it to the matches
591
+ treeWalker.nextNode();
592
+ }
593
+ newNode = null;
594
+ } else if (currentNode.tagName.toLowerCase() === 'img') {
595
+ if (!startReached && (match.start <= currentTextIndex)) {
596
+ startReached = true;
597
+ }
598
+ if (startReached) {
599
+ matchedNodes.push(currentNode);
600
+ }
584
601
  }
585
- newNode = null;
586
602
  }
587
603
  return matchedNodes;
588
604
  },
@@ -650,6 +666,10 @@ MediumEditor.extensions = {};
650
666
  * <blockquote> container, they are the elements returned.
651
667
  */
652
668
  splitByBlockElements: function (element) {
669
+ if (element.nodeType !== 3 && element.nodeType !== 1) {
670
+ return [];
671
+ }
672
+
653
673
  var toRet = [],
654
674
  blockElementQuery = MediumEditor.util.blockContainerElementNames.join(',');
655
675
 
@@ -661,7 +681,7 @@ MediumEditor.extensions = {};
661
681
  var child = element.childNodes[i];
662
682
  if (child.nodeType === 3) {
663
683
  toRet.push(child);
664
- } else {
684
+ } else if (child.nodeType === 1) {
665
685
  var blockElements = child.querySelectorAll(blockElementQuery);
666
686
  if (blockElements.length === 0) {
667
687
  toRet.push(child);
@@ -703,6 +723,22 @@ MediumEditor.extensions = {};
703
723
  return nextNode;
704
724
  },
705
725
 
726
+ // Find an element's previous sibling within a medium-editor element
727
+ // If one doesn't exist, find the closest ancestor's previous sibling
728
+ findPreviousSibling: function (node) {
729
+ if (!node || Util.isMediumEditorElement(node)) {
730
+ return false;
731
+ }
732
+
733
+ var previousSibling = node.previousSibling;
734
+ while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) {
735
+ node = node.parentNode;
736
+ previousSibling = node.previousSibling;
737
+ }
738
+
739
+ return previousSibling;
740
+ },
741
+
706
742
  isDescendant: function isDescendant(parent, child, checkEquality) {
707
743
  if (!parent || !child) {
708
744
  return false;
@@ -806,11 +842,20 @@ MediumEditor.extensions = {};
806
842
 
807
843
  // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
808
844
  insertHTMLCommand: function (doc, html) {
809
- var selection, range, el, fragment, node, lastNode, toReplace;
810
-
811
- if (doc.queryCommandSupported('insertHTML')) {
845
+ var selection, range, el, fragment, node, lastNode, toReplace,
846
+ res = false,
847
+ ecArgs = ['insertHTML', false, html];
848
+
849
+ /* Edge's implementation of insertHTML is just buggy right now:
850
+ * - Doesn't allow leading white space at the beginning of an element
851
+ * - Found a case when a <font size="2"> tag was inserted when calling alignCenter inside a blockquote
852
+ *
853
+ * There are likely other bugs, these are just the ones we found so far.
854
+ * For now, let's just use the same fallback we did for IE
855
+ */
856
+ if (!MediumEditor.util.isEdge && doc.queryCommandSupported('insertHTML')) {
812
857
  try {
813
- return doc.execCommand('insertHTML', false, html);
858
+ return doc.execCommand.apply(doc, ecArgs);
814
859
  } catch (ignore) {}
815
860
  }
816
861
 
@@ -855,17 +900,26 @@ MediumEditor.extensions = {};
855
900
  selection.removeAllRanges();
856
901
  selection.addRange(range);
857
902
  }
903
+ res = true;
858
904
  }
905
+
906
+ // https://github.com/yabwe/medium-editor/issues/992
907
+ // If we're monitoring calls to execCommand, notify listeners as if a real call had happened
908
+ if (doc.execCommand.callListeners) {
909
+ doc.execCommand.callListeners(ecArgs, res);
910
+ }
911
+ return res;
859
912
  },
860
913
 
861
914
  execFormatBlock: function (doc, tagName) {
862
915
  // Get the top level block element that contains the selection
863
- var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc));
916
+ var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)),
917
+ childNodes;
864
918
 
865
919
  // Special handling for blockquote
866
920
  if (tagName === 'blockquote') {
867
921
  if (blockContainer) {
868
- var childNodes = Array.prototype.slice.call(blockContainer.childNodes);
922
+ childNodes = Array.prototype.slice.call(blockContainer.childNodes);
869
923
  // Check if the blockquote has a block element as a child (nested blocks)
870
924
  if (childNodes.some(function (childNode) {
871
925
  return Util.isBlockContainer(childNode);
@@ -895,6 +949,28 @@ MediumEditor.extensions = {};
895
949
  if (Util.isIE) {
896
950
  tagName = '<' + tagName + '>';
897
951
  }
952
+
953
+ // When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work.
954
+ // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands
955
+ if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') {
956
+ // For IE, just use outdent
957
+ if (Util.isIE && tagName === '<p>') {
958
+ return doc.execCommand('outdent', false, tagName);
959
+ }
960
+
961
+ // For Firefox and Edge, make sure there's a nested block element before calling outdent
962
+ if ((Util.isFF || Util.isEdge) && tagName === 'p') {
963
+ childNodes = Array.prototype.slice.call(blockContainer.childNodes);
964
+ // If there are some non-block elements we need to wrap everything in a <p> before we outdent
965
+ if (childNodes.some(function (childNode) {
966
+ return !Util.isBlockContainer(childNode);
967
+ })) {
968
+ doc.execCommand('formatBlock', false, tagName);
969
+ }
970
+ return doc.execCommand('outdent', false, tagName);
971
+ }
972
+ }
973
+
898
974
  return doc.execCommand('formatBlock', false, tagName);
899
975
  },
900
976
 
@@ -923,6 +999,25 @@ MediumEditor.extensions = {};
923
999
  }
924
1000
  },
925
1001
 
1002
+ /*
1003
+ * this function is called to explicitly remove the target='_blank' as FF holds on to _blank value even
1004
+ * after unchecking the checkbox on anchor form
1005
+ */
1006
+ removeTargetBlank: function (el, anchorUrl) {
1007
+ var i;
1008
+ if (el.nodeName.toLowerCase() === 'a') {
1009
+ el.removeAttribute('target');
1010
+ } else {
1011
+ el = el.getElementsByTagName('a');
1012
+
1013
+ for (i = 0; i < el.length; i += 1) {
1014
+ if (anchorUrl === el[i].attributes.href.value) {
1015
+ el[i].removeAttribute('target');
1016
+ }
1017
+ }
1018
+ }
1019
+ },
1020
+
926
1021
  addClassToAnchors: function (el, buttonClass) {
927
1022
  var classes = buttonClass.split(' '),
928
1023
  i,
@@ -1228,18 +1323,30 @@ MediumEditor.extensions = {};
1228
1323
  return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1;
1229
1324
  },
1230
1325
 
1326
+ /* Finds the closest ancestor which is a block container element
1327
+ * If element is within editor element but not within any other block element,
1328
+ * the editor element is returned
1329
+ */
1231
1330
  getClosestBlockContainer: function (node) {
1232
1331
  return Util.traverseUp(node, function (node) {
1233
- return Util.isBlockContainer(node);
1332
+ return Util.isBlockContainer(node) || Util.isMediumEditorElement(node);
1234
1333
  });
1235
1334
  },
1236
1335
 
1336
+ /* Finds highest level ancestor element which is a block container element
1337
+ * If element is within editor element but not within any other block element,
1338
+ * the editor element is returned
1339
+ */
1237
1340
  getTopBlockContainer: function (element) {
1238
- var topBlock = element;
1341
+ var topBlock = Util.isBlockContainer(element) ? element : false;
1239
1342
  Util.traverseUp(element, function (el) {
1240
1343
  if (Util.isBlockContainer(el)) {
1241
1344
  topBlock = el;
1242
1345
  }
1346
+ if (!topBlock && Util.isMediumEditorElement(el)) {
1347
+ topBlock = el;
1348
+ return true;
1349
+ }
1243
1350
  return false;
1244
1351
  });
1245
1352
  return topBlock;
@@ -1264,13 +1371,19 @@ MediumEditor.extensions = {};
1264
1371
  return element;
1265
1372
  },
1266
1373
 
1374
+ // TODO: remove getFirstTextNode AND _getFirstTextNode when jumping in 6.0.0 (no code references)
1267
1375
  getFirstTextNode: function (element) {
1376
+ Util.warn('getFirstTextNode is deprecated and will be removed in version 6.0.0');
1377
+ return Util._getFirstTextNode(element);
1378
+ },
1379
+
1380
+ _getFirstTextNode: function (element) {
1268
1381
  if (element.nodeType === 3) {
1269
1382
  return element;
1270
1383
  }
1271
1384
 
1272
1385
  for (var i = 0; i < element.childNodes.length; i++) {
1273
- var textNode = Util.getFirstTextNode(element.childNodes[i]);
1386
+ var textNode = Util._getFirstTextNode(element.childNodes[i]);
1274
1387
  if (textNode !== null) {
1275
1388
  return textNode;
1276
1389
  }
@@ -1666,6 +1779,21 @@ MediumEditor.extensions = {};
1666
1779
  start: start,
1667
1780
  end: start + range.toString().length
1668
1781
  };
1782
+
1783
+ // Check to see if the selection starts with any images
1784
+ // if so we need to make sure the the beginning of the selection is
1785
+ // set correctly when importing selection
1786
+ if (this.doesRangeStartWithImages(range, doc)) {
1787
+ selectionState.startsWithImage = true;
1788
+ }
1789
+
1790
+ // Check to see if the selection has any trailing images
1791
+ // if so, this this means we need to look for them when we import selection
1792
+ var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset);
1793
+ if (trailingImageCount) {
1794
+ selectionState.trailingImageCount = trailingImageCount;
1795
+ }
1796
+
1669
1797
  // If start = 0 there may still be an empty paragraph before it, but we don't care.
1670
1798
  if (start !== 0) {
1671
1799
  var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset);
@@ -1701,33 +1829,106 @@ MediumEditor.extensions = {};
1701
1829
  nodeStack = [],
1702
1830
  charIndex = 0,
1703
1831
  foundStart = false,
1832
+ foundEnd = false,
1833
+ trailingImageCount = 0,
1704
1834
  stop = false,
1705
- nextCharIndex;
1835
+ nextCharIndex,
1836
+ allowRangeToStartAtEndOfNode = false,
1837
+ lastTextNode = null;
1838
+
1839
+ // When importing selection, the start of the selection may lie at the end of an element
1840
+ // or at the beginning of an element. Since visually there is no difference between these 2
1841
+ // we will try to move the selection to the beginning of an element since this is generally
1842
+ // what users will expect and it's a more predictable behavior.
1843
+ //
1844
+ // However, there are some specific cases when we don't want to do this:
1845
+ // 1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true]
1846
+ // 2) The selection starts with an image, which is special since an image doesn't have any 'content'
1847
+ // as far as selection and ranges are concerned
1848
+ // 3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex)
1849
+ //
1850
+ // For these cases, we want the selection to start at a very specific location, so we should NOT
1851
+ // automatically move the cursor to the beginning of the first actual chunk of text
1852
+ if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') {
1853
+ allowRangeToStartAtEndOfNode = true;
1854
+ }
1706
1855
 
1707
1856
  while (!stop && node) {
1708
- if (node.nodeType === 3) {
1857
+ // Only iterate over elements and text nodes
1858
+ if (node.nodeType > 3) {
1859
+ node = nodeStack.pop();
1860
+ continue;
1861
+ }
1862
+
1863
+ // If we hit a text node, we need to add the amount of characters to the overall count
1864
+ if (node.nodeType === 3 && !foundEnd) {
1709
1865
  nextCharIndex = charIndex + node.length;
1866
+ // Check if we're at or beyond the start of the selection we're importing
1710
1867
  if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
1711
- range.setStart(node, selectionState.start - charIndex);
1712
- foundStart = true;
1868
+ // NOTE: We only want to allow a selection to start at the END of an element if
1869
+ // allowRangeToStartAtEndOfNode is true
1870
+ if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) {
1871
+ range.setStart(node, selectionState.start - charIndex);
1872
+ foundStart = true;
1873
+ }
1874
+ // We're at the end of a text node where the selection could start but we shouldn't
1875
+ // make the selection start here because allowRangeToStartAtEndOfNode is false.
1876
+ // However, we should keep a reference to this node in case there aren't any more
1877
+ // text nodes after this, so that we have somewhere to import the selection to
1878
+ else {
1879
+ lastTextNode = node;
1880
+ }
1713
1881
  }
1882
+ // We've found the start of the selection, check if we're at or beyond the end of the selection we're importing
1714
1883
  if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
1715
- range.setEnd(node, selectionState.end - charIndex);
1716
- stop = true;
1884
+ if (!selectionState.trailingImageCount) {
1885
+ range.setEnd(node, selectionState.end - charIndex);
1886
+ stop = true;
1887
+ } else {
1888
+ foundEnd = true;
1889
+ }
1717
1890
  }
1718
1891
  charIndex = nextCharIndex;
1719
1892
  } else {
1720
- var i = node.childNodes.length - 1;
1721
- while (i >= 0) {
1722
- nodeStack.push(node.childNodes[i]);
1723
- i -= 1;
1893
+ if (selectionState.trailingImageCount && foundEnd) {
1894
+ if (node.nodeName.toLowerCase() === 'img') {
1895
+ trailingImageCount++;
1896
+ }
1897
+ if (trailingImageCount === selectionState.trailingImageCount) {
1898
+ // Find which index the image is in its parent's children
1899
+ var endIndex = 0;
1900
+ while (node.parentNode.childNodes[endIndex] !== node) {
1901
+ endIndex++;
1902
+ }
1903
+ range.setEnd(node.parentNode, endIndex + 1);
1904
+ stop = true;
1905
+ }
1906
+ }
1907
+
1908
+ if (!stop && node.nodeType === 1) {
1909
+ // this is an element
1910
+ // add all its children to the stack
1911
+ var i = node.childNodes.length - 1;
1912
+ while (i >= 0) {
1913
+ nodeStack.push(node.childNodes[i]);
1914
+ i -= 1;
1915
+ }
1724
1916
  }
1725
1917
  }
1918
+
1726
1919
  if (!stop) {
1727
1920
  node = nodeStack.pop();
1728
1921
  }
1729
1922
  }
1730
1923
 
1924
+ // If we've gone through the entire text but didn't find the beginning of a text node
1925
+ // to make the selection start at, we should fall back to starting the selection
1926
+ // at the END of the last text node we found
1927
+ if (!foundStart && lastTextNode) {
1928
+ range.setStart(lastTextNode, lastTextNode.length);
1929
+ range.setEnd(lastTextNode, lastTextNode.length);
1930
+ }
1931
+
1731
1932
  if (typeof selectionState.emptyBlocksIndex !== 'undefined') {
1732
1933
  range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range);
1733
1934
  }
@@ -1816,6 +2017,10 @@ MediumEditor.extensions = {};
1816
2017
  }
1817
2018
  }
1818
2019
 
2020
+ if (!targetNode) {
2021
+ targetNode = startBlock;
2022
+ }
2023
+
1819
2024
  // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest
1820
2025
  // element at the beginning of the block
1821
2026
  range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0);
@@ -1839,8 +2044,21 @@ MediumEditor.extensions = {};
1839
2044
  if (node.nodeType !== 3) {
1840
2045
  node = cursorContainer.childNodes[cursorOffset];
1841
2046
  }
1842
- if (node && !MediumEditor.util.isElementAtBeginningOfBlock(node)) {
1843
- return -1;
2047
+ if (node) {
2048
+ // The element isn't at the beginning of a block, so it has content before it
2049
+ if (!MediumEditor.util.isElementAtBeginningOfBlock(node)) {
2050
+ return -1;
2051
+ }
2052
+
2053
+ var previousSibling = MediumEditor.util.findPreviousSibling(node);
2054
+ // If there is no previous sibling, this is the first text element in the editor
2055
+ if (!previousSibling) {
2056
+ return -1;
2057
+ }
2058
+ // If the previous sibling has text, then there are no empty blocks before this
2059
+ else if (previousSibling.nodeValue) {
2060
+ return -1;
2061
+ }
1844
2062
  }
1845
2063
 
1846
2064
  // Walk over block elements, counting number of empty blocks between last piece of text
@@ -1864,6 +2082,138 @@ MediumEditor.extensions = {};
1864
2082
  return emptyBlocksCount;
1865
2083
  },
1866
2084
 
2085
+ // Returns true if the selection range begins with an image tag
2086
+ // Returns false if the range starts with any non empty text nodes
2087
+ doesRangeStartWithImages: function (range, doc) {
2088
+ if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) {
2089
+ return false;
2090
+ }
2091
+
2092
+ if (range.startContainer.nodeName.toLowerCase() === 'img') {
2093
+ return true;
2094
+ }
2095
+
2096
+ var img = range.startContainer.querySelector('img');
2097
+ if (!img) {
2098
+ return false;
2099
+ }
2100
+
2101
+ var treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false);
2102
+ while (treeWalker.nextNode()) {
2103
+ var next = treeWalker.currentNode;
2104
+ // If we hit the image, then there isn't any text before the image so
2105
+ // the image is at the beginning of the range
2106
+ if (next === img) {
2107
+ break;
2108
+ }
2109
+ // If we haven't hit the iamge, but found text that contains content
2110
+ // then the range doesn't start with an image
2111
+ if (next.nodeValue) {
2112
+ return false;
2113
+ }
2114
+ }
2115
+
2116
+ return true;
2117
+ },
2118
+
2119
+ getTrailingImageCount: function (root, selectionState, endContainer, endOffset) {
2120
+ // If the endOffset of a range is 0, the endContainer doesn't contain images
2121
+ // If the endContainer is a text node, there are no trailing images
2122
+ if (endOffset === 0 || endContainer.nodeType !== 1) {
2123
+ return 0;
2124
+ }
2125
+
2126
+ // If the endContainer isn't an image, and doesn't have an image descendants
2127
+ // there are no trailing images
2128
+ if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) {
2129
+ return 0;
2130
+ }
2131
+
2132
+ var lastNode = endContainer.childNodes[endOffset - 1];
2133
+ while (lastNode.hasChildNodes()) {
2134
+ lastNode = lastNode.lastChild;
2135
+ }
2136
+
2137
+ var node = root,
2138
+ nodeStack = [],
2139
+ charIndex = 0,
2140
+ foundStart = false,
2141
+ foundEnd = false,
2142
+ stop = false,
2143
+ nextCharIndex,
2144
+ trailingImages = 0;
2145
+
2146
+ while (!stop && node) {
2147
+ // Only iterate over elements and text nodes
2148
+ if (node.nodeType > 3) {
2149
+ node = nodeStack.pop();
2150
+ continue;
2151
+ }
2152
+
2153
+ if (node.nodeType === 3 && !foundEnd) {
2154
+ trailingImages = 0;
2155
+ nextCharIndex = charIndex + node.length;
2156
+ if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
2157
+ foundStart = true;
2158
+ }
2159
+ if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
2160
+ foundEnd = true;
2161
+ }
2162
+ charIndex = nextCharIndex;
2163
+ } else {
2164
+ if (node.nodeName.toLowerCase() === 'img') {
2165
+ trailingImages++;
2166
+ }
2167
+
2168
+ if (node === lastNode) {
2169
+ stop = true;
2170
+ } else if (node.nodeType === 1) {
2171
+ // this is an element
2172
+ // add all its children to the stack
2173
+ var i = node.childNodes.length - 1;
2174
+ while (i >= 0) {
2175
+ nodeStack.push(node.childNodes[i]);
2176
+ i -= 1;
2177
+ }
2178
+ }
2179
+ }
2180
+
2181
+ if (!stop) {
2182
+ node = nodeStack.pop();
2183
+ }
2184
+ }
2185
+
2186
+ return trailingImages;
2187
+ },
2188
+
2189
+ // determine if the current selection contains any 'content'
2190
+ // content being any non-white space text or an image
2191
+ selectionContainsContent: function (doc) {
2192
+ var sel = doc.getSelection();
2193
+
2194
+ // collapsed selection or selection withour range doesn't contain content
2195
+ if (!sel || sel.isCollapsed || !sel.rangeCount) {
2196
+ return false;
2197
+ }
2198
+
2199
+ // if toString() contains any text, the selection contains some content
2200
+ if (sel.toString().trim() !== '') {
2201
+ return true;
2202
+ }
2203
+
2204
+ // if selection contains only image(s), it will return empty for toString()
2205
+ // so check for an image manually
2206
+ var selectionNode = this.getSelectedParentElement(sel.getRangeAt(0));
2207
+ if (selectionNode) {
2208
+ if (selectionNode.nodeName.toLowerCase() === 'img' ||
2209
+ (selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) {
2210
+ return true;
2211
+ }
2212
+ }
2213
+
2214
+ return false;
2215
+ },
2216
+
1867
2217
  selectionInContentEditableFalse: function (contentWindow) {
1868
2218
  // determine if the current selection is exclusively inside
1869
2219
  // a contenteditable="false", though treat the case of an
@@ -2004,6 +2354,20 @@ MediumEditor.extensions = {};
2004
2354
  return range;
2005
2355
  },
2006
2356
 
2357
+ /**
2358
+ * Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end.
2359
+ *
2360
+ * @param {DomDocument} doc Current document
2361
+ * @param {boolean} moveCursorToStart A boolean representing whether or not to set the caret to the beginning of the prior selection.
2362
+ */
2363
+ clearSelection: function (doc, moveCursorToStart) {
2364
+ if (moveCursorToStart) {
2365
+ doc.getSelection().collapseToStart();
2366
+ } else {
2367
+ doc.getSelection().collapseToEnd();
2368
+ }
2369
+ },
2370
+
2007
2371
  /**
2008
2372
  * Move cursor to the given node with the given offset.
2009
2373
  *
@@ -2049,7 +2413,7 @@ MediumEditor.extensions = {};
2049
2413
  };
2050
2414
 
2051
2415
  Events.prototype = {
2052
- InputEventOnContenteditableSupported: !MediumEditor.util.isIE,
2416
+ InputEventOnContenteditableSupported: !MediumEditor.util.isIE && !MediumEditor.util.isEdge,
2053
2417
 
2054
2418
  // Helpers for event handling
2055
2419
 
@@ -2198,30 +2562,37 @@ MediumEditor.extensions = {};
2198
2562
  return;
2199
2563
  }
2200
2564
 
2565
+ // Helper method to call all listeners to execCommand
2566
+ var callListeners = function (args, result) {
2567
+ if (doc.execCommand.listeners) {
2568
+ doc.execCommand.listeners.forEach(function (listener) {
2569
+ listener({
2570
+ command: args[0],
2571
+ value: args[2],
2572
+ args: args,
2573
+ result: result
2574
+ });
2575
+ });
2576
+ }
2577
+ },
2578
+
2201
2579
  // Create a wrapper method for execCommand which will:
2202
2580
  // 1) Call document.execCommand with the correct arguments
2203
2581
  // 2) Loop through any listeners and notify them that execCommand was called
2204
2582
  // passing extra info on the call
2205
2583
  // 3) Return the result
2206
- var wrapper = function (aCommandName, aShowDefaultUI, aValueArgument) {
2207
- var result = doc.execCommand.orig.apply(this, arguments);
2584
+ wrapper = function () {
2585
+ var result = doc.execCommand.orig.apply(this, arguments);
2208
2586
 
2209
- if (!doc.execCommand.listeners) {
2210
- return result;
2211
- }
2587
+ if (!doc.execCommand.listeners) {
2588
+ return result;
2589
+ }
2212
2590
 
2213
- var args = Array.prototype.slice.call(arguments);
2214
- doc.execCommand.listeners.forEach(function (listener) {
2215
- listener({
2216
- command: aCommandName,
2217
- value: aValueArgument,
2218
- args: args,
2219
- result: result
2220
- });
2221
- });
2591
+ var args = Array.prototype.slice.call(arguments);
2592
+ callListeners(args, result);
2222
2593
 
2223
- return result;
2224
- };
2594
+ return result;
2595
+ };
2225
2596
 
2226
2597
  // Store a reference to the original execCommand
2227
2598
  wrapper.orig = doc.execCommand;
@@ -2229,6 +2600,9 @@ MediumEditor.extensions = {};
2229
2600
  // Attach an array for storing listeners
2230
2601
  wrapper.listeners = [];
2231
2602
 
2603
+ // Helper for notifying listeners
2604
+ wrapper.callListeners = callListeners;
2605
+
2232
2606
  // Overwrite execCommand
2233
2607
  doc.execCommand = wrapper;
2234
2608
  },
@@ -3049,6 +3423,11 @@ MediumEditor.extensions = {};
3049
3423
  formSaveLabel: '&#10003;',
3050
3424
  formCloseLabel: '&times;',
3051
3425
 
3426
+ /* activeClass: [string]
3427
+ * set class which added to shown form
3428
+ */
3429
+ activeClass: 'medium-editor-toolbar-form-active',
3430
+
3052
3431
  /* hasForm: [boolean]
3053
3432
  *
3054
3433
  * Setting this to true will cause getForm() to be called
@@ -3071,14 +3450,34 @@ MediumEditor.extensions = {};
3071
3450
  * This function should return true/false reflecting
3072
3451
  * whether the form is currently displayed
3073
3452
  */
3074
- isDisplayed: function () {},
3453
+ isDisplayed: function () {
3454
+ if (this.hasForm) {
3455
+ return this.getForm().classList.contains(this.activeClass);
3456
+ }
3457
+ return false;
3458
+ },
3459
+
3460
+ /* hideForm: [function ()]
3461
+ *
3462
+ * This function should show the form element inside
3463
+ * the toolbar container
3464
+ */
3465
+ showForm: function () {
3466
+ if (this.hasForm) {
3467
+ this.getForm().classList.add(this.activeClass);
3468
+ }
3469
+ },
3075
3470
 
3076
3471
  /* hideForm: [function ()]
3077
3472
  *
3078
3473
  * This function should hide the form element inside
3079
3474
  * the toolbar container
3080
3475
  */
3081
- hideForm: function () {},
3476
+ hideForm: function () {
3477
+ if (this.hasForm) {
3478
+ this.getForm().classList.remove(this.activeClass);
3479
+ }
3480
+ },
3082
3481
 
3083
3482
  /************************ Helpers ************************
3084
3483
  * The following are helpers that are either set by MediumEditor
@@ -3267,11 +3666,11 @@ MediumEditor.extensions = {};
3267
3666
 
3268
3667
  // Used by medium-editor when the default toolbar is to be displayed
3269
3668
  isDisplayed: function () {
3270
- return this.getForm().style.display === 'block';
3669
+ return MediumEditor.extensions.form.prototype.isDisplayed.apply(this);
3271
3670
  },
3272
3671
 
3273
3672
  hideForm: function () {
3274
- this.getForm().style.display = 'none';
3673
+ MediumEditor.extensions.form.prototype.hideForm.apply(this);
3275
3674
  this.getInput().value = '';
3276
3675
  },
3277
3676
 
@@ -3291,7 +3690,7 @@ MediumEditor.extensions = {};
3291
3690
 
3292
3691
  this.base.saveSelection();
3293
3692
  this.hideToolbarDefaultActions();
3294
- this.getForm().style.display = 'block';
3693
+ MediumEditor.extensions.form.prototype.showForm.apply(this);
3295
3694
  this.setToolbarPosition();
3296
3695
 
3297
3696
  input.value = opts.url;
@@ -3362,8 +3761,18 @@ MediumEditor.extensions = {};
3362
3761
  },
3363
3762
 
3364
3763
  checkLinkFormat: function (value) {
3365
- var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
3366
- return (re.test(value) ? '' : 'http://') + value;
3764
+ // Matches any alphabetical characters followed by ://
3765
+ // Matches protocol relative "//"
3766
+ // Matches common external protocols "mailto:" "tel:" "maps:"
3767
+ var urlSchemeRegex = /^([a-z]+:)?\/\/|^(mailto|tel|maps):/i,
3768
+ // var te is a regex for checking if the string is a telephone number
3769
+ telRegex = /^\+?\s?\(?(?:\d\s?\-?\)?){3,20}$/;
3770
+ if (telRegex.test(value)) {
3771
+ return 'tel:' + value;
3772
+ } else {
3773
+ // Check for URL scheme and default to http:// if none found
3774
+ return (urlSchemeRegex.test(value) ? '' : 'http://') + value;
3775
+ }
3367
3776
  },
3368
3777
 
3369
3778
  doFormCancel: function () {
@@ -3564,12 +3973,16 @@ MediumEditor.extensions = {};
3564
3973
  defaultLeft = diffLeft - halfOffsetWidth;
3565
3974
 
3566
3975
  this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - diffTop + this.window.pageYOffset - this.anchorPreview.offsetHeight) + 'px';
3976
+ this.anchorPreview.style.right = 'initial';
3567
3977
  if (middleBoundary < halfOffsetWidth) {
3568
3978
  this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
3979
+ this.anchorPreview.style.right = 'initial';
3569
3980
  } else if ((this.window.innerWidth - middleBoundary) < halfOffsetWidth) {
3570
- this.anchorPreview.style.left = this.window.innerWidth + defaultLeft - halfOffsetWidth + 'px';
3981
+ this.anchorPreview.style.left = 'auto';
3982
+ this.anchorPreview.style.right = 0;
3571
3983
  } else {
3572
3984
  this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
3985
+ this.anchorPreview.style.right = 'initial';
3573
3986
  }
3574
3987
  },
3575
3988
 
@@ -3751,9 +4164,20 @@ MediumEditor.extensions = {};
3751
4164
  this.document.execCommand('AutoUrlDetect', false, false);
3752
4165
  },
3753
4166
 
4167
+ isLastInstance: function () {
4168
+ var activeInstances = 0;
4169
+ for (var i = 0; i < this.window._mediumEditors.length; i++) {
4170
+ var editor = this.window._mediumEditors[i];
4171
+ if (editor !== null && editor.getExtensionByName('autoLink') !== undefined) {
4172
+ activeInstances++;
4173
+ }
4174
+ }
4175
+ return activeInstances === 1;
4176
+ },
4177
+
3754
4178
  destroy: function () {
3755
4179
  // Turn AutoUrlDetect back on
3756
- if (this.document.queryCommandSupported('AutoUrlDetect')) {
4180
+ if (this.document.queryCommandSupported('AutoUrlDetect') && this.isLastInstance()) {
3757
4181
  this.document.execCommand('AutoUrlDetect', false, true);
3758
4182
  }
3759
4183
  },
@@ -3818,6 +4242,7 @@ MediumEditor.extensions = {};
3818
4242
  documentModified = this.removeObsoleteAutoLinkSpans(blockElements[i]) || documentModified;
3819
4243
  documentModified = this.performLinkingWithinElement(blockElements[i]) || documentModified;
3820
4244
  }
4245
+ this.base.events.updateInput(contenteditable, { target: contenteditable, currentTarget: contenteditable });
3821
4246
  return documentModified;
3822
4247
  },
3823
4248
 
@@ -3992,7 +4417,12 @@ MediumEditor.extensions = {};
3992
4417
  // Prevent file from opening in the current window
3993
4418
  event.preventDefault();
3994
4419
  event.stopPropagation();
3995
-
4420
+ // Select the dropping target, and set the selection to the end of the target
4421
+ // https://github.com/yabwe/medium-editor/issues/980
4422
+ this.base.selectElement(event.target);
4423
+ var selection = this.base.exportSelection();
4424
+ selection.start = selection.end;
4425
+ this.base.importSelection(selection);
3996
4426
  // IE9 does not support the File API, so prevent file from opening in the window
3997
4427
  // but also don't try to actually get the file
3998
4428
  if (event.dataTransfer.files) {
@@ -4016,20 +4446,18 @@ MediumEditor.extensions = {};
4016
4446
  },
4017
4447
 
4018
4448
  insertImageFile: function (file) {
4449
+ if (typeof FileReader !== 'function') {
4450
+ return;
4451
+ }
4019
4452
  var fileReader = new FileReader();
4020
4453
  fileReader.readAsDataURL(file);
4021
4454
 
4022
- var id = 'medium-img-' + (+new Date());
4023
- MediumEditor.util.insertHTMLCommand(this.document, '<img class="medium-editor-image-loading" id="' + id + '" />');
4024
-
4025
- fileReader.onload = function () {
4026
- var img = this.document.getElementById(id);
4027
- if (img) {
4028
- img.removeAttribute('id');
4029
- img.removeAttribute('class');
4030
- img.src = fileReader.result;
4031
- }
4032
- }.bind(this);
4455
+ // attach the onload event handler, makes it easier to listen in with jasmine
4456
+ fileReader.addEventListener('load', function (e) {
4457
+ var addImageElement = this.document.createElement('img');
4458
+ addImageElement.src = e.target.result;
4459
+ MediumEditor.util.insertHTMLCommand(this.document, addImageElement.outerHTML);
4460
+ }.bind(this));
4033
4461
  }
4034
4462
  });
4035
4463
 
@@ -4545,9 +4973,17 @@ MediumEditor.extensions = {};
4545
4973
  */
4546
4974
  cleanPastedHTML: false,
4547
4975
 
4976
+ /* preCleanReplacements: [Array]
4977
+ * custom pairs (2 element arrays) of RegExp and replacement text to use during past when
4978
+ * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
4979
+ * These replacements are executed before any medium editor defined replacements.
4980
+ */
4981
+ preCleanReplacements: [],
4982
+
4548
4983
  /* cleanReplacements: [Array]
4549
4984
  * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when
4550
4985
  * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
4986
+ * These replacements are executed after any medium editor defined replacements.
4551
4987
  */
4552
4988
  cleanReplacements: [],
4553
4989
 
@@ -4625,7 +5061,10 @@ MediumEditor.extensions = {};
4625
5061
  cleanPaste: function (text) {
4626
5062
  var i, elList, tmp, workEl,
4627
5063
  multiline = /<p|<br|<div/.test(text),
4628
- replacements = createReplacements().concat(this.cleanReplacements || []);
5064
+ replacements = [].concat(
5065
+ this.preCleanReplacements || [],
5066
+ createReplacements(),
5067
+ this.cleanReplacements || []);
4629
5068
 
4630
5069
  for (i = 0; i < replacements.length; i += 1) {
4631
5070
  text = text.replace(replacements[i][0], replacements[i][1]);
@@ -4934,6 +5373,11 @@ MediumEditor.extensions = {};
4934
5373
  */
4935
5374
  sticky: false,
4936
5375
 
5376
+ /* stickyTopOffset: [Number]
5377
+ * Value in pixel of the top offset above the toolbar
5378
+ */
5379
+ stickyTopOffset: 0,
5380
+
4937
5381
  /* updateOnEmptySelection: [boolean]
4938
5382
  * When the __static__ option is true, this enables/disables updating
4939
5383
  * the state of the toolbar buttons even when the selection is collapsed
@@ -5282,7 +5726,7 @@ MediumEditor.extensions = {};
5282
5726
  }
5283
5727
 
5284
5728
  // If we don't have a 'valid' selection -> hide toolbar
5285
- if (this.window.getSelection().toString().trim() === '' ||
5729
+ if (!MediumEditor.selection.selectionContainsContent(this.document) ||
5286
5730
  (this.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {
5287
5731
  return this.hideToolbar();
5288
5732
  }
@@ -5426,15 +5870,13 @@ MediumEditor.extensions = {};
5426
5870
 
5427
5871
  if (this.sticky) {
5428
5872
  // If it's beyond the height of the editor, position it at the bottom of the editor
5429
- if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) {
5873
+ if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight - this.stickyTopOffset)) {
5430
5874
  toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
5431
5875
  toolbarElement.classList.remove('medium-editor-sticky-toolbar');
5432
-
5433
5876
  // Stick the toolbar to the top of the window
5434
- } else if (scrollTop > (containerTop - toolbarHeight)) {
5877
+ } else if (scrollTop > (containerTop - toolbarHeight - this.stickyTopOffset)) {
5435
5878
  toolbarElement.classList.add('medium-editor-sticky-toolbar');
5436
- toolbarElement.style.top = '0px';
5437
-
5879
+ toolbarElement.style.top = this.stickyTopOffset + 'px';
5438
5880
  // Normal static toolbar position
5439
5881
  } else {
5440
5882
  toolbarElement.classList.remove('medium-editor-sticky-toolbar');
@@ -5470,10 +5912,22 @@ MediumEditor.extensions = {};
5470
5912
  positionToolbar: function (selection) {
5471
5913
  // position the toolbar at left 0, so we can get the real width of the toolbar
5472
5914
  this.getToolbarElement().style.left = '0';
5915
+ this.getToolbarElement().style.right = 'initial';
5916
+
5917
+ var range = selection.getRangeAt(0),
5918
+ boundary = range.getBoundingClientRect();
5919
+
5920
+ // Handle selections with just images
5921
+ if (!boundary || ((boundary.height === 0 && boundary.width === 0) && range.startContainer === range.endContainer)) {
5922
+ // If there's a nested image, use that for the bounding rectangle
5923
+ if (range.startContainer.nodeType === 1 && range.startContainer.querySelector('img')) {
5924
+ boundary = range.startContainer.querySelector('img').getBoundingClientRect();
5925
+ } else {
5926
+ boundary = range.startContainer.getBoundingClientRect();
5927
+ }
5928
+ }
5473
5929
 
5474
5930
  var windowWidth = this.window.innerWidth,
5475
- range = selection.getRangeAt(0),
5476
- boundary = range.getBoundingClientRect(),
5477
5931
  middleBoundary = (boundary.left + boundary.right) / 2,
5478
5932
  toolbarElement = this.getToolbarElement(),
5479
5933
  toolbarHeight = toolbarElement.offsetHeight,
@@ -5494,10 +5948,13 @@ MediumEditor.extensions = {};
5494
5948
 
5495
5949
  if (middleBoundary < halfOffsetWidth) {
5496
5950
  toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px';
5951
+ toolbarElement.style.right = 'initial';
5497
5952
  } else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
5498
- toolbarElement.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px';
5953
+ toolbarElement.style.left = 'auto';
5954
+ toolbarElement.style.right = 0;
5499
5955
  } else {
5500
5956
  toolbarElement.style.left = defaultLeft + middleBoundary + 'px';
5957
+ toolbarElement.style.right = 'initial';
5501
5958
  }
5502
5959
  }
5503
5960
  });
@@ -5575,7 +6032,7 @@ MediumEditor.extensions = {};
5575
6032
  textContent = node.textContent,
5576
6033
  caretPositions = MediumEditor.selection.getCaretOffsets(node);
5577
6034
 
5578
- if ((textContent[caretPositions.left - 1] === undefined) || (textContent[caretPositions.left - 1] === ' ') || (textContent[caretPositions.left] === undefined)) {
6035
+ if ((textContent[caretPositions.left - 1] === undefined) || (textContent[caretPositions.left - 1].trim() === '') || (textContent[caretPositions.left] !== undefined && textContent[caretPositions.left].trim() === '')) {
5579
6036
  event.preventDefault();
5580
6037
  }
5581
6038
  }
@@ -5637,7 +6094,7 @@ MediumEditor.extensions = {};
5637
6094
  // instead delete previous node and cancel the event.
5638
6095
  node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
5639
6096
  event.preventDefault();
5640
- } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) {
6097
+ } else if (!this.options.disableDoubleReturn && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) {
5641
6098
  // hitting return in the begining of a header will create empty header elements before the current one
5642
6099
  // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
5643
6100
  p = this.options.ownerDocument.createElement('p');
@@ -5697,6 +6154,14 @@ MediumEditor.extensions = {};
5697
6154
  node.parentElement.removeChild(node);
5698
6155
 
5699
6156
  event.preventDefault();
6157
+ } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
6158
+ (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
6159
+ MediumEditor.selection.getCaretOffsets(node).left === 0) {
6160
+
6161
+ // when cursor is at the begining of the element and the element is <blockquote>
6162
+ // then pressing backspace key should change the <blockquote> to a <p> tag
6163
+ event.preventDefault();
6164
+ MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');
5700
6165
  }
5701
6166
  }
5702
6167
 
@@ -5712,16 +6177,19 @@ MediumEditor.extensions = {};
5712
6177
  this.options.ownerDocument.execCommand('formatBlock', false, 'p');
5713
6178
  }
5714
6179
 
5715
- if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) && !MediumEditor.util.isListItem(node)) {
6180
+ // https://github.com/yabwe/medium-editor/issues/834
6181
+ // https://github.com/yabwe/medium-editor/pull/382
6182
+ // Don't call format block if this is a block element (ie h1, figCaption, etc.)
6183
+ if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&
6184
+ !MediumEditor.util.isListItem(node) &&
6185
+ !MediumEditor.util.isBlockContainer(node)) {
6186
+
5716
6187
  tagName = node.nodeName.toLowerCase();
5717
6188
  // For anchor tags, unlink
5718
6189
  if (tagName === 'a') {
5719
6190
  this.options.ownerDocument.execCommand('unlink', false, null);
5720
6191
  } else if (!event.shiftKey && !event.ctrlKey) {
5721
- // only format block if this is not a header tag
5722
- if (!/h\d/.test(tagName)) {
5723
- this.options.ownerDocument.execCommand('formatBlock', false, 'p');
5724
- }
6192
+ this.options.ownerDocument.execCommand('formatBlock', false, 'p');
5725
6193
  }
5726
6194
  }
5727
6195
  }
@@ -5897,6 +6365,8 @@ MediumEditor.extensions = {};
5897
6365
  }
5898
6366
 
5899
6367
  function initElements() {
6368
+ var isTextareaUsed = false;
6369
+
5900
6370
  this.elements.forEach(function (element, index) {
5901
6371
  if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {
5902
6372
  element.setAttribute('contentEditable', true);
@@ -5908,15 +6378,18 @@ MediumEditor.extensions = {};
5908
6378
  element.setAttribute('medium-editor-index', index);
5909
6379
 
5910
6380
  if (element.hasAttribute('medium-editor-textarea-id')) {
5911
- this.on(element, 'input', function (event) {
5912
- var target = event.target,
5913
- textarea = target.parentNode.querySelector('textarea[medium-editor-textarea-id="' + target.getAttribute('medium-editor-textarea-id') + '"]');
5914
- if (textarea) {
5915
- textarea.value = this.serialize()[target.id].value;
5916
- }
5917
- }.bind(this));
6381
+ isTextareaUsed = true;
5918
6382
  }
5919
6383
  }, this);
6384
+
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));
6392
+ }
5920
6393
  }
5921
6394
 
5922
6395
  function attachHandlers() {
@@ -6069,7 +6542,8 @@ MediumEditor.extensions = {};
6069
6542
  }
6070
6543
 
6071
6544
  if (action === 'image') {
6072
- return this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
6545
+ var src = this.options.contentWindow.getSelection().toString().trim();
6546
+ return this.options.ownerDocument.execCommand('insertImage', false, src);
6073
6547
  }
6074
6548
 
6075
6549
  /* Issue: https://github.com/yabwe/medium-editor/issues/595
@@ -6519,7 +6993,7 @@ MediumEditor.extensions = {};
6519
6993
  // but the selection is contained within the same block element
6520
6994
  // we want to make sure we create a single link, and not multiple links
6521
6995
  // which can happen with the built in browser functionality
6522
- if (commonAncestorContainer.nodeType !== 3 && startContainerParentElement === endContainerParentElement) {
6996
+ if (commonAncestorContainer.nodeType !== 3 && commonAncestorContainer.textContent.length !== 0 && startContainerParentElement === endContainerParentElement) {
6523
6997
  var parentElement = (startContainerParentElement || currentEditor),
6524
6998
  fragment = this.options.ownerDocument.createDocumentFragment();
6525
6999
 
@@ -6598,6 +7072,8 @@ MediumEditor.extensions = {};
6598
7072
 
6599
7073
  if (this.options.targetBlank || opts.target === '_blank') {
6600
7074
  MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url);
7075
+ } else {
7076
+ MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url);
6601
7077
  }
6602
7078
 
6603
7079
  if (opts.buttonClass) {
@@ -6678,7 +7154,7 @@ MediumEditor.parseVersionString = function (release) {
6678
7154
 
6679
7155
  MediumEditor.version = MediumEditor.parseVersionString.call(this, ({
6680
7156
  // grunt-bump looks for this:
6681
- 'version': '5.10.0'
7157
+ 'version': '5.15.0'
6682
7158
  }).version);
6683
7159
 
6684
7160
  return MediumEditor;
@@ -17,8 +17,8 @@
17
17
  border-right: 1px solid #9ccea6;
18
18
  background-color: transparent;
19
19
  color: #fff;
20
- -webkit-transition: background-color 0.2s ease-in, color 0.2s ease-in;
21
- transition: background-color 0.2s ease-in, color 0.2s ease-in; }
20
+ -webkit-transition: background-color .2s ease-in, color .2s ease-in;
21
+ transition: background-color .2s ease-in, color .2s ease-in; }
22
22
  .medium-editor-toolbar li button:hover {
23
23
  background-color: #346a3f;
24
24
  color: #fff; }
@@ -89,7 +89,9 @@
89
89
  left: 0;
90
90
  position: absolute;
91
91
  top: 0;
92
- white-space: pre; }
92
+ white-space: pre;
93
+ padding: inherit;
94
+ margin: inherit; }
93
95
 
94
96
  .medium-toolbar-arrow-under:after, .medium-toolbar-arrow-over:before {
95
97
  border-style: solid;
@@ -196,6 +198,9 @@
196
198
  margin: 0 10px;
197
199
  text-decoration: none; }
198
200
 
201
+ .medium-editor-toolbar-form-active {
202
+ display: block; }
203
+
199
204
  .medium-editor-toolbar-actions:after {
200
205
  clear: both;
201
206
  content: "";
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.2.0
4
+ version: 1.3.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-03-14 00:00:00.000000000 Z
11
+ date: 2016-04-20 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.2.0
47
+ version: 1.3.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.2.0
54
+ version: 1.3.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.2.0
61
+ version: 1.3.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.2.0
68
+ version: 1.3.0.rc1
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: coffee-rails
71
71
  requirement: !ruby/object:Gem::Requirement