wysihtml-rails 0.5.0.beta11 → 0.5.0.beta12

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license wysihtml v0.5.0-beta11
2
+ * @license wysihtml v0.5.0-beta12
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.0-beta11",
13
+ version: "0.5.0-beta12",
14
14
 
15
15
  // namespaces
16
16
  commands: {},
@@ -5890,6 +5890,20 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5890
5890
  })(wysihtml5);
5891
5891
  ;// TODO: Refactor dom tree traversing here
5892
5892
  (function(wysihtml5) {
5893
+
5894
+ // Finds parents of a node, returning the outermost node first in Array
5895
+ // if contain node is given parents search is stopped at the container
5896
+ function parents(node, container) {
5897
+ var nodes = [node], n = node;
5898
+
5899
+ // iterate parents while parent exists and it is not container element
5900
+ while((container && n && n !== container) || (!container && n)) {
5901
+ nodes.unshift(n);
5902
+ n = n.parentNode;
5903
+ }
5904
+ return nodes;
5905
+ }
5906
+
5893
5907
  wysihtml5.dom.domNode = function(node) {
5894
5908
  var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE];
5895
5909
 
@@ -5951,6 +5965,30 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5951
5965
  return nextNode;
5952
5966
  },
5953
5967
 
5968
+ // Finds the common acnestor container of two nodes
5969
+ // If container given stops search at the container
5970
+ // If no common ancestor found returns null
5971
+ // var node = wysihtml5.dom.domNode(element).commonAncestor(node2, container);
5972
+ commonAncestor: function(node2, container) {
5973
+ var parents1 = parents(node, container),
5974
+ parents2 = parents(node2, container);
5975
+
5976
+ // Ensure we have found a common ancestor, which will be the first one if anything
5977
+ if (parents1[0] != parents2[0]) {
5978
+ return null;
5979
+ }
5980
+
5981
+ // Traverse up the hierarchy of parents until we reach where they're no longer
5982
+ // the same. Then return previous which was the common ancestor.
5983
+ for (var i = 0; i < parents1.length; i++) {
5984
+ if (parents1[i] != parents2[i]) {
5985
+ return parents1[i - 1];
5986
+ }
5987
+ }
5988
+
5989
+ return null;
5990
+ },
5991
+
5954
5992
  // Traverses a node for last children and their chidren (including itself), and finds the last node that has no children.
5955
5993
  // Array of classes for forced last-leaves (ex: uneditable-container) can be defined (options = {leafClasses: [...]})
5956
5994
  // Useful for finding the actually visible element before cursor
@@ -7443,9 +7481,10 @@ wysihtml5.dom.removeEmptyTextNodes = function(node) {
7443
7481
  childNodes = wysihtml5.lang.array(node.childNodes).get(),
7444
7482
  childNodesLength = childNodes.length,
7445
7483
  i = 0;
7484
+
7446
7485
  for (; i<childNodesLength; i++) {
7447
7486
  childNode = childNodes[i];
7448
- if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
7487
+ if (childNode.nodeType === wysihtml5.TEXT_NODE && (/^[\n\r]*$/).test(childNode.data)) {
7449
7488
  childNode.parentNode.removeChild(childNode);
7450
7489
  }
7451
7490
  }
@@ -7913,7 +7952,7 @@ wysihtml5.dom.replaceWithChildNodes = function(node) {
7913
7952
 
7914
7953
  // initiates an allready existent contenteditable
7915
7954
  _bindElement: function(contentEditable) {
7916
- contentEditable.className = (contentEditable.className && contentEditable.className !== '') ? contentEditable.className + " wysihtml5-sandbox" : "wysihtml5-sandbox";
7955
+ contentEditable.className = contentEditable.className ? contentEditable.className + " wysihtml5-sandbox" : "wysihtml5-sandbox";
7917
7956
  this._loadElement(contentEditable, true);
7918
7957
  return contentEditable;
7919
7958
  },
@@ -9430,71 +9469,140 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9430
9469
 
9431
9470
  };
9432
9471
  ;(function(wysihtml5) {
9433
- var RGBA_REGEX = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i,
9434
- RGB_REGEX = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i,
9435
- HEX6_REGEX = /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i,
9436
- HEX3_REGEX = /^#([0-9a-f])([0-9a-f])([0-9a-f])/i;
9472
+
9473
+ // List of supported color format parsing methods
9474
+ // If radix is not defined 10 is expected as default
9475
+ var colorParseMethods = {
9476
+ rgba : {
9477
+ regex: /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i,
9478
+ name: "rgba"
9479
+ },
9480
+ rgb : {
9481
+ regex: /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i,
9482
+ name: "rgb"
9483
+ },
9484
+ hex6 : {
9485
+ regex: /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i,
9486
+ name: "hex",
9487
+ radix: 16
9488
+ },
9489
+ hex3 : {
9490
+ regex: /^#([0-9a-f])([0-9a-f])([0-9a-f])/i,
9491
+ name: "hex",
9492
+ radix: 16
9493
+ }
9494
+ },
9495
+ // Takes a style key name as an argument and makes a regex that can be used to the match key:value pair from style string
9496
+ makeParamRegExp = function (p) {
9497
+ return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+", "gi");
9498
+ };
9437
9499
 
9438
- var param_REGX = function (p) {
9439
- return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+" , "gi");
9440
- };
9500
+ // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns suitable parsing method for it
9501
+ function getColorParseMethod (colorStr) {
9502
+ var prop, colorTypeConf;
9503
+
9504
+ for (prop in colorParseMethods) {
9505
+ if (!colorParseMethods.hasOwnProperty(prop)) { continue; }
9506
+
9507
+ colorTypeConf = colorParseMethods[prop];
9508
+
9509
+ if (colorTypeConf.regex.test(colorStr)) {
9510
+ return colorTypeConf;
9511
+ }
9512
+ }
9513
+ }
9514
+
9515
+ // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns the type of that color format "hex", "rgb", "rgba".
9516
+ function getColorFormat (colorStr) {
9517
+ var type = getColorParseMethod(colorStr);
9441
9518
 
9519
+ return type ? type.name : undefined;
9520
+ }
9521
+
9522
+ // Public API functions for styleParser
9442
9523
  wysihtml5.quirks.styleParser = {
9443
9524
 
9444
- parseColor: function(stylesStr, paramName) {
9445
- var paramRegex = param_REGX(paramName),
9446
- params = stylesStr.match(paramRegex),
9447
- radix = 10,
9448
- str, colorMatch;
9525
+ // Takes color string value as an argument and returns suitable parsing method for it
9526
+ getColorParseMethod : getColorParseMethod,
9449
9527
 
9450
- if (params) {
9451
- for (var i = params.length; i--;) {
9452
- params[i] = wysihtml5.lang.string(params[i].split(':')[1]).trim();
9453
- }
9454
- str = params[params.length-1];
9455
-
9456
- if (RGBA_REGEX.test(str)) {
9457
- colorMatch = str.match(RGBA_REGEX);
9458
- } else if (RGB_REGEX.test(str)) {
9459
- colorMatch = str.match(RGB_REGEX);
9460
- } else if (HEX6_REGEX.test(str)) {
9461
- colorMatch = str.match(HEX6_REGEX);
9462
- radix = 16;
9463
- } else if (HEX3_REGEX.test(str)) {
9464
- colorMatch = str.match(HEX3_REGEX);
9465
- colorMatch.shift();
9466
- colorMatch.push(1);
9467
- return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
9468
- return (idx < 3) ? (parseInt(d, 16) * 16) + parseInt(d, 16): parseFloat(d);
9469
- });
9470
- }
9528
+ // Takes color string value as an argument and returns the type of that color format "hex", "rgb", "rgba".
9529
+ getColorFormat : getColorFormat,
9530
+
9531
+ /* Parses a color string to and array of [red, green, blue, alpha].
9532
+ * paramName: optional argument to parse color value directly from style string parameter
9533
+ *
9534
+ * Examples:
9535
+ * var colorArray = wysihtml5.quirks.styleParser.parseColor("#ABC"); // [170, 187, 204, 1]
9536
+ * var colorArray = wysihtml5.quirks.styleParser.parseColor("#AABBCC"); // [170, 187, 204, 1]
9537
+ * var colorArray = wysihtml5.quirks.styleParser.parseColor("rgb(1,2,3)"); // [1, 2, 3, 1]
9538
+ * var colorArray = wysihtml5.quirks.styleParser.parseColor("rgba(1,2,3,0.5)"); // [1, 2, 3, 0.5]
9539
+ *
9540
+ * var colorArray = wysihtml5.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "background-color"); // [170, 187, 204, 1]
9541
+ * var colorArray = wysihtml5.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "color"); // [0, 0, 0, 1]
9542
+ */
9543
+ parseColor : function (stylesStr, paramName) {
9544
+ var paramsRegex, params, colorType, colorMatch, radix,
9545
+ colorStr = stylesStr;
9471
9546
 
9472
- if (colorMatch) {
9473
- colorMatch.shift();
9474
- if (!colorMatch[3]) {
9475
- colorMatch.push(1);
9476
- }
9477
- return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
9478
- return (idx < 3) ? parseInt(d, radix): parseFloat(d);
9479
- });
9480
- }
9547
+ if (paramName) {
9548
+ paramsRegex = makeParamRegExp(paramName);
9549
+
9550
+ if (!(params = stylesStr.match(paramsRegex))) { return false; }
9551
+
9552
+ params = params.pop().split(":")[1];
9553
+ colorStr = wysihtml5.lang.string(params).trim();
9481
9554
  }
9482
- return false;
9555
+
9556
+ if (!(colorType = getColorParseMethod(colorStr))) { return false; }
9557
+ if (!(colorMatch = colorStr.match(colorType.regex))) { return false; }
9558
+
9559
+ radix = colorType.radix || 10;
9560
+
9561
+ if (colorType === colorParseMethods.hex3) {
9562
+ colorMatch.shift();
9563
+ colorMatch.push(1);
9564
+ return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
9565
+ return (idx < 3) ? (parseInt(d, radix) * radix) + parseInt(d, radix): parseFloat(d);
9566
+ });
9567
+ }
9568
+
9569
+ colorMatch.shift();
9570
+
9571
+ if (!colorMatch[3]) {
9572
+ colorMatch.push(1);
9573
+ }
9574
+
9575
+ return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
9576
+ return (idx < 3) ? parseInt(d, radix): parseFloat(d);
9577
+ });
9483
9578
  },
9484
9579
 
9485
- unparseColor: function(val, props) {
9486
- if (props) {
9487
- if (props == "hex") {
9488
- return (val[0].toString(16).toUpperCase()) + (val[1].toString(16).toUpperCase()) + (val[2].toString(16).toUpperCase());
9489
- } else if (props == "hash") {
9490
- return "#" + (val[0].toString(16).toUpperCase()) + (val[1].toString(16).toUpperCase()) + (val[2].toString(16).toUpperCase());
9491
- } else if (props == "rgb") {
9492
- return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
9493
- } else if (props == "rgba") {
9494
- return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
9495
- } else if (props == "csv") {
9496
- return val[0] + "," + val[1] + "," + val[2] + "," + val[3];
9497
- }
9580
+ /* Takes rgba color array [r,g,b,a] as a value and formats it to color string with given format type
9581
+ * If no format is given, rgba/rgb is returned based on alpha value
9582
+ *
9583
+ * Example:
9584
+ * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "hash"); // "#AABBCC"
9585
+ * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "hex"); // "AABBCC"
9586
+ * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "csv"); // "170, 187, 204, 1"
9587
+ * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgba"); // "rgba(170,187,204,1)"
9588
+ * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgb"); // "rgb(170,187,204)"
9589
+ *
9590
+ * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 0.5]); // "rgba(170,187,204,0.5)"
9591
+ * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1]); // "rgb(170,187,204)"
9592
+ */
9593
+ unparseColor: function(val, colorFormat) {
9594
+ var hexRadix = 16;
9595
+
9596
+ if (colorFormat === "hex") {
9597
+ return (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
9598
+ } else if (colorFormat === "hash") {
9599
+ return "#" + (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
9600
+ } else if (colorFormat === "rgb") {
9601
+ return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
9602
+ } else if (colorFormat === "rgba") {
9603
+ return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
9604
+ } else if (colorFormat === "csv") {
9605
+ return val[0] + "," + val[1] + "," + val[2] + "," + val[3];
9498
9606
  }
9499
9607
 
9500
9608
  if (val[3] && val[3] !== 1) {
@@ -9504,10 +9612,11 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9504
9612
  }
9505
9613
  },
9506
9614
 
9615
+ // Parses font size value from style string
9507
9616
  parseFontSize: function(stylesStr) {
9508
- var params = stylesStr.match(param_REGX('font-size'));
9617
+ var params = stylesStr.match(makeParamRegExp("font-size"));
9509
9618
  if (params) {
9510
- return wysihtml5.lang.string(params[params.length - 1].split(':')[1]).trim();
9619
+ return wysihtml5.lang.string(params[params.length - 1].split(":")[1]).trim();
9511
9620
  }
9512
9621
  return false;
9513
9622
  }
@@ -9546,6 +9655,50 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9546
9655
  return ret;
9547
9656
  }
9548
9657
 
9658
+ function getWebkitSelectionFixNode(container) {
9659
+ var blankNode = document.createElement('span');
9660
+
9661
+ var placeholderRemover = function(event) {
9662
+ // Self-destructs the caret and keeps the text inserted into it by user
9663
+ var lastChild;
9664
+
9665
+ container.removeEventListener('mouseup', placeholderRemover);
9666
+ container.removeEventListener('keydown', placeholderRemover);
9667
+ container.removeEventListener('touchstart', placeholderRemover);
9668
+ container.removeEventListener('focus', placeholderRemover);
9669
+ container.removeEventListener('blur', placeholderRemover);
9670
+ container.removeEventListener('paste', delayedPlaceholderRemover);
9671
+ container.removeEventListener('drop', delayedPlaceholderRemover);
9672
+ container.removeEventListener('beforepaste', delayedPlaceholderRemover);
9673
+
9674
+ if (blankNode && blankNode.parentNode) {
9675
+ blankNode.parentNode.removeChild(blankNode);
9676
+ }
9677
+ },
9678
+ delayedPlaceholderRemover = function (event) {
9679
+ if (blankNode && blankNode.parentNode) {
9680
+ setTimeout(placeholderRemover, 0);
9681
+ }
9682
+ };
9683
+
9684
+ blankNode.appendChild(document.createTextNode(wysihtml5.INVISIBLE_SPACE));
9685
+ blankNode.className = '_wysihtml5-temp-caret-fix';
9686
+ blankNode.style.display = 'block';
9687
+ blankNode.style.minWidth = '1px';
9688
+ blankNode.style.height = '0px';
9689
+
9690
+ container.addEventListener('mouseup', placeholderRemover);
9691
+ container.addEventListener('keydown', placeholderRemover);
9692
+ container.addEventListener('touchstart', placeholderRemover);
9693
+ container.addEventListener('focus', placeholderRemover);
9694
+ container.addEventListener('blur', placeholderRemover);
9695
+ container.addEventListener('paste', delayedPlaceholderRemover);
9696
+ container.addEventListener('drop', delayedPlaceholderRemover);
9697
+ container.addEventListener('beforepaste', delayedPlaceholderRemover);
9698
+
9699
+ return blankNode;
9700
+ }
9701
+
9549
9702
  // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon
9550
9703
  // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection
9551
9704
  function expandRangeToSurround(range) {
@@ -10218,6 +10371,20 @@ wysihtml5.quirks.ensureProperClearing = (function() {
10218
10371
  }
10219
10372
  },
10220
10373
 
10374
+ canAppendChild: function (node) {
10375
+ var anchorNode, anchorNodeTagNameLower,
10376
+ voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"],
10377
+ range = this.getRange();
10378
+
10379
+ anchorNode = node || range.startContainer;
10380
+
10381
+ if (anchorNode) {
10382
+ anchorNodeTagNameLower = (anchorNode.tagName || anchorNode.nodeName).toLowerCase();
10383
+ }
10384
+
10385
+ return voidElements.indexOf(anchorNodeTagNameLower) === -1;
10386
+ },
10387
+
10221
10388
  splitElementAtCaret: function (element, insertNode) {
10222
10389
  var sel = this.getSelection(),
10223
10390
  range, contentAfterRangeStart,
@@ -10598,6 +10765,52 @@ wysihtml5.quirks.ensureProperClearing = (function() {
10598
10765
  return (selection && selection.anchorNode && selection.focusNode) ? selection : null;
10599
10766
  },
10600
10767
 
10768
+
10769
+
10770
+ // Webkit has an ancient error of not selecting all contents when uneditable block element is first or last in editable area
10771
+ selectAll: function() {
10772
+ var range = this.createRange(),
10773
+ composer = this.composer,
10774
+ that = this,
10775
+ blankEndNode = getWebkitSelectionFixNode(this.composer.element),
10776
+ blankStartNode = getWebkitSelectionFixNode(this.composer.element),
10777
+ s;
10778
+
10779
+ var doSelect = function() {
10780
+ range.setStart(composer.element, 0);
10781
+ range.setEnd(composer.element, composer.element.childNodes.length);
10782
+ s = that.setSelection(range);
10783
+ };
10784
+
10785
+ var notSelected = function() {
10786
+ return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None"));
10787
+ }
10788
+
10789
+ wysihtml5.dom.removeInvisibleSpaces(this.composer.element);
10790
+ doSelect();
10791
+
10792
+ if (this.composer.element.firstChild && notSelected()) {
10793
+ // Try fixing end
10794
+ this.composer.element.appendChild(blankEndNode);
10795
+ doSelect();
10796
+
10797
+ if (notSelected()) {
10798
+ // Remove end fix
10799
+ blankEndNode.parentNode.removeChild(blankEndNode);
10800
+
10801
+ // Try fixing beginning
10802
+ this.composer.element.insertBefore(blankStartNode, this.composer.element.firstChild);
10803
+ doSelect();
10804
+
10805
+ if (notSelected()) {
10806
+ // Try fixing both
10807
+ this.composer.element.appendChild(blankEndNode);
10808
+ doSelect();
10809
+ }
10810
+ }
10811
+ }
10812
+ },
10813
+
10601
10814
  createRange: function() {
10602
10815
  return rangy.createRange(this.doc);
10603
10816
  },
@@ -10656,6 +10869,25 @@ wysihtml5.quirks.ensureProperClearing = (function() {
10656
10869
  return (wysihtml5.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
10657
10870
  },
10658
10871
 
10872
+ isInThisEditable: function() {
10873
+ var sel = this.getSelection(),
10874
+ fnode = sel.focusNode,
10875
+ anode = sel.anchorNode;
10876
+
10877
+ // In IE node contains will not work for textnodes, thus taking parentNode
10878
+ if (fnode && fnode.nodeType !== 1) {
10879
+ fnode = fnode.parentNode;
10880
+ }
10881
+
10882
+ if (anode && anode.nodeType !== 1) {
10883
+ anode = anode.parentNode;
10884
+ }
10885
+
10886
+ return anode && fnode &&
10887
+ (wysihtml5.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) &&
10888
+ (wysihtml5.dom.contains(this.composer.element, anode) || this.composer.element === anode);
10889
+ },
10890
+
10659
10891
  deselect: function() {
10660
10892
  var sel = this.getSelection();
10661
10893
  sel && sel.removeAllRanges();
@@ -11509,12 +11741,12 @@ wysihtml5.Commands = Base.extend(
11509
11741
  exec: function(composer, command, size) {
11510
11742
  size = size.size || size;
11511
11743
  if (!(/^\s*$/).test(size)) {
11512
- wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: "fontSize", styleValue: size, toggle: true});
11744
+ wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: "fontSize", styleValue: size, toggle: false});
11513
11745
  }
11514
11746
  },
11515
11747
 
11516
11748
  state: function(composer, command, size) {
11517
- return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: "fontSize", styleValue: size});
11749
+ return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: "fontSize", styleValue: size || undefined});
11518
11750
  },
11519
11751
 
11520
11752
  remove: function(composer, command) {
@@ -11522,15 +11754,14 @@ wysihtml5.Commands = Base.extend(
11522
11754
  },
11523
11755
 
11524
11756
  stateValue: function(composer, command) {
11525
- var st = this.state(composer, command),
11526
- styleStr, fontsizeMatches,
11527
- val = false;
11757
+ var styleStr,
11758
+ st = this.state(composer, command);
11528
11759
 
11529
11760
  if (st && wysihtml5.lang.object(st).isArray()) {
11530
11761
  st = st[0];
11531
11762
  }
11532
11763
  if (st) {
11533
- styleStr = st.getAttribute('style');
11764
+ styleStr = st.getAttribute("style");
11534
11765
  if (styleStr) {
11535
11766
  return wysihtml5.quirks.styleParser.parseFontSize(styleStr);
11536
11767
  }
@@ -11562,12 +11793,15 @@ wysihtml5.Commands = Base.extend(
11562
11793
 
11563
11794
  wysihtml5.commands.foreColorStyle = {
11564
11795
  exec: function(composer, command, color) {
11565
- var colorVals = wysihtml5.quirks.styleParser.parseColor("color:" + (color.color || color), "color"),
11566
- colString;
11796
+ var colorVals, colString;
11797
+
11798
+ if (!color) { return; }
11799
+
11800
+ colorVals = wysihtml5.quirks.styleParser.parseColor("color:" + (color.color || color), "color");
11567
11801
 
11568
11802
  if (colorVals) {
11569
- colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')';
11570
- wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: 'color', styleValue: colString});
11803
+ colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(", ") : "rgba(" + colorVals.join(', ')) + ')';
11804
+ wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: "color", styleValue: colString});
11571
11805
  }
11572
11806
  },
11573
11807
 
@@ -11577,14 +11811,14 @@ wysihtml5.Commands = Base.extend(
11577
11811
 
11578
11812
 
11579
11813
  if (colorVals) {
11580
- colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')';
11814
+ colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(", ") : "rgba(" + colorVals.join(', ')) + ')';
11581
11815
  }
11582
11816
 
11583
- return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: 'color', styleValue: colString});
11817
+ return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: "color", styleValue: colString});
11584
11818
  },
11585
11819
 
11586
11820
  remove: function(composer, command) {
11587
- return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: 'color'});
11821
+ return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: "color"});
11588
11822
  },
11589
11823
 
11590
11824
  stateValue: function(composer, command, props) {
@@ -11597,7 +11831,7 @@ wysihtml5.Commands = Base.extend(
11597
11831
  }
11598
11832
 
11599
11833
  if (st) {
11600
- colorStr = st.getAttribute('style');
11834
+ colorStr = st.getAttribute("style");
11601
11835
  if (colorStr) {
11602
11836
  val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color");
11603
11837
  return wysihtml5.quirks.styleParser.unparseColor(val, props);
@@ -11675,6 +11909,14 @@ wysihtml5.Commands = Base.extend(
11675
11909
  BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote",
11676
11910
  INLINE_ELEMENTS = "b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, q, span, sub, sup, button, label, textarea, input, select, u";
11677
11911
 
11912
+ function correctOptionsForSimilarityCheck(options) {
11913
+ return {
11914
+ nodeName: options.nodeName || null,
11915
+ className: (!options.classRegExp) ? options.className || null : null,
11916
+ classRegExp: options.classRegExp || null,
11917
+ styleProperty: options.styleProperty || null
11918
+ };
11919
+ }
11678
11920
 
11679
11921
  // Removes empty block level elements
11680
11922
  function cleanup(composer) {
@@ -11684,7 +11926,7 @@ wysihtml5.Commands = Base.extend(
11684
11926
  elements = wysihtml5.lang.array(allElements).without(uneditables);
11685
11927
 
11686
11928
  for (var i = elements.length; i--;) {
11687
- if (elements[i].innerHTML === "") {
11929
+ if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "") {
11688
11930
  elements[i].parentNode.removeChild(elements[i]);
11689
11931
  }
11690
11932
  }
@@ -11811,7 +12053,7 @@ wysihtml5.Commands = Base.extend(
11811
12053
 
11812
12054
  for (var i = contentBlocks.length; i--;) {
11813
12055
  if (!contentBlocks[i].nextSibling || contentBlocks[i].nextSibling.nodeType !== 1 || contentBlocks[i].nextSibling.nodeName !== 'BR') {
11814
- if ((contentBlocks[i].innerHTML || contentBlocks[i].nodeValue).trim() !== "") {
12056
+ if ((contentBlocks[i].innerHTML || contentBlocks[i].nodeValue || '').trim() !== '') {
11815
12057
  contentBlocks[i].parentNode.insertBefore(contentBlocks[i].ownerDocument.createElement('BR'), contentBlocks[i].nextSibling);
11816
12058
  }
11817
12059
  }
@@ -11877,8 +12119,10 @@ wysihtml5.Commands = Base.extend(
11877
12119
  rangeStartContainer = r.startContainer,
11878
12120
  content = r.extractContents(),
11879
12121
  fragment = composer.doc.createDocumentFragment(),
12122
+ similarOptions = defaultOptions ? correctOptionsForSimilarityCheck(defaultOptions) : null,
12123
+ similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
11880
12124
  splitAllBlocks = !defaultOptions || (defaultName === "BLOCKQUOTE" && defaultOptions.nodeName && defaultOptions.nodeName === "BLOCKQUOTE"),
11881
- firstOuterBlock = findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
12125
+ firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
11882
12126
  wrapper, blocks, children;
11883
12127
 
11884
12128
  if (options && options.nodeName && options.nodeName === "BLOCKQUOTE") {
@@ -12003,7 +12247,7 @@ wysihtml5.Commands = Base.extend(
12003
12247
 
12004
12248
  if (composer.selection.isCollapsed()) {
12005
12249
  parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
12006
- query: BLOCK_ELEMENTS
12250
+ query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (options && options.nodeName ? options.nodeName.toLowerCase() : 'div'),
12007
12251
  }, null, composer.element);
12008
12252
  if (parent) {
12009
12253
  bookmark = rangy.saveSelection(composer.win);
@@ -12016,7 +12260,7 @@ wysihtml5.Commands = Base.extend(
12016
12260
  }
12017
12261
  }
12018
12262
 
12019
- // And get all selection ranges of current composer and iterat
12263
+ // And get all selection ranges of current composer and iterate
12020
12264
  ranges = composer.selection.getOwnRanges();
12021
12265
  for (var i = ranges.length; i--;) {
12022
12266
  newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, getParentBlockNodeName(ranges[i].startContainer, composer), composer));
@@ -12026,6 +12270,13 @@ wysihtml5.Commands = Base.extend(
12026
12270
 
12027
12271
  // Remove empty block elements that may be left behind
12028
12272
  cleanup(composer);
12273
+ // If cleanup removed some new block elements. remove them from array too
12274
+ for (var e = newBlockElements.length; e--;) {
12275
+ if (!newBlockElements[e].parentNode) {
12276
+ newBlockElements.splice(e, 1);
12277
+ }
12278
+ }
12279
+
12029
12280
  // Restore correct selection
12030
12281
  if (bookmark) {
12031
12282
  rangy.restoreSelection(bookmark);
@@ -12081,8 +12332,9 @@ wysihtml5.Commands = Base.extend(
12081
12332
  wysihtml5.commands.formatCode = {
12082
12333
 
12083
12334
  exec: function(composer, command, classname) {
12084
- var pre = this.state(composer),
12335
+ var pre = this.state(composer)[0],
12085
12336
  code, range, selectedNodes;
12337
+
12086
12338
  if (pre) {
12087
12339
  // caret is already within a <pre><code>...</code></pre>
12088
12340
  composer.selection.executeAndRestore(function() {
@@ -12111,12 +12363,13 @@ wysihtml5.Commands = Base.extend(
12111
12363
  },
12112
12364
 
12113
12365
  state: function(composer) {
12114
- var selectedNode = composer.selection.getSelectedNode();
12366
+ var selectedNode = composer.selection.getSelectedNode(), node;
12115
12367
  if (selectedNode && selectedNode.nodeName && selectedNode.nodeName == "PRE"&&
12116
12368
  selectedNode.firstChild && selectedNode.firstChild.nodeName && selectedNode.firstChild.nodeName == "CODE") {
12117
- return selectedNode;
12369
+ return [selectedNode];
12118
12370
  } else {
12119
- return wysihtml5.dom.getParentElement(selectedNode, { query: "pre code" });
12371
+ node = wysihtml5.dom.getParentElement(selectedNode, { query: "pre code" });
12372
+ return node ? [node.parentNode] : false;
12120
12373
  }
12121
12374
  }
12122
12375
  };
@@ -12260,9 +12513,7 @@ wysihtml5.Commands = Base.extend(
12260
12513
  }
12261
12514
 
12262
12515
  function updateFormatOfElement(element, options) {
12263
- var attr, newNode, a, newAttributes, nodeNameQuery;
12264
-
12265
-
12516
+ var attr, newNode, a, newAttributes, nodeNameQuery, nodeQueryMatch;
12266
12517
 
12267
12518
  if (options.className) {
12268
12519
  if (options.toggle !== false && element.classList.contains(options.className)) {
@@ -12297,30 +12548,19 @@ wysihtml5.Commands = Base.extend(
12297
12548
  updateElementAttributes(element, newAttributes, options.toggle);
12298
12549
  }
12299
12550
 
12300
- // Handle similar semanticallys ame elements (queryAliasMap)
12551
+
12552
+ // Handle similar semantically same elements (queryAliasMap)
12301
12553
  nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null;
12554
+ nodeQueryMatch = nodeNameQuery ? wysihtml5.dom.domNode(element).test({ query: nodeNameQuery }) : false;
12302
12555
 
12303
- if ((options.nodeName && wysihtml5.dom.domNode(element).test({ query: nodeNameQuery })) || (!options.nodeName && element.nodeName === defaultTag)) {
12304
-
12305
-
12306
- if (hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element)) {
12556
+ // Unwrap element if no attributes present and node name given
12557
+ // or no attributes and if no nodename set but node is the default
12558
+ if (!options.nodeName || options.nodeName === defaultTag || nodeQueryMatch) {
12559
+ if (
12560
+ ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) &&
12561
+ hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element)
12562
+ ) {
12307
12563
  wysihtml5.dom.unwrap(element);
12308
- } else if (!options.nodeName) {
12309
- newNode = element.ownerDocument.createElement(defaultTag);
12310
-
12311
- // pass present attributes
12312
- attr = wysihtml5.dom.getAttributes(element);
12313
- for (a in attr) {
12314
- if (attr.hasOwnProperty(a)) {
12315
- newNode.setAttribute(a, attr[a]);
12316
- }
12317
- }
12318
-
12319
- while (element.firstChild) {
12320
- newNode.appendChild(element.firstChild);
12321
- }
12322
- element.parentNode.insertBefore(newNode, element);
12323
- element.parentNode.removeChild(element);
12324
12564
  }
12325
12565
 
12326
12566
  }
@@ -12425,35 +12665,39 @@ wysihtml5.Commands = Base.extend(
12425
12665
  partial = false,
12426
12666
  node, range, caretNode;
12427
12667
 
12428
- if (searchNodes.length === 0 && composer.selection.isCollapsed()) {
12429
- caretNode = composer.selection.getSelection().anchorNode;
12430
- if (!caretNode) {
12431
- // selection not in editor
12432
- return {
12433
- nodes: [],
12434
- partial: false
12435
- };
12436
- }
12437
- if (caretNode.nodeType === 3) {
12438
- searchNodes = [caretNode];
12668
+ if (composer.selection.isInThisEditable()) {
12669
+
12670
+ if (searchNodes.length === 0 && composer.selection.isCollapsed()) {
12671
+ caretNode = composer.selection.getSelection().anchorNode;
12672
+ if (!caretNode) {
12673
+ // selection not in editor
12674
+ return {
12675
+ nodes: [],
12676
+ partial: false
12677
+ };
12678
+ }
12679
+ if (caretNode.nodeType === 3) {
12680
+ searchNodes = [caretNode];
12681
+ }
12439
12682
  }
12440
- }
12441
12683
 
12442
- // Handle collapsed selection caret
12443
- if (!searchNodes.length) {
12444
- range = composer.selection.getOwnRanges()[0];
12445
- if (range) {
12446
- searchNodes = [range.endContainer];
12684
+ // Handle collapsed selection caret
12685
+ if (!searchNodes.length) {
12686
+ range = composer.selection.getOwnRanges()[0];
12687
+ if (range) {
12688
+ searchNodes = [range.endContainer];
12689
+ }
12447
12690
  }
12448
- }
12449
12691
 
12450
- for (var i = 0, maxi = searchNodes.length; i < maxi; i++) {
12451
- node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact);
12452
- if (node) {
12453
- nodes.push(node);
12454
- } else {
12455
- partial = true;
12692
+ for (var i = 0, maxi = searchNodes.length; i < maxi; i++) {
12693
+ node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact);
12694
+ if (node) {
12695
+ nodes.push(node);
12696
+ } else {
12697
+ partial = true;
12698
+ }
12456
12699
  }
12700
+
12457
12701
  }
12458
12702
 
12459
12703
  return {
@@ -12774,9 +13018,7 @@ wysihtml5.Commands = Base.extend(
12774
13018
 
12775
13019
  state: function(composer, command, options) {
12776
13020
  options = fixOptions(options);
12777
-
12778
13021
  var nodes = getState(composer, options, true).nodes;
12779
-
12780
13022
  return (nodes.length === 0) ? false : nodes;
12781
13023
  }
12782
13024
  };
@@ -14635,14 +14877,14 @@ wysihtml5.views.View = Base.extend(
14635
14877
  // Deletion with caret in the beginning of headings needs special attention
14636
14878
  // Heading does not concate text to previous block node correctly (browsers do unexpected miracles here especially webkit)
14637
14879
  var fixDeleteInTheBeginnigOfHeading = function(composer) {
14638
- var selection = composer.selection;
14880
+ var selection = composer.selection,
14881
+ prevNode = selection.getPreviousNode();
14639
14882
 
14640
14883
  if (selection.caretIsFirstInSelection() &&
14641
- selection.getPreviousNode() &&
14642
- selection.getPreviousNode().nodeName &&
14643
- (/^H\d$/gi).test(selection.getPreviousNode().nodeName)
14884
+ prevNode &&
14885
+ prevNode.nodeType === 1 &&
14886
+ (/block/).test(composer.win.getComputedStyle(prevNode).display)
14644
14887
  ) {
14645
- var prevNode = selection.getPreviousNode();
14646
14888
  if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
14647
14889
  // If heading is empty remove the heading node
14648
14890
  prevNode.parentNode.removeChild(prevNode);
@@ -14650,20 +14892,23 @@ wysihtml5.views.View = Base.extend(
14650
14892
  } else {
14651
14893
  if (prevNode.lastChild) {
14652
14894
  var selNode = prevNode.lastChild,
14653
- curNode = wysihtml5.dom.getParentElement(selection.getSelectedNode(), { query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote" }, false, composer.element);
14654
- if (prevNode) {
14895
+ selectedNode = selection.getSelectedNode(),
14896
+ commonAncestorNode = wysihtml5.dom.domNode(prevNode).commonAncestor(selectedNode, composer.element);
14897
+ curNode = commonAncestorNode ? wysihtml5.dom.getParentElement(selectedNode, {
14898
+ query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote"
14899
+ }, false, commonAncestorNode) : null;
14900
+
14655
14901
  if (curNode) {
14656
14902
  while (curNode.firstChild) {
14657
14903
  prevNode.appendChild(curNode.firstChild);
14658
14904
  }
14659
14905
  selection.setAfter(selNode);
14660
14906
  return true;
14661
- } else if (selection.getSelectedNode().nodeType === 3) {
14662
- prevNode.appendChild(selection.getSelectedNode());
14907
+ } else if (selectedNode.nodeType === 3) {
14908
+ prevNode.appendChild(selectedNode);
14663
14909
  selection.setAfter(selNode);
14664
14910
  return true;
14665
14911
  }
14666
- }
14667
14912
  }
14668
14913
  }
14669
14914
  }
@@ -14675,23 +14920,17 @@ wysihtml5.views.View = Base.extend(
14675
14920
  element = composer.element;
14676
14921
 
14677
14922
  if (selection.isCollapsed()) {
14678
- if (selection.caretIsInTheBeginnig('li')) {
14679
- // delete in the beginnig of LI will outdent not delete
14923
+ if (fixDeleteInTheBeginnigOfHeading(composer)) {
14680
14924
  event.preventDefault();
14681
- composer.commands.exec('outdentList');
14682
- } else {
14683
- if (fixDeleteInTheBeginnigOfHeading(composer)) {
14684
- event.preventDefault();
14685
- return;
14686
- }
14687
- if (fixLastBrDeletionInTable(composer)) {
14688
- event.preventDefault();
14689
- return;
14690
- }
14691
- if (handleUneditableDeletion(composer)) {
14692
- event.preventDefault();
14693
- return;
14694
- }
14925
+ return;
14926
+ }
14927
+ if (fixLastBrDeletionInTable(composer)) {
14928
+ event.preventDefault();
14929
+ return;
14930
+ }
14931
+ if (handleUneditableDeletion(composer)) {
14932
+ event.preventDefault();
14933
+ return;
14695
14934
  }
14696
14935
  } else {
14697
14936
  if (selection.containsUneditable()) {
@@ -14701,11 +14940,15 @@ wysihtml5.views.View = Base.extend(
14701
14940
  }
14702
14941
  };
14703
14942
 
14704
- var handleTabKeyDown = function(composer, element) {
14943
+ var handleTabKeyDown = function(composer, element, shiftKey) {
14705
14944
  if (!composer.selection.isCollapsed()) {
14706
14945
  composer.selection.deleteContents();
14707
14946
  } else if (composer.selection.caretIsInTheBeginnig('li')) {
14708
- if (composer.commands.exec('indentList')) return;
14947
+ if (shiftKey) {
14948
+ if (composer.commands.exec('outdentList')) return;
14949
+ } else {
14950
+ if (composer.commands.exec('indentList')) return;
14951
+ }
14709
14952
  }
14710
14953
 
14711
14954
  // Is &emsp; close enough to tab. Could not find enough counter arguments for now.
@@ -14721,9 +14964,9 @@ wysihtml5.views.View = Base.extend(
14721
14964
 
14722
14965
  // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires
14723
14966
  var handleUserInteraction = function (event) {
14724
- this.parent.fire("beforeinteraction").fire("beforeinteraction:composer");
14967
+ this.parent.fire("beforeinteraction", event).fire("beforeinteraction:composer", event);
14725
14968
  setTimeout((function() {
14726
- this.parent.fire("interaction").fire("interaction:composer");
14969
+ this.parent.fire("interaction", event).fire("interaction:composer", event);
14727
14970
  }).bind(this), 0);
14728
14971
  };
14729
14972
 
@@ -14837,6 +15080,13 @@ wysihtml5.views.View = Base.extend(
14837
15080
  command = shortcuts[keyCode],
14838
15081
  target, parent;
14839
15082
 
15083
+ // Select all (meta/ctrl + a)
15084
+ if ((event.ctrlKey || event.metaKey) && keyCode === 65) {
15085
+ this.selection.selectAll();
15086
+ event.preventDefault();
15087
+ return;
15088
+ }
15089
+
14840
15090
  // Shortcut logic
14841
15091
  if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
14842
15092
  this.commands.exec(command);
@@ -14859,16 +15109,16 @@ wysihtml5.views.View = Base.extend(
14859
15109
  if (parent.nodeName === "A" && !parent.firstChild) {
14860
15110
  parent.parentNode.removeChild(parent);
14861
15111
  }
14862
- setTimeout(function() {
15112
+ setTimeout((function() {
14863
15113
  wysihtml5.quirks.redraw(this.element);
14864
- }, 0);
15114
+ }).bind(this), 0);
14865
15115
  }
14866
15116
  }
14867
15117
 
14868
15118
  if (this.config.handleTabKey && keyCode === wysihtml5.TAB_KEY) {
14869
15119
  // TAB key handling
14870
15120
  event.preventDefault();
14871
- handleTabKeyDown(this, this.element);
15121
+ handleTabKeyDown(this, this.element, event.shiftKey);
14872
15122
  }
14873
15123
 
14874
15124
  };
@@ -15227,6 +15477,8 @@ wysihtml5.views.View = Base.extend(
15227
15477
  // Whether toolbar is displayed after init by script automatically.
15228
15478
  // Can be set to false if toolobar is set to display only on editable area focus
15229
15479
  showToolbarAfterInit: true,
15480
+ // With default toolbar it shows dialogs in toolbar when their related text format state becomes active (click on link in text opens link dialogue)
15481
+ showToolbarDialogsOnSelection: true,
15230
15482
  // Whether urls, entered by the user should automatically become clickable-links
15231
15483
  autoLink: true,
15232
15484
  // Includes table editing events and cell selection tracking
@@ -15400,8 +15652,7 @@ wysihtml5.views.View = Base.extend(
15400
15652
  * - Observes for paste and drop
15401
15653
  */
15402
15654
  _initParser: function() {
15403
- var oldHtml,
15404
- cleanHtml;
15655
+ var oldHtml;
15405
15656
 
15406
15657
  if (wysihtml5.browser.supportsModernPaste()) {
15407
15658
  this.on("paste:composer", function(event) {