wysihtml-rails 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  module Wysihtml
2
2
  module Rails
3
- VERSION = "0.5.2"
3
+ VERSION = "0.5.3"
4
4
  end
5
5
  end
@@ -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