wysihtml-rails 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2b94233ba0df90edb3f3d7be49cffac2af98c886
4
- data.tar.gz: bdbdd01f05311262cdf42f443159e38be7cf9630
3
+ metadata.gz: f669d05b4737529b9d67aa2b90ec224ba385c19e
4
+ data.tar.gz: 90aaea2e8cdb31da89b4c0fc2d87d7cfaeeb58be
5
5
  SHA512:
6
- metadata.gz: 193c1f3fd743e30643a974e93aad3321460b889a262ce75dee28ed32b8b51d4fd4bb54d004bc3a9d2c0a1805485beeae104e178059f690ec73a568596457e836
7
- data.tar.gz: ea14ffb5949e3b4ec8b8d3b8d4f0489cf73a340a35c4240133a7b477051f8f9c6c882c30e7c57d0faff9f1f18ae4f6918b6180945c8be83848146ab4da459cdc
6
+ metadata.gz: 42679b7d2407ff704cb3ddff9b0d7ea393cacd7aafda17b4ea8d5aba0530ee602e871d16059168c02543d5162e5f93ba73b0e55c99a23730a5b5a2f94ba9e603
7
+ data.tar.gz: d73d3b367728f467d3018b74caaaac94555ad2a8dd27fedc7193a67bbd8b896a12008bc604f4c6d1034617ca1c7a890b9f5a24f54bb49fded489d84ae4841fbf
@@ -1,5 +1,5 @@
1
1
  module Wysihtml
2
2
  module Rails
3
- VERSION = "0.5.1"
3
+ VERSION = "0.5.2"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license wysihtml v0.5.1
2
+ * @license wysihtml v0.5.2
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.1",
13
+ version: "0.5.2",
14
14
 
15
15
  // namespaces
16
16
  commands: {},
@@ -76,19 +76,19 @@ var wysihtml5 = {
76
76
 
77
77
  // element.textContent polyfill.
78
78
  if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent").get) {
79
- (function() {
80
- var innerText = Object.getOwnPropertyDescriptor(win.Element.prototype, "innerText");
81
- Object.defineProperty(win.Element.prototype, "textContent",
82
- {
83
- get: function() {
84
- return innerText.get.call(this);
85
- },
86
- set: function(s) {
87
- return innerText.set.call(this, s);
88
- }
89
- }
90
- );
91
- })();
79
+ (function() {
80
+ var innerText = Object.getOwnPropertyDescriptor(win.Element.prototype, "innerText");
81
+ Object.defineProperty(win.Element.prototype, "textContent",
82
+ {
83
+ get: function() {
84
+ return innerText.get.call(this);
85
+ },
86
+ set: function(s) {
87
+ return innerText.set.call(this, s);
88
+ }
89
+ }
90
+ );
91
+ })();
92
92
  }
93
93
 
94
94
  // isArray polyfill for ie8
@@ -133,20 +133,36 @@ var wysihtml5 = {
133
133
  };
134
134
  }
135
135
 
136
- // Element.matches Adds ie8 support and unifies nonstandard function names in other browsers
137
- win.Element && function(ElementPrototype) {
138
- ElementPrototype.matches = ElementPrototype.matches ||
139
- ElementPrototype.matchesSelector ||
140
- ElementPrototype.mozMatchesSelector ||
141
- ElementPrototype.msMatchesSelector ||
142
- ElementPrototype.oMatchesSelector ||
143
- ElementPrototype.webkitMatchesSelector ||
144
- function (selector) {
145
- var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
146
- while (nodes[++i] && nodes[i] != node);
147
- return !!nodes[i];
136
+ // closest and matches polyfill
137
+ // https://github.com/jonathantneal/closest
138
+ (function (ELEMENT) {
139
+ ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) {
140
+ var
141
+ element = this,
142
+ elements = (element.document || element.ownerDocument).querySelectorAll(selector),
143
+ index = 0;
144
+
145
+ while (elements[index] && elements[index] !== element) {
146
+ ++index;
147
+ }
148
+
149
+ return elements[index] ? true : false;
148
150
  };
149
- }(win.Element.prototype);
151
+
152
+ ELEMENT.closest = ELEMENT.closest || function closest(selector) {
153
+ var element = this;
154
+
155
+ while (element) {
156
+ if (element.matches(selector)) {
157
+ break;
158
+ }
159
+
160
+ element = element.parentElement;
161
+ }
162
+
163
+ return element;
164
+ };
165
+ }(Element.prototype));
150
166
 
151
167
  // Element.classList for ie8-9 (toggle all IE)
152
168
  // source http://purl.eligrey.com/github/classList.js/blob/master/classList.js
@@ -4335,7 +4351,8 @@ wysihtml5.polyfills(window, document);
4335
4351
  }
4336
4352
 
4337
4353
  return api;
4338
- }, this);;/**
4354
+ }, this);
4355
+ ;/**
4339
4356
  * Text range module for Rangy.
4340
4357
  * Text-based manipulation and searching of ranges and selections.
4341
4358
  *
@@ -7869,6 +7886,11 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
7869
7886
  return nodes;
7870
7887
  }
7871
7888
 
7889
+ // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
7890
+ function isBookmark(n) {
7891
+ return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
7892
+ }
7893
+
7872
7894
  wysihtml5.dom.domNode = function(node) {
7873
7895
  var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE];
7874
7896
 
@@ -7902,6 +7924,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
7902
7924
  }
7903
7925
 
7904
7926
  if (
7927
+ isBookmark(prevNode) || // is Rangy temporary boomark element (bypass)
7905
7928
  (!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
7906
7929
  (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set
7907
7930
  ) {
@@ -7921,6 +7944,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
7921
7944
  }
7922
7945
 
7923
7946
  if (
7947
+ isBookmark(nextNode) || // is Rangy temporary boomark element (bypass)
7924
7948
  (!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
7925
7949
  (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set
7926
7950
  ) {
@@ -8082,7 +8106,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
8082
8106
  }
8083
8107
  }
8084
8108
 
8085
- if (properties.nodeName && node.nodeName !== properties.nodeName) {
8109
+ if (properties.nodeName && node.nodeName.toLowerCase() !== properties.nodeName.toLowerCase()) {
8086
8110
  return false;
8087
8111
  }
8088
8112
 
@@ -12532,15 +12556,40 @@ wysihtml5.quirks.ensureProperClearing = (function() {
12532
12556
  * Select line where the caret is in
12533
12557
  */
12534
12558
  selectLine: function() {
12559
+ var r = rangy.createRange();
12535
12560
  if (wysihtml5.browser.supportsSelectionModify()) {
12536
12561
  this._selectLine_W3C();
12537
- } else if (this.doc.selection) {
12538
- this._selectLine_MSIE();
12539
- } else {
12540
- // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)
12541
- this._selectLineUniversal();
12562
+ } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) {
12563
+ // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)*/
12564
+ this._selectLineUniversal();
12542
12565
  }
12543
12566
  },
12567
+
12568
+ includeRangyRangeHelpers: function() {
12569
+ var s = this.getSelection(),
12570
+ r = s.getRangeAt(0),
12571
+ isHelperNode = function(node) {
12572
+ return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'));
12573
+ },
12574
+ getNodeLength = function (node) {
12575
+ if (node.nodeType === 1) {
12576
+ return node.childNodes && node.childNodes.length || 0;
12577
+ } else {
12578
+ return node.data && node.data.length || 0;
12579
+ }
12580
+ // body...
12581
+ },
12582
+ anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
12583
+ fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode;
12584
+
12585
+ if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) {
12586
+ r.setEndAfter(fnode.nextSibling);
12587
+ }
12588
+ if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) {
12589
+ r.setStartBefore(anode.previousSibling);
12590
+ }
12591
+ r.select();
12592
+ },
12544
12593
 
12545
12594
  /**
12546
12595
  * See https://developer.mozilla.org/en/DOM/Selection/modify
@@ -12559,6 +12608,8 @@ wysihtml5.quirks.ensureProperClearing = (function() {
12559
12608
  selection.focusOffset === initialBoundry[3]
12560
12609
  ) {
12561
12610
  this._selectLineUniversal();
12611
+ } else {
12612
+ this.includeRangyRangeHelpers();
12562
12613
  }
12563
12614
  },
12564
12615
 
@@ -12610,19 +12661,45 @@ wysihtml5.quirks.ensureProperClearing = (function() {
12610
12661
  rect,
12611
12662
  startRange, endRange, testRange,
12612
12663
  count = 0,
12613
- amount, testRect, found;
12664
+ amount, testRect, found,
12665
+ that = this,
12666
+ isLineBreakingElement = function(el) {
12667
+ return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml5.lang.array(['BR', 'HR']).contains(el.nodeName));
12668
+ },
12669
+ prevNode = function(node) {
12670
+ var pnode = node;
12671
+ if (pnode) {
12672
+ while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) {
12673
+ pnode = pnode.previousSibling;
12674
+ }
12675
+ }
12676
+ return pnode;
12677
+ };
12614
12678
 
12615
12679
  startRange = r.cloneRange();
12616
12680
  endRange = r.cloneRange();
12617
12681
 
12618
12682
  if (r.collapsed) {
12619
- r.expand('word', 1);
12620
- rect = r.nativeRange.getBoundingClientRect();
12683
+ // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary
12684
+ // TODO: figure out a shorter and more readable way
12685
+ if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) {
12686
+ r.moveEnd('character', 1);
12687
+ } else if (r.startContainer.nodeType === 1 && r.startContainer.childNodes[r.startOffset] && r.startContainer.childNodes[r.startOffset].nodeType === 3 && r.startContainer.childNodes[r.startOffset].data.length > 0) {
12688
+ r.moveEnd('character', 1);
12689
+ } else if (r.startOffset > 0 && ( r.startContainer.nodeType === 3 || (r.startContainer.nodeType === 1 && !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))))) {
12690
+ r.moveStart('character', -1);
12691
+ }
12621
12692
  }
12622
-
12693
+ if (!r.collapsed) {
12694
+ r.insertNode(this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE));
12695
+ }
12696
+
12697
+ // Is probably just empty line as can not be expanded
12698
+ rect = r.nativeRange.getBoundingClientRect();
12623
12699
  do {
12624
12700
  amount = r.moveStart('character', -1);
12625
12701
  testRect = r.nativeRange.getBoundingClientRect();
12702
+
12626
12703
  if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) {
12627
12704
  r.moveStart('character', 1);
12628
12705
  found = true;
@@ -12638,61 +12715,24 @@ wysihtml5.quirks.ensureProperClearing = (function() {
12638
12715
  testRect = r.nativeRange.getBoundingClientRect();
12639
12716
  if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) {
12640
12717
  r.moveEnd('character', -1);
12718
+
12719
+ // Fix a IE line end marked by linebreak element although caret is before it
12720
+ // If causes problems should be changed to be applied only to IE
12721
+ if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) {
12722
+ if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) {
12723
+ r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length);
12724
+ } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) {
12725
+ r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length);
12726
+ }
12727
+ }
12728
+
12641
12729
  found = true;
12642
12730
  }
12643
12731
  count++;
12644
12732
  } while (amount !== 0 && !found && count < 2000);
12645
12733
 
12646
12734
  r.select();
12647
- },
12648
-
12649
- _selectLine_MSIE: function() {
12650
- var range = this.doc.selection && this.doc.selection.createRange ? this.doc.selection.createRange() : this.doc.createRange(),
12651
- rangeTop = range.boundingTop,
12652
- scrollWidth = this.doc.body.scrollWidth,
12653
- rangeBottom,
12654
- rangeEnd,
12655
- measureNode,
12656
- i,
12657
- j;
12658
-
12659
- window.r = range;
12660
-
12661
- if (!range.moveToPoint) {
12662
- return;
12663
- }
12664
-
12665
- if (rangeTop === 0) {
12666
- // Don't know why, but when the selection ends at the end of a line
12667
- // range.boundingTop is 0
12668
- measureNode = this.doc.createElement("span");
12669
- this.insertNode(measureNode);
12670
- rangeTop = measureNode.offsetTop;
12671
- measureNode.parentNode.removeChild(measureNode);
12672
- }
12673
-
12674
- rangeTop += 1;
12675
-
12676
- for (i=-10; i<scrollWidth; i+=2) {
12677
- try {
12678
- range.moveToPoint(i, rangeTop);
12679
- break;
12680
- } catch(e1) {}
12681
- }
12682
-
12683
- // Investigate the following in order to handle multi line selections
12684
- // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
12685
- rangeBottom = rangeTop;
12686
- rangeEnd = this.doc.selection.createRange();
12687
- for (j=scrollWidth; j>=0; j--) {
12688
- try {
12689
- rangeEnd.moveToPoint(j, rangeBottom);
12690
- break;
12691
- } catch(e2) {}
12692
- }
12693
-
12694
- range.setEndPoint("EndToEnd", rangeEnd);
12695
- range.select();
12735
+ this.includeRangyRangeHelpers();
12696
12736
  },
12697
12737
 
12698
12738
  getText: function() {
@@ -13993,18 +14033,56 @@ wysihtml5.Commands = Base.extend(
13993
14033
  };
13994
14034
  }
13995
14035
 
14036
+ function getRangeNode(node, offset) {
14037
+ if (node.nodeType === 3) {
14038
+ return node;
14039
+ } else {
14040
+ return node.childNodes[offset] || node;
14041
+ }
14042
+ }
14043
+
14044
+ // Returns if node is a line break
14045
+ function isBr(n) {
14046
+ return n && n.nodeType === 1 && n.nodeName === "BR";
14047
+ }
14048
+
14049
+ // Is block level element
14050
+ function isBlock(n, composer) {
14051
+ return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block";
14052
+ }
14053
+
14054
+ // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
14055
+ function isBookmark(n) {
14056
+ return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
14057
+ }
14058
+
14059
+ // Is line breaking node
14060
+ function isLineBreaking(n, composer) {
14061
+ return isBr(n) || isBlock(n, composer);
14062
+ }
14063
+
13996
14064
  // Removes empty block level elements
13997
- function cleanup(composer) {
14065
+ function cleanup(composer, newBlockElements) {
14066
+ wysihtml5.dom.removeInvisibleSpaces(composer.element);
13998
14067
  var container = composer.element,
13999
14068
  allElements = container.querySelectorAll(BLOCK_ELEMENTS),
14000
- uneditables = container.querySelectorAll(composer.config.classNames.uneditableContainer),
14001
- elements = wysihtml5.lang.array(allElements).without(uneditables);
14069
+ noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '),
14070
+ uneditables = container.querySelectorAll(noEditQuery),
14071
+ elements = wysihtml5.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents
14072
+ nbIdx;
14002
14073
 
14003
14074
  for (var i = elements.length; i--;) {
14004
14075
  if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "") {
14076
+ // If cleanup removes some new block elements. remove them from newblocks array too
14077
+ nbIdx = wysihtml5.lang.array(newBlockElements).indexOf(elements[i]);
14078
+ if (nbIdx > -1) {
14079
+ newBlockElements.splice(nbIdx, 1);
14080
+ }
14005
14081
  elements[i].parentNode.removeChild(elements[i]);
14006
14082
  }
14007
14083
  }
14084
+
14085
+ return newBlockElements;
14008
14086
  }
14009
14087
 
14010
14088
  function defaultNodeName(composer) {
@@ -14026,13 +14104,15 @@ wysihtml5.Commands = Base.extend(
14026
14104
  return block;
14027
14105
  }
14028
14106
 
14107
+ // Clone for splitting the inner inline element out of its parent inline elements context
14108
+ // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return
14029
14109
  function cloneOuterInlines(node, container) {
14030
14110
  var n = node,
14031
14111
  innerNode,
14032
14112
  parentNode,
14033
14113
  el = null,
14034
14114
  el2;
14035
-
14115
+
14036
14116
  while (n && container && n !== container) {
14037
14117
  if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) {
14038
14118
  parentNode = n;
@@ -14088,7 +14168,10 @@ wysihtml5.Commands = Base.extend(
14088
14168
  // Unsets element properties by options
14089
14169
  // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
14090
14170
  function removeOptionsFromElement(element, options, composer) {
14091
- var style, classes;
14171
+ var style, classes,
14172
+ prevNode = element.previousSibling,
14173
+ nextNode = element.nextSibling,
14174
+ unwrapped = false;
14092
14175
 
14093
14176
  if (options.styleProperty) {
14094
14177
  element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = '';
@@ -14106,10 +14189,11 @@ wysihtml5.Commands = Base.extend(
14106
14189
  element.removeAttribute('class');
14107
14190
  }
14108
14191
 
14109
- if (options.nodeName && element.nodeName === options.nodeName) {
14192
+ if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) {
14110
14193
  style = element.getAttribute('style');
14111
14194
  if (!style || style.trim() === '') {
14112
14195
  dom.unwrap(element);
14196
+ unwrapped = true;
14113
14197
  } else {
14114
14198
  element = dom.renameElement(element, defaultNodeName(composer));
14115
14199
  }
@@ -14119,60 +14203,79 @@ wysihtml5.Commands = Base.extend(
14119
14203
  if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
14120
14204
  element.removeAttribute('style');
14121
14205
  }
14206
+
14207
+ if (unwrapped) {
14208
+ applySurroundingLineBreaks(prevNode, nextNode, composer);
14209
+ }
14122
14210
  }
14123
14211
 
14124
14212
  // Unwraps block level elements from inside content
14125
14213
  // Useful as not all block level elements can contain other block-levels
14126
14214
  function unwrapBlocksFromContent(element) {
14127
- var contentBlocks = element.querySelectorAll(BLOCK_ELEMENTS) || []; // Find unnestable block elements in extracted contents
14215
+ var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents
14216
+ nextEl, prevEl;
14128
14217
 
14129
- for (var i = contentBlocks.length; i--;) {
14130
- if (!contentBlocks[i].nextSibling || contentBlocks[i].nextSibling.nodeType !== 1 || contentBlocks[i].nextSibling.nodeName !== 'BR') {
14131
- if ((contentBlocks[i].innerHTML || contentBlocks[i].nodeValue || '').trim() !== '') {
14132
- contentBlocks[i].parentNode.insertBefore(contentBlocks[i].ownerDocument.createElement('BR'), contentBlocks[i].nextSibling);
14218
+ for (var i = blocks.length; i--;) {
14219
+ nextEl = wysihtml5.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
14220
+ prevEl = wysihtml5.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
14221
+
14222
+ if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
14223
+ if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
14224
+ blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
14133
14225
  }
14134
14226
  }
14135
- wysihtml5.dom.unwrap(contentBlocks[i]);
14227
+ if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
14228
+ if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
14229
+ blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
14230
+ }
14231
+ }
14232
+ wysihtml5.dom.unwrap(blocks[i]);
14136
14233
  }
14137
14234
  }
14138
14235
 
14139
14236
  // Fix ranges that visually cover whole block element to actually cover the block
14140
14237
  function fixRangeCoverage(range, composer) {
14141
- var node;
14238
+ var node,
14239
+ start = range.startContainer,
14240
+ end = range.endContainer;
14142
14241
 
14143
- if (range.startContainer && range.startContainer.nodeType === 1 && range.startContainer === range.endContainer) {
14144
- if (range.startContainer.firstChild === range.startContainer.lastChild && range.endOffset === 1) {
14145
- if (range.startContainer !== composer.element) {
14146
- range.setStartBefore(range.startContainer);
14147
- range.setEndAfter(range.endContainer);
14242
+ // If range has only one childNode and it is end to end the range, extend the range to contain the container element too
14243
+ // This ensures the wrapper node is modified and optios added to it
14244
+ if (start && start.nodeType === 1 && start === end) {
14245
+ if (start.firstChild === start.lastChild && range.endOffset === 1) {
14246
+ if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
14247
+ range.setStartBefore(start);
14248
+ range.setEndAfter(end);
14148
14249
  }
14149
14250
  }
14150
14251
  return;
14151
14252
  }
14152
14253
 
14153
- if (range.startContainer && range.startContainer.nodeType === 1 && range.endContainer.nodeType === 3) {
14154
- if (range.startContainer.firstChild === range.endContainer && range.endOffset === 1) {
14155
- if (range.startContainer !== composer.element) {
14156
- range.setEndAfter(range.startContainer);
14254
+ // If range starts outside of node and ends inside at textrange and covers the whole node visually, extend end to cover the node end too
14255
+ if (start && start.nodeType === 1 && end.nodeType === 3) {
14256
+ if (start.firstChild === end && range.endOffset === end.data.length) {
14257
+ if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
14258
+ range.setEndAfter(start);
14157
14259
  }
14158
14260
  }
14159
14261
  return;
14160
14262
  }
14161
-
14162
- if (range.endContainer && range.endContainer.nodeType === 1 && range.startContainer.nodeType === 3) {
14163
- if (range.endContainer.firstChild === range.startContainer && range.endOffset === 1) {
14164
- if (range.endContainer !== composer.element) {
14165
- range.setStartBefore(range.endContainer);
14263
+
14264
+ // If range ends outside of node and starts inside at textrange and covers the whole node visually, extend start to cover the node start too
14265
+ if (end && end.nodeType === 1 && start.nodeType === 3) {
14266
+ if (end.firstChild === start && range.startOffset === 0) {
14267
+ if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') {
14268
+ range.setStartBefore(end);
14166
14269
  }
14167
14270
  }
14168
14271
  return;
14169
14272
  }
14170
14273
 
14171
-
14172
- if (range.startContainer && range.startContainer.nodeType === 3 && range.startContainer === range.endContainer && range.startContainer.parentNode) {
14173
- if (range.startContainer.parentNode.firstChild === range.startContainer && range.endOffset == range.endContainer.length && range.startOffset === 0) {
14174
- node = range.startContainer.parentNode;
14175
- if (node !== composer.element) {
14274
+ // If range covers a whole textnode and the textnode is the only child of node, extend range to node
14275
+ if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) {
14276
+ if (range.endOffset == end.data.length && range.startOffset === 0) {
14277
+ node = start.parentNode;
14278
+ if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') {
14176
14279
  range.setStartBefore(node);
14177
14280
  range.setEndAfter(node);
14178
14281
  }
@@ -14180,108 +14283,285 @@ wysihtml5.Commands = Base.extend(
14180
14283
  return;
14181
14284
  }
14182
14285
  }
14286
+
14287
+ // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges
14288
+ // Some places do not allow block level elements inbetween (inside ul and outside li)
14289
+ // TODO: might need extending for other nodes besides li (maybe dd,dl,dt)
14290
+ function fixNotPermittedInsertionPoints(ranges) {
14291
+ var newRanges = [],
14292
+ lis, j, maxj, tmpRange, rangePos, closestLI;
14293
+
14294
+ for (var i = 0, maxi = ranges.length; i < maxi; i++) {
14295
+
14296
+ // Fixes range start and end positions if inside UL or OL element (outside of LI)
14297
+ if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) {
14298
+ ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0);
14299
+ }
14300
+ if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) {
14301
+ closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)];
14302
+ if (closestLI.childNodes) {
14303
+ ranges[i].setEnd(closestLI, closestLI.childNodes.length);
14304
+ }
14305
+ }
14183
14306
 
14184
- // Wrap the range with a block level element
14185
- // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
14186
- function wrapRangeWithElement(range, options, defaultName, composer) {
14187
- var defaultOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null;
14188
- if (defaultOptions) {
14189
- defaultOptions.nodeName = defaultOptions.nodeName || defaultName || defaultNodeName(composer);
14307
+ // Get all LI eleemnts in selection (fully or partially covered)
14308
+ // And make sure ranges are either inside LI or outside UL/OL
14309
+ // Split and add new ranges as needed to cover same range content
14310
+ // TODO: Needs improvement to accept DL, DD, DT
14311
+ lis = ranges[i].getNodes([1], function(node) {
14312
+ return node.nodeName === "LI";
14313
+ });
14314
+ if (lis.length > 0) {
14315
+
14316
+ for (j = 0, maxj = lis.length; j < maxj; j++) {
14317
+ rangePos = ranges[i].compareNode(lis[j]);
14318
+
14319
+ // Fixes start of range that crosses LI border
14320
+ if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) {
14321
+ // Range starts before and ends inside the node
14322
+
14323
+ tmpRange = ranges[i].cloneRange();
14324
+ closestLI = wysihtml5.dom.domNode(lis[j]).prev({nodeTypes: [1]});
14325
+
14326
+ if (closestLI) {
14327
+ tmpRange.setEnd(closestLI, closestLI.childNodes.length);
14328
+ } else if (lis[j].closest('ul, ol')) {
14329
+ tmpRange.setEndBefore(lis[j].closest('ul, ol'));
14330
+ } else {
14331
+ tmpRange.setEndBefore(lis[j]);
14332
+ }
14333
+ newRanges.push(tmpRange);
14334
+ ranges[i].setStart(lis[j], 0);
14335
+ }
14336
+
14337
+ // Fixes end of range that crosses li border
14338
+ if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) {
14339
+ // Range starts inside the node and ends after node
14340
+
14341
+ tmpRange = ranges[i].cloneRange();
14342
+ tmpRange.setEnd(lis[j], lis[j].childNodes.length);
14343
+ newRanges.push(tmpRange);
14344
+
14345
+ // Find next LI in list and if present set range to it, else
14346
+ closestLI = wysihtml5.dom.domNode(lis[j]).next({nodeTypes: [1]});
14347
+ if (closestLI) {
14348
+ ranges[i].setStart(closestLI, 0);
14349
+ } else if (lis[j].closest('ul, ol')) {
14350
+ ranges[i].setStartAfter(lis[j].closest('ul, ol'));
14351
+ } else {
14352
+ ranges[i].setStartAfter(lis[j]);
14353
+ }
14354
+ }
14355
+ }
14356
+ newRanges.push(ranges[i]);
14357
+ } else {
14358
+ newRanges.push(ranges[i]);
14359
+ }
14190
14360
  }
14191
- fixRangeCoverage(range, composer);
14361
+ return newRanges;
14362
+ }
14363
+
14364
+ // Return options object with nodeName set if original did not have any
14365
+ // Node name is set to local or global default
14366
+ function getOptionsWithNodename(options, defaultName, composer) {
14367
+ var correctedOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null;
14368
+ if (correctedOptions) {
14369
+ correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer);
14370
+ }
14371
+ return correctedOptions;
14372
+ }
14373
+
14374
+ // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted
14375
+ // Also wraps empty clones of split parent tags around fragment to keep formatting
14376
+ // If firstOuterBlock is given assume that instead of finding outer (useful for solving cases of some blocks are allowed into others while others are not)
14377
+ function injectFragmentToRange(fragment, range, composer, firstOuterBlock) {
14378
+ var rangeStartContainer = range.startContainer,
14379
+ firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true),
14380
+ outerInlines, first, last, prev, next;
14381
+
14382
+ if (firstOuterBlock) {
14383
+ // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
14384
+ first = fragment.firstChild;
14385
+ last = fragment.lastChild;
14386
+
14387
+ composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
14388
+
14389
+ next = wysihtml5.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true});
14390
+ prev = wysihtml5.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
14192
14391
 
14392
+ if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) {
14393
+ first.parentNode.insertBefore(composer.doc.createElement('br'), first);
14394
+ }
14395
+
14396
+ if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) {
14397
+ next.parentNode.insertBefore(composer.doc.createElement('br'), next);
14398
+ }
14399
+
14400
+ } else {
14401
+ // Ensure node does not get inserted into an inline where it is not allowed
14402
+ outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
14403
+ if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
14404
+ if (fragment.childNodes.length === 1) {
14405
+ while(fragment.firstChild.firstChild) {
14406
+ outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
14407
+ }
14408
+ fragment.firstChild.appendChild(outerInlines.outerNode);
14409
+ }
14410
+ composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
14411
+ } else {
14412
+ // Otherwise just insert
14413
+ range.insertNode(fragment);
14414
+ }
14415
+ }
14416
+ }
14417
+
14418
+ // Removes all block formatting from range
14419
+ function clearRangeBlockFromating(range, closestBlockName, composer) {
14193
14420
  var r = range.cloneRange(),
14421
+ prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling,
14422
+ nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling,
14423
+ content = r.extractContents(),
14424
+ fragment = composer.doc.createDocumentFragment(),
14425
+ children, blocks,
14426
+ first = true;
14427
+
14428
+ while(content.firstChild) {
14429
+ // Iterate over all selection content first level childNodes
14430
+ if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
14431
+ // If node is a block element
14432
+ // Split block formating and add new block to wrap caret
14433
+
14434
+ unwrapBlocksFromContent(content.firstChild);
14435
+ children = wysihtml5.dom.unwrap(content.firstChild);
14436
+
14437
+ // Add line break before if needed
14438
+ if (children.length > 0) {
14439
+ if (
14440
+ (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) ||
14441
+ (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer)))
14442
+ ){
14443
+ fragment.appendChild(composer.doc.createElement('BR'));
14444
+ }
14445
+ }
14446
+
14447
+ for (var c = 0, cmax = children.length; c < cmax; c++) {
14448
+ fragment.appendChild(children[c]);
14449
+ }
14450
+
14451
+ // Add line break after if needed
14452
+ if (children.length > 0) {
14453
+ if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) {
14454
+ if (nextNode || fragment.lastChild !== content.lastChild) {
14455
+ fragment.appendChild(composer.doc.createElement('BR'));
14456
+ }
14457
+ }
14458
+ }
14459
+
14460
+ } else {
14461
+ fragment.appendChild(content.firstChild);
14462
+ }
14463
+
14464
+ first = false;
14465
+ }
14466
+ blocks = wysihtml5.lang.array(fragment.childNodes).get();
14467
+ injectFragmentToRange(fragment, r, composer);
14468
+ return blocks;
14469
+ }
14470
+
14471
+ // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself)
14472
+ function removeSurroundingLineBreaks(prevNode, nextNode, composer) {
14473
+ var prevPrev = prevNode && wysihtml5.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
14474
+ if (isBr(nextNode)) {
14475
+ nextNode.parentNode.removeChild(nextNode);
14476
+ }
14477
+ if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) {
14478
+ prevNode.parentNode.removeChild(prevNode);
14479
+ }
14480
+ }
14481
+
14482
+ function applySurroundingLineBreaks(prevNode, nextNode, composer) {
14483
+ var prevPrev;
14484
+
14485
+ if (prevNode && isBookmark(prevNode)) {
14486
+ prevNode = prevNode.previousSibling;
14487
+ }
14488
+ if (nextNode && isBookmark(nextNode)) {
14489
+ nextNode = nextNode.nextSibling;
14490
+ }
14491
+
14492
+ prevPrev = prevNode && prevNode.previousSibling;
14493
+
14494
+ if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) {
14495
+ prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling);
14496
+ }
14497
+
14498
+ if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) {
14499
+ nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode);
14500
+ }
14501
+ }
14502
+
14503
+ // Wrap the range with a block level element
14504
+ // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
14505
+ function wrapRangeWithElement(range, options, closestBlockName, composer) {
14506
+ var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
14507
+ r = range.cloneRange(),
14194
14508
  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}),
14195
14511
  content = r.extractContents(),
14196
14512
  fragment = composer.doc.createDocumentFragment(),
14197
- similarOptions = defaultOptions ? correctOptionsForSimilarityCheck(defaultOptions) : null,
14198
14513
  similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
14199
- splitAllBlocks = !defaultOptions || (defaultName === "BLOCKQUOTE" && defaultOptions.nodeName && defaultOptions.nodeName === "BLOCKQUOTE"),
14514
+ splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
14200
14515
  firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
14201
14516
  wrapper, blocks, children;
14202
14517
 
14203
- if (options && options.nodeName && options.nodeName === "BLOCKQUOTE") {
14518
+ if (options && options.nodeName === "BLOCKQUOTE") {
14519
+
14520
+ // If blockquote is to be inserted no quessing just add it as outermost block on line or selection
14204
14521
  var tmpEl = applyOptionsToElement(null, options, composer);
14205
14522
  tmpEl.appendChild(content);
14206
14523
  fragment.appendChild(tmpEl);
14207
14524
  blocks = [tmpEl];
14525
+
14208
14526
  } else {
14209
14527
 
14210
14528
  if (!content.firstChild) {
14529
+ // IF selection is caret (can happen if line is empty) add format around tag
14211
14530
  fragment.appendChild(applyOptionsToElement(null, options, composer));
14212
14531
  } else {
14213
14532
 
14214
14533
  while(content.firstChild) {
14534
+ // Iterate over all selection content first level childNodes
14215
14535
 
14216
14536
  if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
14217
14537
 
14218
- if (options) {
14219
- // Escape(split) block formatting at caret
14220
- applyOptionsToElement(content.firstChild, options, composer);
14221
- if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
14222
- unwrapBlocksFromContent(content.firstChild);
14223
- }
14224
- fragment.appendChild(content.firstChild);
14225
-
14226
- } else {
14227
- // Split block formating and add new block to wrap caret
14538
+ // If node is a block element
14539
+ // Escape(split) block formatting at caret
14540
+ applyOptionsToElement(content.firstChild, options, composer);
14541
+ if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
14228
14542
  unwrapBlocksFromContent(content.firstChild);
14229
- children = wysihtml5.dom.unwrap(content.firstChild);
14230
- for (var c = 0, cmax = children.length; c < cmax; c++) {
14231
- fragment.appendChild(children[c]);
14232
- }
14233
-
14234
- if (fragment.childNodes.length > 0) {
14235
- fragment.appendChild(composer.doc.createElement('BR'));
14236
- }
14237
14543
  }
14544
+ fragment.appendChild(content.firstChild);
14545
+
14238
14546
  } else {
14239
-
14240
- if (options) {
14241
- // Wrap subsequent non-block nodes inside new block element
14242
- wrapper = applyOptionsToElement(null, defaultOptions, composer);
14243
- while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
14244
- if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
14245
- unwrapBlocksFromContent(content.firstChild);
14246
- }
14247
- wrapper.appendChild(content.firstChild);
14248
- }
14249
- fragment.appendChild(wrapper);
14250
14547
 
14251
- } else {
14252
- // Escape(split) block formatting at selection
14253
- if (content.firstChild.nodeType == 1) {
14548
+ // Wrap subsequent non-block nodes inside new block element
14549
+ wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer);
14550
+ while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
14551
+ if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
14254
14552
  unwrapBlocksFromContent(content.firstChild);
14255
14553
  }
14256
- fragment.appendChild(content.firstChild);
14554
+ wrapper.appendChild(content.firstChild);
14257
14555
  }
14258
-
14556
+ fragment.appendChild(wrapper);
14259
14557
  }
14260
14558
  }
14261
14559
  }
14262
14560
 
14263
14561
  blocks = wysihtml5.lang.array(fragment.childNodes).get();
14264
14562
  }
14265
- if (firstOuterBlock) {
14266
- // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
14267
- composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
14268
- } else {
14269
- // Ensure node does not get inserted into an inline where it is not allowed
14270
- var outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
14271
- if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
14272
- if (fragment.childNodes.length === 1) {
14273
- while(fragment.firstChild.firstChild) {
14274
- outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
14275
- }
14276
- fragment.firstChild.appendChild(outerInlines.outerNode);
14277
- }
14278
- composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
14279
- } else {
14280
- // Otherwise just insert
14281
- r.insertNode(fragment);
14282
- }
14283
- }
14284
-
14563
+ injectFragmentToRange(fragment, r, composer, firstOuterBlock);
14564
+ removeSurroundingLineBreaks(prevNode, nextNode, composer);
14285
14565
  return blocks;
14286
14566
  }
14287
14567
 
@@ -14293,101 +14573,154 @@ wysihtml5.Commands = Base.extend(
14293
14573
 
14294
14574
  return (parentNode) ? parentNode.nodeName : null;
14295
14575
  }
14576
+
14577
+ // Expands caret to cover the closest block that:
14578
+ // * cannot contain other block level elements (h1-6,p, etc)
14579
+ // * Has the same nodeName that is to be inserted
14580
+ // * has insertingNodeName
14581
+ // * is DIV if insertingNodeName is not present
14582
+ //
14583
+ // If nothing found selects the current line
14584
+ function expandCaretToBlock(composer, insertingNodeName) {
14585
+ var parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
14586
+ query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'),
14587
+ }, null, composer.element),
14588
+ range;
14589
+
14590
+ if (parent) {
14591
+ range = composer.selection.createRange();
14592
+ range.selectNode(parent);
14593
+ composer.selection.setSelection(range);
14594
+ } else if (!composer.isEmpty()) {
14595
+ composer.selection.selectLine();
14596
+ }
14597
+ }
14598
+
14599
+ // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element
14600
+ // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
14601
+ function selectElements(newBlockElements, composer) {
14602
+ var range = composer.selection.createRange(),
14603
+ lastEl = newBlockElements[newBlockElements.length - 1],
14604
+ lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
14605
+
14606
+ range.setStart(newBlockElements[0], 0);
14607
+ range.setEnd(lastEl, lastOffset);
14608
+ range.select();
14609
+ }
14610
+
14611
+ // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each
14612
+ // Return created/modified block level elements
14613
+ // Method can be either "apply" or "remove"
14614
+ function formatSelection(method, composer, options) {
14615
+ var ranges = composer.selection.getOwnRanges(),
14616
+ newBlockElements = [],
14617
+ closestBlockName;
14618
+
14619
+ // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th)
14620
+ ranges = fixNotPermittedInsertionPoints(ranges);
14621
+
14622
+ for (var i = ranges.length; i--;) {
14623
+ fixRangeCoverage(ranges[i], composer);
14624
+ closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer);
14625
+ if (method === "remove") {
14626
+ newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer));
14627
+ } else {
14628
+ newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer));
14629
+ }
14630
+ }
14631
+ return newBlockElements;
14632
+ }
14633
+
14634
+ // If properties is passed as a string, look for tag with that tagName/query
14635
+ function parseOptions(options) {
14636
+ if (typeof options === "string") {
14637
+ options = {
14638
+ nodeName: options.toUpperCase()
14639
+ };
14640
+ }
14641
+ return options;
14642
+ }
14296
14643
 
14297
14644
  wysihtml5.commands.formatBlock = {
14298
14645
  exec: function(composer, command, options) {
14646
+ options = parseOptions(options);
14299
14647
  var newBlockElements = [],
14300
- placeholder, ranges, range, parent, bookmark, state;
14301
-
14302
- // If properties is passed as a string, look for tag with that tagName/query
14303
- if (typeof options === "string") {
14304
- options = {
14305
- nodeName: options.toUpperCase()
14306
- };
14307
- }
14648
+ ranges, range, bookmark, state, closestBlockName;
14308
14649
 
14309
- // Remove state if toggle set and state on and selection is collapsed
14650
+ // Find if current format state is active if options.toggle is set as true
14651
+ // In toggle case active state elemets are formatted instead of working directly on selection
14310
14652
  if (options && options.toggle) {
14311
14653
  state = this.state(composer, command, options);
14312
- if (state) {
14313
- bookmark = rangy.saveSelection(composer.win);
14314
- for (var j = 0, jmax = state.length; j < jmax; j++) {
14315
- removeOptionsFromElement(state[j], options, composer);
14316
- }
14317
- }
14318
14654
  }
14655
+ if (state) {
14656
+ // Remove format from state nodes if toggle set and state on and selection is collapsed
14657
+ bookmark = rangy.saveSelection(composer.win);
14658
+ for (var j = 0, jmax = state.length; j < jmax; j++) {
14659
+ removeOptionsFromElement(state[j], options, composer);
14660
+ }
14319
14661
 
14320
- // Otherwise expand selection so it will cover closest block if option caretSelectsBlock is true and selection is collapsed
14321
- if (!state) {
14322
-
14662
+ } else {
14663
+ // If selection is caret expand it to cover nearest suitable block element or row if none found
14323
14664
  if (composer.selection.isCollapsed()) {
14324
- parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
14325
- query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (options && options.nodeName ? options.nodeName.toLowerCase() : 'div'),
14326
- }, null, composer.element);
14327
- if (parent) {
14328
- bookmark = rangy.saveSelection(composer.win);
14329
- range = composer.selection.createRange();
14330
- range.selectNode(parent);
14331
- composer.selection.setSelection(range);
14332
- } else if (!composer.isEmpty()) {
14333
- bookmark = rangy.saveSelection(composer.win);
14334
- composer.selection.selectLine();
14335
- }
14665
+ bookmark = rangy.saveSelection(composer.win);
14666
+ expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
14336
14667
  }
14337
-
14338
- // And get all selection ranges of current composer and iterate
14339
- ranges = composer.selection.getOwnRanges();
14340
- for (var i = ranges.length; i--;) {
14341
- newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, getParentBlockNodeName(ranges[i].startContainer, composer), composer));
14668
+ if (options) {
14669
+ newBlockElements = formatSelection("apply", composer, options);
14670
+ } else {
14671
+ // Options == null means block formatting should be removed from selection
14672
+ newBlockElements = formatSelection("remove", composer);
14342
14673
  }
14343
-
14674
+
14344
14675
  }
14345
14676
 
14346
14677
  // Remove empty block elements that may be left behind
14347
- cleanup(composer);
14348
- // If cleanup removed some new block elements. remove them from array too
14349
- for (var e = newBlockElements.length; e--;) {
14350
- if (!newBlockElements[e].parentNode) {
14351
- newBlockElements.splice(e, 1);
14352
- }
14353
- }
14678
+ // Also remove them from new blocks list
14679
+ newBlockElements = cleanup(composer, newBlockElements);
14354
14680
 
14355
- // Restore correct selection
14681
+ // Restore selection
14356
14682
  if (bookmark) {
14357
- wysihtml5.dom.removeInvisibleSpaces(composer.element);
14358
14683
  rangy.restoreSelection(bookmark);
14359
14684
  } else {
14360
- wysihtml5.dom.removeInvisibleSpaces(composer.element);
14361
- // Set selection to beging inside first created block element (beginning of it) and end inside (and after content) of last block element
14362
- // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
14363
- range = composer.selection.createRange();
14364
- range.setStart(newBlockElements[0], 0);
14365
- var lastEl = newBlockElements[newBlockElements.length - 1],
14366
- lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
14367
- range.setEnd(lastEl, lastOffset);
14368
- range.select();
14685
+ selectElements(newBlockElements, composer);
14369
14686
  }
14370
14687
  },
14371
-
14372
- // If properties as null is passed returns status describing all block level elements
14373
- state: function(composer, command, properties) {
14688
+
14689
+ // Removes all block formatting from selection
14690
+ remove: function(composer, command, options) {
14691
+ options = parseOptions(options);
14692
+ var newBlockElements, bookmark;
14374
14693
 
14375
- // If properties is passed as a string, look for tag with that tagName/query
14376
- if (typeof properties === "string") {
14377
- properties = {
14378
- query: properties
14379
- };
14694
+ // If selection is caret expand it to cover nearest suitable block element or row if none found
14695
+ if (composer.selection.isCollapsed()) {
14696
+ bookmark = rangy.saveSelection(composer.win);
14697
+ expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
14380
14698
  }
14699
+
14700
+ newBlockElements = formatSelection("remove", composer);
14701
+ newBlockElements = cleanup(composer, newBlockElements);
14702
+
14703
+ // Restore selection
14704
+ if (bookmark) {
14705
+ rangy.restoreSelection(bookmark);
14706
+ } else {
14707
+ selectElements(newBlockElements, composer);
14708
+ }
14709
+ },
14710
+
14711
+ // If options as null is passed returns status describing all block level elements
14712
+ state: function(composer, command, options) {
14713
+ options = parseOptions(options);
14381
14714
 
14382
14715
  var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
14383
- return wysihtml5.dom.domNode(element).test(properties || { query: BLOCK_ELEMENTS });
14716
+ return wysihtml5.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS });
14384
14717
  }).bind(this)),
14385
14718
  parentNodes = composer.selection.getSelectedOwnNodes(),
14386
14719
  parent;
14387
14720
 
14388
14721
  // Finds matching elements that are parents of selection and adds to nodes list
14389
14722
  for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
14390
- parent = dom.getParentElement(parentNodes[i], properties || { query: BLOCK_ELEMENTS }, null, composer.element);
14723
+ parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element);
14391
14724
  if (parent && nodes.indexOf(parent) === -1) {
14392
14725
  nodes.push(parent);
14393
14726
  }
@@ -14597,6 +14930,9 @@ wysihtml5.Commands = Base.extend(
14597
14930
  if (options.toggle !== false && element.classList.contains(options.className)) {
14598
14931
  element.classList.remove(options.className);
14599
14932
  } else {
14933
+ if (options.classRegExp) {
14934
+ element.className = element.className.replace(options.classRegExp, '');
14935
+ }
14600
14936
  element.classList.add(options.className);
14601
14937
  }
14602
14938
  if (hasNoClass(element)) {
@@ -16247,7 +16583,7 @@ wysihtml5.views.View = Base.extend(
16247
16583
 
16248
16584
  cleanUp: function(rules) {
16249
16585
  var bookmark;
16250
- if (this.selection) {
16586
+ if (this.selection && this.selection.isInThisEditable()) {
16251
16587
  bookmark = rangy.saveSelection(this.win);
16252
16588
  }
16253
16589
  this.parent.parse(this.element, undefined, rules);
@@ -16419,6 +16755,8 @@ wysihtml5.views.View = Base.extend(
16419
16755
  ]).from(this.textarea.element).to(this.element);
16420
16756
  }
16421
16757
 
16758
+ this._initAutoLinking();
16759
+
16422
16760
  dom.addClass(this.element, this.config.classNames.composer);
16423
16761
  //
16424
16762
  // Make the editor look like the original textarea, by syncing styles
@@ -16451,7 +16789,6 @@ wysihtml5.views.View = Base.extend(
16451
16789
  // Make sure that the browser avoids using inline styles whenever possible
16452
16790
  this.commands.exec("styleWithCSS", false);
16453
16791
 
16454
- this._initAutoLinking();
16455
16792
  this._initObjectResizing();
16456
16793
  this._initUndoManager();
16457
16794
  this._initLineBreaking();
@@ -16485,10 +16822,7 @@ wysihtml5.views.View = Base.extend(
16485
16822
  supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
16486
16823
 
16487
16824
  if (supportsDisablingOfAutoLinking) {
16488
- // I have no idea why IE edge deletes element content here when calling the command,
16489
- var tmpHTML = this.element.innerHTML;
16490
16825
  this.commands.exec("AutoUrlDetect", false, false);
16491
- this.element.innerHTML = tmpHTML;
16492
16826
  }
16493
16827
 
16494
16828
  if (!this.config.autoLink) {