wysihtml-rails 0.5.1 → 0.5.2

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.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);
14192
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});
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) {