wysihtml-rails 0.5.2 → 0.5.3

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license wysihtml v0.5.2
2
+ * @license wysihtml v0.5.3
3
3
  * https://github.com/Voog/wysihtml
4
4
  *
5
5
  * Author: Christopher Blum (https://github.com/tiff)
@@ -10,7 +10,7 @@
10
10
  *
11
11
  */
12
12
  var wysihtml5 = {
13
- version: "0.5.2",
13
+ version: "0.5.3",
14
14
 
15
15
  // namespaces
16
16
  commands: {},
@@ -24,6 +24,8 @@ var wysihtml5 = {
24
24
  INVISIBLE_SPACE: "\uFEFF",
25
25
  INVISIBLE_SPACE_REG_EXP: /\uFEFF/g,
26
26
 
27
+ VOID_ELEMENTS: "area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr",
28
+
27
29
  EMPTY_FUNCTION: function() {},
28
30
 
29
31
  ELEMENT_NODE: 1,
@@ -421,7 +423,19 @@ var wysihtml5 = {
421
423
  return all;
422
424
  };
423
425
 
426
+ var isInDom = function(node) {
427
+ var doc = node.ownerDocument,
428
+ n = node;
424
429
 
430
+ do {
431
+ if (n === doc) {
432
+ return true;
433
+ }
434
+ n = n.parentNode;
435
+ } while(n);
436
+
437
+ return false;
438
+ };
425
439
 
426
440
  var normalizeFix = function() {
427
441
  var f = Node.prototype.normalize;
@@ -482,7 +496,7 @@ var wysihtml5 = {
482
496
  aoffset = Array.prototype.indexOf.call(aelement.parentNode.childNodes, aelement);
483
497
  }
484
498
 
485
- if (anode && anode.parentNode && fnode && fnode.parentNode) {
499
+ if (isInDom(this) && anode && anode.parentNode && fnode && fnode.parentNode) {
486
500
  r.setStart(anode, aoffset);
487
501
  r.setEnd(fnode, foffset);
488
502
  s.removeAllRanges();
@@ -504,6 +518,31 @@ var wysihtml5 = {
504
518
  } else {
505
519
  F();
506
520
  }
521
+
522
+ // CustomEvent for ie9 and up
523
+ function nativeCustomEventSupported() {
524
+ try {
525
+ var p = new CustomEvent('cat', {detail: {foo: 'bar'}});
526
+ return 'cat' === p.type && 'bar' === p.detail.foo;
527
+ } catch (e) {}
528
+ return false;
529
+ }
530
+ var customEventSupported = nativeCustomEventSupported();
531
+
532
+ // Polyfills CustomEvent object for IE9 and up
533
+ (function() {
534
+ if (!customEventSupported && "CustomEvent" in window) {
535
+ function CustomEvent(event, params) {
536
+ params = params || {bubbles: false, cancelable: false, detail: undefined};
537
+ var evt = doc.createEvent('CustomEvent');
538
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
539
+ return evt;
540
+ }
541
+ CustomEvent.prototype = win.Event.prototype;
542
+ win.CustomEvent = CustomEvent;
543
+ customEventSupported = true;
544
+ }
545
+ })();
507
546
  };
508
547
 
509
548
  wysihtml5.polyfills(window, document);
@@ -6677,10 +6716,11 @@ wysihtml5.browser = (function() {
6677
6716
  var userAgent = navigator.userAgent,
6678
6717
  testElement = document.createElement("div"),
6679
6718
  // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
6680
- isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1,
6681
- isWebKit = userAgent.indexOf("AppleWebKit/") !== -1,
6682
- isChrome = userAgent.indexOf("Chrome/") !== -1,
6683
- isOpera = userAgent.indexOf("Opera/") !== -1;
6719
+ // We need to be extra careful about Microsoft as it shows increasing tendency of tainting its userAgent strings with false feathers
6720
+ isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1 && !isIE(),
6721
+ isWebKit = userAgent.indexOf("AppleWebKit/") !== -1 && !isIE(),
6722
+ isChrome = userAgent.indexOf("Chrome/") !== -1 && !isIE(),
6723
+ isOpera = userAgent.indexOf("Opera/") !== -1 && !isIE();
6684
6724
 
6685
6725
  function iosVersion(userAgent) {
6686
6726
  return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
@@ -6850,14 +6890,15 @@ wysihtml5.browser = (function() {
6850
6890
  */
6851
6891
  supportsCommand: (function() {
6852
6892
  // Following commands are supported but contain bugs in some browsers
6893
+ // TODO: investigate if some of these bugs can be tested without altering selection on page, instead of targeting browsers and versions directly
6853
6894
  var buggyCommands = {
6854
6895
  // formatBlock fails with some tags (eg. <blockquote>)
6855
6896
  "formatBlock": isIE(10, "<="),
6856
6897
  // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
6857
6898
  // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
6858
6899
  // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
6859
- "insertUnorderedList": isIE(9, ">=") || isIE(12, "<="),
6860
- "insertOrderedList": isIE(9, ">=")|| isIE(12, "<=")
6900
+ "insertUnorderedList": isIE(),
6901
+ "insertOrderedList": isIE()
6861
6902
  };
6862
6903
 
6863
6904
  // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
@@ -7014,6 +7055,11 @@ wysihtml5.browser = (function() {
7014
7055
  return isIE();
7015
7056
  },
7016
7057
 
7058
+ /* In IE when deleting with caret at the begining of LI, List get broken into half instead of merging the LI with previous */
7059
+ hasLiDeletingProblem: function() {
7060
+ return isIE();
7061
+ },
7062
+
7017
7063
  hasUndoInContextMenu: function() {
7018
7064
  return isGecko || isChrome || isOpera;
7019
7065
  },
@@ -7047,6 +7093,12 @@ wysihtml5.browser = (function() {
7047
7093
  return isWebKit;
7048
7094
  },
7049
7095
 
7096
+ // In all webkit browsers there are some places where caret can not be placed at the end of blocks and directly before block level element
7097
+ // when startContainer is element.
7098
+ hasCaretBlockElementIssue: function() {
7099
+ return isWebKit;
7100
+ },
7101
+
7050
7102
  supportsMutationEvents: function() {
7051
7103
  return ("MutationEvent" in window);
7052
7104
  },
@@ -7911,6 +7963,20 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
7911
7963
  }
7912
7964
  }
7913
7965
  return isVisible;
7966
+ },
7967
+ lineBreak: function() {
7968
+ return node && node.nodeType === 1 && node.nodeName === "BR";
7969
+ },
7970
+ block: function() {
7971
+ return node && node.nodeType === 1 && node.ownerDocument.defaultView.getComputedStyle(node).display === "block";
7972
+ },
7973
+ // Void elements are elemens that can not have content
7974
+ // In most cases browsers should solve the cases for you when you try to insert content into those,
7975
+ // but IE does not and it is not nice to do so anyway.
7976
+ voidElement: function() {
7977
+ return wysihtml5.dom.domNode(node).test({
7978
+ query: wysihtml5.VOID_ELEMENTS
7979
+ });
7914
7980
  }
7915
7981
  },
7916
7982
 
@@ -8068,6 +8134,29 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
8068
8134
  }
8069
8135
  },
8070
8136
 
8137
+ transferContentTo: function(targetNode, removeOldWrapper) {
8138
+ if (node.nodeType === 1) {
8139
+ if (wysihtml5.dom.domNode(targetNode).is.voidElement()) {
8140
+ while (node.firstChild) {
8141
+ targetNode.parentNode.insertBefore(node.lastChild, targetNode.nextSibling);
8142
+ }
8143
+ } else {
8144
+ while (node.firstChild) {
8145
+ targetNode.appendChild(node.firstChild);
8146
+ }
8147
+ }
8148
+ if (removeOldWrapper) {
8149
+ node.parentNode.removeChild(node);
8150
+ }
8151
+ } else if (node.nodeType === 3 || node.nodeType === 8){
8152
+ if (wysihtml5.dom.domNode(targetNode).is.voidElement()) {
8153
+ targetNode.parentNode.insertBefore(node, targetNode.nextSibling);
8154
+ } else {
8155
+ targetNode.appendChild(node);
8156
+ }
8157
+ }
8158
+ },
8159
+
8071
8160
  /*
8072
8161
  Tests a node against properties, and returns true if matches.
8073
8162
  Tests on principle that all properties defined must have at least one match.
@@ -9541,17 +9630,10 @@ wysihtml5.dom.replaceWithChildNodes = function(node) {
9541
9630
  return;
9542
9631
  }
9543
9632
 
9544
- if (!node.firstChild) {
9545
- node.parentNode.removeChild(node);
9546
- return;
9547
- }
9548
-
9549
- var fragment = node.ownerDocument.createDocumentFragment();
9550
9633
  while (node.firstChild) {
9551
- fragment.appendChild(node.firstChild);
9634
+ node.parentNode.insertBefore(node.firstChild, node);
9552
9635
  }
9553
- node.parentNode.replaceChild(fragment, node);
9554
- node = fragment = null;
9636
+ node.parentNode.removeChild(node);
9555
9637
  };
9556
9638
  ;/**
9557
9639
  * Unwraps an unordered/ordered list
@@ -12096,6 +12178,43 @@ wysihtml5.quirks.ensureProperClearing = (function() {
12096
12178
  return (ret !== this.contain) ? ret : false;
12097
12179
  },
12098
12180
 
12181
+ // Gather info about caret location (caret node, previous and next node)
12182
+ getNodesNearCaret: function() {
12183
+ if (!this.isCollapsed()) {
12184
+ throw "Selection must be caret when using selection.getNodesNearCaret()";
12185
+ }
12186
+
12187
+ var r = this.getOwnRanges(),
12188
+ caretNode, prevNode, nextNode, offset;
12189
+
12190
+ if (r && r.length > 0) {
12191
+ if (r[0].startContainer.nodeType === 1) {
12192
+ caretNode = r[0].startContainer.childNodes[r[0].startOffset - 1];
12193
+ if (!caretNode && r[0].startOffset === 0) {
12194
+ // Is first position before all nodes
12195
+ nextNode = r[0].startContainer.childNodes[0];
12196
+ } else if (caretNode) {
12197
+ prevNode = caretNode.previousSibling;
12198
+ nextNode = caretNode.nextSibling;
12199
+ }
12200
+ } else {
12201
+ caretNode = r[0].startContainer;
12202
+ prevNode = caretNode.previousSibling;
12203
+ nextNode = caretNode.nextSibling;
12204
+ offset = r[0].startOffset;
12205
+ }
12206
+
12207
+ return {
12208
+ "caretNode": caretNode,
12209
+ "prevNode": prevNode,
12210
+ "nextNode": nextNode,
12211
+ "textOffset": offset
12212
+ };
12213
+ }
12214
+
12215
+ return null;
12216
+ },
12217
+
12099
12218
  getSelectionParentsByTag: function(tagName) {
12100
12219
  var nodes = this.getSelectedOwnNodes(),
12101
12220
  curEl, parents = [];
@@ -12167,6 +12286,11 @@ wysihtml5.quirks.ensureProperClearing = (function() {
12167
12286
  startOffset = (sel.isBackwards()) ? sel.focusOffset : sel.anchorOffset,
12168
12287
  rng = this.createRange(), endNode, inTmpCaret;
12169
12288
 
12289
+ // If start is textnode and all is whitespace before caret. Set start offset to 0
12290
+ if (startNode && startNode.nodeType === 3 && (/^\s*$/).test(startNode.data.slice(0, startOffset))) {
12291
+ startOffset = 0;
12292
+ }
12293
+
12170
12294
  // Escape temproray helper nodes if selection in them
12171
12295
  inTmpCaret = wysihtml5.dom.getParentElement(startNode, { query: '._wysihtml5-temp-caret-fix' }, 1);
12172
12296
  if (inTmpCaret) {
@@ -12486,43 +12610,6 @@ wysihtml5.quirks.ensureProperClearing = (function() {
12486
12610
  return nodes;
12487
12611
  },
12488
12612
 
12489
- deblockAndSurround: function(nodeOptions) {
12490
- var tempElement = this.doc.createElement('div'),
12491
- range = rangy.createRange(this.doc),
12492
- tempDivElements,
12493
- tempElements,
12494
- firstChild;
12495
-
12496
- tempElement.className = nodeOptions.className;
12497
-
12498
- this.composer.commands.exec("formatBlock", nodeOptions);
12499
- tempDivElements = this.contain.querySelectorAll("." + nodeOptions.className);
12500
- if (tempDivElements[0]) {
12501
- tempDivElements[0].parentNode.insertBefore(tempElement, tempDivElements[0]);
12502
-
12503
- range.setStartBefore(tempDivElements[0]);
12504
- range.setEndAfter(tempDivElements[tempDivElements.length - 1]);
12505
- tempElements = range.extractContents();
12506
-
12507
- while (tempElements.firstChild) {
12508
- firstChild = tempElements.firstChild;
12509
- if (firstChild.nodeType == 1 && wysihtml5.dom.hasClass(firstChild, nodeOptions.className)) {
12510
- while (firstChild.firstChild) {
12511
- tempElement.appendChild(firstChild.firstChild);
12512
- }
12513
- if (firstChild.nodeName !== "BR") { tempElement.appendChild(this.doc.createElement('br')); }
12514
- tempElements.removeChild(firstChild);
12515
- } else {
12516
- tempElement.appendChild(firstChild);
12517
- }
12518
- }
12519
- } else {
12520
- tempElement = null;
12521
- }
12522
-
12523
- return tempElement;
12524
- },
12525
-
12526
12613
  /**
12527
12614
  * Scroll the current caret position into the view
12528
12615
  * FIXME: This is a bit hacky, there might be a smarter way of doing this
@@ -14500,20 +14587,55 @@ wysihtml5.Commands = Base.extend(
14500
14587
  }
14501
14588
  }
14502
14589
 
14590
+ var isWhitespaceBefore = function (textNode, offset) {
14591
+ var str = textNode.data ? textNode.data.slice(0, offset) : "";
14592
+ return (/^\s*$/).test(str);
14593
+ }
14594
+
14595
+ var isWhitespaceAfter = function (textNode, offset) {
14596
+ var str = textNode.data ? textNode.data.slice(offset) : "";
14597
+ return (/^\s*$/).test(str);
14598
+ }
14599
+
14600
+ var trimBlankTextsAndBreaks = function(fragment) {
14601
+ if (fragment) {
14602
+ while (fragment.firstChild && fragment.firstChild.nodeType === 3 && (/^\s*$/).test(fragment.firstChild.data) && fragment.lastChild !== fragment.firstChild) {
14603
+ fragment.removeChild(fragment.firstChild);
14604
+ }
14605
+
14606
+ while (fragment.lastChild && fragment.lastChild.nodeType === 3 && (/^\s*$/).test(fragment.lastChild.data) && fragment.lastChild !== fragment.firstChild) {
14607
+ fragment.removeChild(fragment.lastChild);
14608
+ }
14609
+
14610
+ if (fragment.firstChild && fragment.firstChild.nodeType === 1 && fragment.firstChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
14611
+ fragment.removeChild(fragment.firstChild);
14612
+ }
14613
+
14614
+ if (fragment.lastChild && fragment.lastChild.nodeType === 1 && fragment.lastChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
14615
+ fragment.removeChild(fragment.lastChild);
14616
+ }
14617
+ }
14618
+ }
14619
+
14503
14620
  // Wrap the range with a block level element
14504
14621
  // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
14505
14622
  function wrapRangeWithElement(range, options, closestBlockName, composer) {
14506
14623
  var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
14507
14624
  r = range.cloneRange(),
14508
14625
  rangeStartContainer = r.startContainer,
14509
- prevNode = wysihtml5.dom.domNode(getRangeNode(r.startContainer, r.startOffset)).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
14510
- nextNode = wysihtml5.dom.domNode(getRangeNode(r.endContainer, r.endOffset)).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
14626
+ startNode = getRangeNode(r.startContainer, r.startOffset),
14627
+ endNode = getRangeNode(r.endContainer, r.endOffset),
14628
+ prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml5.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
14629
+ nextNode = ((r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] === endNode) || (r.endContainer === endNode && endNode.nodeType === 3 && !isWhitespaceAfter(endNode, r.endOffset))) ? endNode : wysihtml5.dom.domNode(getRangeNode(r.endContainer, r.endOffset)).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
14511
14630
  content = r.extractContents(),
14512
14631
  fragment = composer.doc.createDocumentFragment(),
14513
14632
  similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
14514
14633
  splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
14515
14634
  firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
14516
- wrapper, blocks, children;
14635
+ wrapper, blocks, children,
14636
+ firstc, lastC;
14637
+
14638
+ trimBlankTextsAndBreaks(content);
14517
14639
 
14518
14640
  if (options && options.nodeName === "BLOCKQUOTE") {
14519
14641
 
@@ -14641,6 +14763,29 @@ wysihtml5.Commands = Base.extend(
14641
14763
  return options;
14642
14764
  }
14643
14765
 
14766
+ function caretIsOnEmptyLine(composer) {
14767
+ var caretInfo;
14768
+ if (composer.selection.isCollapsed()) {
14769
+ caretInfo = composer.selection.getNodesNearCaret();
14770
+ if (caretInfo && caretInfo.caretNode) {
14771
+ if (
14772
+ // caret is allready breaknode
14773
+ wysihtml5.dom.domNode(caretInfo.caretNode).is.lineBreak() ||
14774
+ // caret is textnode
14775
+ (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml5.dom.domNode(caretInfo.prevNode).is.lineBreak())) ||
14776
+ // Caret is temprorary rangy selection marker
14777
+ (caretInfo.caretNode.nodeType === 1 && caretInfo.caretNode.classList.contains('rangySelectionBoundary') &&
14778
+ (!caretInfo.prevNode || wysihtml5.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml5.dom.domNode(caretInfo.prevNode).is.block()) &&
14779
+ (!caretInfo.nextNode || wysihtml5.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml5.dom.domNode(caretInfo.nextNode).is.block())
14780
+ )
14781
+ ) {
14782
+ return true;
14783
+ }
14784
+ }
14785
+ }
14786
+ return false;
14787
+ }
14788
+
14644
14789
  wysihtml5.commands.formatBlock = {
14645
14790
  exec: function(composer, command, options) {
14646
14791
  options = parseOptions(options);
@@ -14663,7 +14808,11 @@ wysihtml5.Commands = Base.extend(
14663
14808
  // If selection is caret expand it to cover nearest suitable block element or row if none found
14664
14809
  if (composer.selection.isCollapsed()) {
14665
14810
  bookmark = rangy.saveSelection(composer.win);
14666
- expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
14811
+ if (caretIsOnEmptyLine(composer)) {
14812
+ composer.selection.selectLine();
14813
+ } else {
14814
+ expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
14815
+ }
14667
14816
  }
14668
14817
  if (options) {
14669
14818
  newBlockElements = formatSelection("apply", composer, options);
@@ -15719,20 +15868,19 @@ wysihtml5.Commands = Base.extend(
15719
15868
  };
15720
15869
 
15721
15870
  var createListFallback = function(nodeName, composer) {
15722
- var sel;
15723
-
15724
- if (!composer.selection.isCollapsed()) {
15725
- sel = rangy.saveSelection(composer.win);
15726
- }
15871
+ var sel = rangy.saveSelection(composer.win);
15727
15872
 
15728
15873
  // Fallback for Create list
15729
15874
  var tempClassName = "_wysihtml5-temp-" + new Date().getTime(),
15730
- tempElement = composer.selection.deblockAndSurround({
15731
- "nodeName": "div",
15732
- "className": tempClassName
15733
- }),
15734
15875
  isEmpty, list;
15735
15876
 
15877
+ composer.commands.exec("formatBlock", {
15878
+ "nodeName": "div",
15879
+ "className": tempClassName
15880
+ });
15881
+
15882
+ var tempElement = composer.element.querySelector("." + tempClassName);
15883
+
15736
15884
  // This space causes new lists to never break on enter
15737
15885
  var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
15738
15886
  tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, "");
@@ -16946,8 +17094,11 @@ wysihtml5.views.View = Base.extend(
16946
17094
  function adjust(selectedNode) {
16947
17095
  var parentElement = dom.getParentElement(selectedNode, { query: "p, div" }, 2);
16948
17096
  if (parentElement && dom.contains(that.element, parentElement)) {
16949
- that.selection.executeAndRestore(function() {
17097
+ that.selection.executeAndRestoreRangy(function() {
16950
17098
  if (that.config.useLineBreaks) {
17099
+ if (!parentElement.firstChild || (parentElement.firstChild === parentElement.lastChild && parentElement.firstChild.nodeType === 1 && parentElement.firstChild.classList.contains('rangySelectionBoundary'))) {
17100
+ parentElement.appendChild(that.doc.createElement('br'));
17101
+ }
16951
17102
  dom.replaceWithChildNodes(parentElement);
16952
17103
  } else if (parentElement.nodeName !== "P") {
16953
17104
  dom.renameElement(parentElement, "p");
@@ -16956,18 +17107,21 @@ wysihtml5.views.View = Base.extend(
16956
17107
  }
16957
17108
  }
16958
17109
 
17110
+ // Ensures when editor is empty and not line breaks mode, the inital state has a paragraph in it on focus with caret inside paragraph
16959
17111
  if (!this.config.useLineBreaks) {
16960
- dom.observe(this.element, ["focus", "keydown"], function() {
17112
+ dom.observe(this.element, ["focus"], function() {
16961
17113
  if (that.isEmpty()) {
16962
- var paragraph = that.doc.createElement("P");
16963
- that.element.innerHTML = "";
16964
- that.element.appendChild(paragraph);
16965
- if (!browser.displaysCaretInEmptyContentEditableCorrectly()) {
16966
- paragraph.innerHTML = "<br>";
16967
- that.selection.setBefore(paragraph.firstChild);
16968
- } else {
16969
- that.selection.selectNode(paragraph, true);
16970
- }
17114
+ setTimeout(function() {
17115
+ var paragraph = that.doc.createElement("P");
17116
+ that.element.innerHTML = "";
17117
+ that.element.appendChild(paragraph);
17118
+ if (!browser.displaysCaretInEmptyContentEditableCorrectly()) {
17119
+ paragraph.innerHTML = "<br>";
17120
+ that.selection.setBefore(paragraph.firstChild);
17121
+ } else {
17122
+ that.selection.selectNode(paragraph, true);
17123
+ }
17124
+ }, 0);
16971
17125
  }
16972
17126
  });
16973
17127
  }
@@ -16975,7 +17129,7 @@ wysihtml5.views.View = Base.extend(
16975
17129
  dom.observe(this.element, "keydown", function(event) {
16976
17130
  var keyCode = event.keyCode;
16977
17131
 
16978
- if (event.shiftKey) {
17132
+ if (event.shiftKey || event.ctrlKey || event.defaultPrevented) {
16979
17133
  return;
16980
17134
  }
16981
17135
 
@@ -17007,11 +17161,9 @@ wysihtml5.views.View = Base.extend(
17007
17161
  }, 0);
17008
17162
  return;
17009
17163
  }
17010
-
17011
17164
  if (that.config.useLineBreaks && keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
17012
17165
  event.preventDefault();
17013
17166
  that.commands.exec("insertLineBreak");
17014
-
17015
17167
  }
17016
17168
  });
17017
17169
  }
@@ -17228,6 +17380,7 @@ wysihtml5.views.View = Base.extend(
17228
17380
  */
17229
17381
  (function(wysihtml5) {
17230
17382
  var dom = wysihtml5.dom,
17383
+ domNode = dom.domNode,
17231
17384
  browser = wysihtml5.browser,
17232
17385
  /**
17233
17386
  * Map keyCodes to query commands
@@ -17278,7 +17431,7 @@ wysihtml5.views.View = Base.extend(
17278
17431
  return true;
17279
17432
  }
17280
17433
  try {
17281
- var ev = new CustomEvent("wysihtml5:uneditable:delete");
17434
+ var ev = new CustomEvent("wysihtml5:uneditable:delete", {bubbles: true, cancelable: false});
17282
17435
  before.node.dispatchEvent(ev);
17283
17436
  } catch (err) {}
17284
17437
  before.node.parentNode.removeChild(before.node);
@@ -17287,16 +17440,19 @@ wysihtml5.views.View = Base.extend(
17287
17440
  return false;
17288
17441
  };
17289
17442
 
17290
- // Deletion with caret in the beginning of headings needs special attention
17291
- // Heading does not concate text to previous block node correctly (browsers do unexpected miracles here especially webkit)
17292
- var fixDeleteInTheBeginnigOfHeading = function(composer) {
17443
+ // Deletion with caret in the beginning of headings and other block elvel elements needs special attention
17444
+ // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit)
17445
+ var fixDeleteInTheBeginningOfBlock = function(composer) {
17293
17446
  var selection = composer.selection,
17294
17447
  prevNode = selection.getPreviousNode();
17295
17448
 
17296
17449
  if (selection.caretIsFirstInSelection() &&
17297
17450
  prevNode &&
17298
17451
  prevNode.nodeType === 1 &&
17299
- (/block/).test(composer.win.getComputedStyle(prevNode).display)
17452
+ (/block/).test(composer.win.getComputedStyle(prevNode).display) &&
17453
+ !domNode(prevNode).test({
17454
+ query: "ol, ul, table, tr, dl"
17455
+ })
17300
17456
  ) {
17301
17457
  if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
17302
17458
  // If heading is empty remove the heading node
@@ -17306,42 +17462,83 @@ wysihtml5.views.View = Base.extend(
17306
17462
  if (prevNode.lastChild) {
17307
17463
  var selNode = prevNode.lastChild,
17308
17464
  selectedNode = selection.getSelectedNode(),
17309
- commonAncestorNode = wysihtml5.dom.domNode(prevNode).commonAncestor(selectedNode, composer.element);
17310
- curNode = commonAncestorNode ? wysihtml5.dom.getParentElement(selectedNode, {
17465
+ commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element);
17466
+ curNode = selectedNode.nodeType === 3 ? selectedNode : wysihtml5.dom.getParentElement(selectedNode, {
17311
17467
  query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote"
17312
- }, false, commonAncestorNode) : null;
17313
-
17314
- if (curNode) {
17315
- while (curNode.firstChild) {
17316
- prevNode.appendChild(curNode.firstChild);
17468
+ }, false, commonAncestorNode || composer.element);
17469
+
17470
+ if (curNode) {
17471
+ domNode(curNode).transferContentTo(prevNode, true);
17472
+ selection.setAfter(selNode);
17473
+ return true;
17474
+ }
17475
+ }
17476
+ }
17477
+ }
17478
+ return false;
17479
+ };
17480
+
17481
+ /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */
17482
+ /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */
17483
+ var fixDeleteInTheBeginningOfLi = function(composer) {
17484
+ if (wysihtml5.browser.hasLiDeletingProblem()) {
17485
+ var selection = composer.selection.getSelection(),
17486
+ aNode = selection.anchorNode,
17487
+ listNode, prevNode, firstNode,
17488
+ isInBeginnig = composer.selection.caretIsFirstInSelection();
17489
+
17490
+ // Fix caret at the beginnig of first textNode in LI
17491
+ if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) {
17492
+ aNode = aNode.parentNode;
17493
+ isInBeginnig = true;
17494
+ }
17495
+
17496
+ if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") {
17497
+ prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
17498
+ if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) {
17499
+ prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
17500
+ }
17501
+ if (prevNode) {
17502
+ firstNode = aNode.firstChild;
17503
+ domNode(aNode).transferContentTo(prevNode, true);
17504
+ if (firstNode) {
17505
+ composer.selection.setBefore(firstNode);
17506
+ } else if (prevNode) {
17507
+ if (prevNode.nodeType === 1) {
17508
+ if (prevNode.lastChild) {
17509
+ composer.selection.setAfter(prevNode.lastChild);
17510
+ } else {
17511
+ composer.selection.selectNode(prevNode);
17317
17512
  }
17318
- selection.setAfter(selNode);
17319
- return true;
17320
- } else if (selectedNode.nodeType === 3) {
17321
- prevNode.appendChild(selectedNode);
17322
- selection.setAfter(selNode);
17323
- return true;
17513
+ } else {
17514
+ composer.selection.setAfter(prevNode);
17324
17515
  }
17516
+ }
17517
+ return true;
17325
17518
  }
17326
17519
  }
17327
17520
  }
17328
17521
  return false;
17329
- };
17522
+ }
17330
17523
 
17331
17524
  var handleDeleteKeyPress = function(event, composer) {
17332
17525
  var selection = composer.selection,
17333
17526
  element = composer.element;
17334
17527
 
17335
17528
  if (selection.isCollapsed()) {
17336
- if (fixDeleteInTheBeginnigOfHeading(composer)) {
17529
+ if (handleUneditableDeletion(composer)) {
17337
17530
  event.preventDefault();
17338
17531
  return;
17339
17532
  }
17340
- if (fixLastBrDeletionInTable(composer)) {
17533
+ if (fixDeleteInTheBeginningOfLi(composer)) {
17341
17534
  event.preventDefault();
17342
17535
  return;
17343
17536
  }
17344
- if (handleUneditableDeletion(composer)) {
17537
+ if (fixDeleteInTheBeginningOfBlock(composer)) {
17538
+ event.preventDefault();
17539
+ return;
17540
+ }
17541
+ if (fixLastBrDeletionInTable(composer)) {
17345
17542
  event.preventDefault();
17346
17543
  return;
17347
17544
  }
@@ -17353,6 +17550,72 @@ wysihtml5.views.View = Base.extend(
17353
17550
  }
17354
17551
  };
17355
17552
 
17553
+ var handleEnterKeyPress = function(event, composer) {
17554
+ if (composer.config.useLineBreaks && !event.shiftKey && !event.ctrlKey) {
17555
+ // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
17556
+
17557
+ var breakNodes = "p, pre, div, blockquote",
17558
+ caretInfo, parent, txtNode;
17559
+
17560
+ if (composer.selection.isCollapsed()) {
17561
+ caretInfo = composer.selection.getNodesNearCaret();
17562
+ if (caretInfo) {
17563
+
17564
+ if (caretInfo.caretNode || caretInfo.nextNode) {
17565
+ parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2);
17566
+ if (parent === composer.element) {
17567
+ parent = undefined;
17568
+ }
17569
+ }
17570
+
17571
+ if (parent && caretInfo.caretNode) {
17572
+ if (domNode(caretInfo.caretNode).is.lineBreak()) {
17573
+
17574
+ if (composer.config.doubleLineBreakEscapesBlock) {
17575
+ // Double enter (enter on blank line) exits block element in useLineBreaks mode.
17576
+ event.preventDefault();
17577
+ caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode);
17578
+
17579
+ // Ensure surplous line breaks are not added to preceding element
17580
+ if (domNode(caretInfo.nextNode).is.lineBreak()) {
17581
+ caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode);
17582
+ }
17583
+
17584
+ var brNode = composer.doc.createElement('br');
17585
+ if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) {
17586
+ parent.parentNode.insertBefore(brNode, parent.nextSibling);
17587
+ } else {
17588
+ composer.selection.splitElementAtCaret(parent, brNode);
17589
+ }
17590
+
17591
+ // Ensure surplous blank lines are not added to preceding element
17592
+ if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) {
17593
+ // Replaces blank lines at the beginning of textnode
17594
+ caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, '');
17595
+ }
17596
+ composer.selection.setBefore(brNode);
17597
+ }
17598
+
17599
+ } else if (caretInfo.caretNode.nodeType === 3 && wysihtml5.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) {
17600
+
17601
+ // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens.
17602
+ // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br
17603
+ event.preventDefault();
17604
+ var br1 = composer.doc.createElement('br'),
17605
+ br2 = composer.doc.createElement('br'),
17606
+ f = composer.doc.createDocumentFragment();
17607
+ f.appendChild(br1);
17608
+ f.appendChild(br2);
17609
+ composer.selection.insertNode(f);
17610
+ composer.selection.setBefore(br2);
17611
+
17612
+ }
17613
+ }
17614
+ }
17615
+ }
17616
+ }
17617
+ };
17618
+
17356
17619
  var handleTabKeyDown = function(composer, element, shiftKey) {
17357
17620
  if (!composer.selection.isCollapsed()) {
17358
17621
  composer.selection.deleteContents();
@@ -17448,26 +17711,6 @@ wysihtml5.views.View = Base.extend(
17448
17711
  }
17449
17712
  };
17450
17713
 
17451
- // TODO: mouseover is not actually a foolproof and obvious place for this, must be changed as it modifies dom on random basis
17452
- // Shows url in tooltip when hovering links or images
17453
- var handleMouseOver = function(event) {
17454
- var titlePrefixes = {
17455
- IMG: "Image: ",
17456
- A: "Link: "
17457
- },
17458
- target = event.target,
17459
- nodeName = target.nodeName,
17460
- title;
17461
-
17462
- if (nodeName !== "A" && nodeName !== "IMG") {
17463
- return;
17464
- }
17465
- if(!target.hasAttribute("title")){
17466
- title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
17467
- target.setAttribute("title", title);
17468
- }
17469
- };
17470
-
17471
17714
  var handleClick = function(event) {
17472
17715
  if (this.config.classNames.uneditableContainer) {
17473
17716
  // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text)
@@ -17534,6 +17777,10 @@ wysihtml5.views.View = Base.extend(
17534
17777
  handleTabKeyDown(this, this.element, event.shiftKey);
17535
17778
  }
17536
17779
 
17780
+ if (keyCode === wysihtml5.ENTER_KEY) {
17781
+ handleEnterKeyPress(event, this);
17782
+ }
17783
+
17537
17784
  };
17538
17785
 
17539
17786
  var handleIframeFocus = function(event) {
@@ -17610,7 +17857,6 @@ wysihtml5.views.View = Base.extend(
17610
17857
  addListeners(this.element, ["drop", "paste", "beforepaste"], handlePaste.bind(this), false);
17611
17858
  this.element.addEventListener("copy", handleCopy.bind(this), false);
17612
17859
  this.element.addEventListener("mousedown", handleMouseDown.bind(this), false);
17613
- this.element.addEventListener("mouseover", handleMouseOver.bind(this), false);
17614
17860
  this.element.addEventListener("click", handleClick.bind(this), false);
17615
17861
  this.element.addEventListener("drop", handleDrop.bind(this), false);
17616
17862
  this.element.addEventListener("keyup", handleKeyUp.bind(this), false);
@@ -17906,6 +18152,9 @@ wysihtml5.views.View = Base.extend(
17906
18152
  parser: wysihtml5.dom.parse,
17907
18153
  // By default wysihtml5 will insert a <br> for line breaks, set this to false to use <p>
17908
18154
  useLineBreaks: true,
18155
+ // Double enter (enter on blank line) exits block element in useLineBreaks mode.
18156
+ // It enables a way of escaping out of block elements and splitting block elements
18157
+ doubleLineBreakEscapesBlock: true,
17909
18158
  // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
17910
18159
  stylesheets: [],
17911
18160
  // Placeholder text to use, defaults to the placeholder attribute on the textarea element