wysihtml-rails 0.5.0.beta8 → 0.5.0.beta9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ad7139f85a6baa918d3ccb084f49bd310c7e3422
4
- data.tar.gz: c11cfb988f9d9428426283ca244c7d47253401ef
3
+ metadata.gz: 390f760e7e1ad7366a3b8463d5fd6ad12dce0920
4
+ data.tar.gz: 88c4a8460bffbb8e885402ba05484859354d0894
5
5
  SHA512:
6
- metadata.gz: 4de3c6dde3a86c14dc00968bd912c76f977630649f1db2ce3987a0a05386eac89f4039e178286a5ddd6eedfe851f67e8b84dfc3138eade2a6740a45c85c08331
7
- data.tar.gz: 1a1ed7532158cd3a900554727d6b75bacc1647dc26470b13853b34faf8a957649daa9c182350bbd92e1d8e7d8cce85ed60d98dbdf9f5f0f32ead6b5c190b26c8
6
+ metadata.gz: 80e460e6841fbe1d87bb34836850187b7c767c5fc69510d7ad7302855f88605eaa27f706bda27c2529607484a1fdc65aaa072724637d898d0b3a350cae0e13af
7
+ data.tar.gz: 3d7420f8b77dd86084a877aec874565ee36f9360ff3fc58c796563b80c844da2c0a3c2d9592b259c8f01bcfb08972fcfbbef6d5b758ea406bb020b3ed99922d2
@@ -1,5 +1,5 @@
1
1
  module Wysihtml
2
2
  module Rails
3
- VERSION = "0.5.0.beta8"
3
+ VERSION = "0.5.0.beta9"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license wysihtml v0.5.0-beta8
2
+ * @license wysihtml v0.5.0-beta9
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-beta8",
13
+ version: "0.5.0-beta9",
14
14
 
15
15
  // namespaces
16
16
  commands: {},
@@ -360,6 +360,101 @@ var wysihtml5 = {
360
360
  }
361
361
 
362
362
  }
363
+
364
+ // Safary has a bug of not restoring selection after node.normalize correctly.
365
+ // Detects the misbegaviour and patches it
366
+ var normalizeHasCaretError = function() {
367
+ if ("createRange" in document && "getSelection" in window) {
368
+ var e = document.createElement('div'),
369
+ t1 = document.createTextNode('a'),
370
+ t2 = document.createTextNode('a'),
371
+ t3 = document.createTextNode('a'),
372
+ r = document.createRange(),
373
+ s, ret;
374
+
375
+ e.setAttribute('contenteditable', 'true');
376
+ e.appendChild(t1);
377
+ e.appendChild(t2);
378
+ e.appendChild(t3);
379
+ document.body.appendChild(e);
380
+ r.setStart(t2, 1);
381
+ r.setEnd(t2, 1);
382
+
383
+ s = window.getSelection();
384
+ s.removeAllRanges();
385
+ s.addRange(r);
386
+ e.normalize();
387
+ s = window.getSelection();
388
+
389
+ ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2);
390
+ e.parentNode.removeChild(e);
391
+ return ret;
392
+ }
393
+ };
394
+
395
+ var getTextNodes = function(node){
396
+ var all = [];
397
+ for (node=node.firstChild;node;node=node.nextSibling){
398
+ if (node.nodeType == 3) {
399
+ all.push(node);
400
+ } else {
401
+ all = all.concat(getTextNodes(node));
402
+ }
403
+ }
404
+ return all;
405
+ };
406
+
407
+ var normalizeFix = function() {
408
+ var f = Node.prototype.normalize;
409
+ var nf = function() {
410
+ var texts = getTextNodes(this),
411
+ s = this.ownerDocument.defaultView.getSelection(),
412
+ anode = s.anchorNode,
413
+ aoffset = s.anchorOffset,
414
+ fnode = s.focusNode,
415
+ foffset = s.focusOffset,
416
+ r = this.ownerDocument.createRange(),
417
+ prevTxt = texts.shift(),
418
+ curText = prevTxt ? texts.shift() : null;
419
+
420
+ if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & Node.DOCUMENT_POSITION_PRECEDING))) {
421
+ fnode = [anode, anode = fnode][0];
422
+ foffset = [aoffset, aoffset = foffset][0];
423
+ }
424
+
425
+ while(prevTxt && curText) {
426
+ if (curText.previousSibling && curText.previousSibling === prevTxt) {
427
+ if (anode === curText) {
428
+ anode = prevTxt;
429
+ aoffset = prevTxt.nodeValue.length + aoffset;
430
+ }
431
+ if (fnode === curText) {
432
+ fnode = prevTxt;
433
+ foffset = prevTxt.nodeValue.length + foffset;
434
+ }
435
+ prevTxt.nodeValue = prevTxt.nodeValue + curText.nodeValue;
436
+ curText.parentNode.removeChild(curText);
437
+ curText = texts.shift();
438
+ } else {
439
+ prevTxt = curText;
440
+ curText = texts.shift();
441
+ }
442
+ }
443
+
444
+ if (anode && anode.parentNode && fnode && fnode.parentNode) {
445
+ r.setStart(anode, aoffset);
446
+ r.setEnd(fnode, foffset);
447
+ s.removeAllRanges();
448
+ s.addRange(r);
449
+ }
450
+
451
+ };
452
+ Node.prototype.normalize = nf;
453
+ };
454
+
455
+ if ("Node" in window && "normalize" in Node.prototype && normalizeHasCaretError()) {
456
+ normalizeFix();
457
+ }
363
458
  };
364
459
 
365
460
  wysihtml5.polyfills(window, document);
@@ -367,10 +462,10 @@ wysihtml5.polyfills(window, document);
367
462
  * Rangy, a cross-browser JavaScript range and selection library
368
463
  * https://github.com/timdown/rangy
369
464
  *
370
- * Copyright 2014, Tim Down
465
+ * Copyright 2015, Tim Down
371
466
  * Licensed under the MIT license.
372
- * Version: 1.3.0-alpha.20140921
373
- * Build date: 21 September 2014
467
+ * Version: 1.3.0
468
+ * Build date: 10 May 2015
374
469
  */
375
470
 
376
471
  (function(factory, root) {
@@ -447,6 +542,16 @@ wysihtml5.polyfills(window, document);
447
542
  return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
448
543
  }
449
544
 
545
+ var forEach = [].forEach ?
546
+ function(arr, func) {
547
+ arr.forEach(func);
548
+ } :
549
+ function(arr, func) {
550
+ for (var i = 0, len = arr.length; i < len; ++i) {
551
+ func(arr[i], i);
552
+ }
553
+ };
554
+
450
555
  var modules = {};
451
556
 
452
557
  var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
@@ -459,11 +564,12 @@ wysihtml5.polyfills(window, document);
459
564
  areHostObjects: areHostObjects,
460
565
  areHostProperties: areHostProperties,
461
566
  isTextRange: isTextRange,
462
- getBody: getBody
567
+ getBody: getBody,
568
+ forEach: forEach
463
569
  };
464
570
 
465
571
  var api = {
466
- version: "1.3.0-alpha.20140921",
572
+ version: "1.3.0",
467
573
  initialized: false,
468
574
  isBrowser: isBrowser,
469
575
  supported: true,
@@ -471,7 +577,7 @@ wysihtml5.polyfills(window, document);
471
577
  features: {},
472
578
  modules: modules,
473
579
  config: {
474
- alertOnFail: true,
580
+ alertOnFail: false,
475
581
  alertOnWarn: false,
476
582
  preferTextRange: false,
477
583
  autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
@@ -539,7 +645,7 @@ wysihtml5.polyfills(window, document);
539
645
  } else {
540
646
  fail("hasOwnProperty not supported");
541
647
  }
542
-
648
+
543
649
  // Test whether we're in a browser and bail out if not
544
650
  if (!isBrowser) {
545
651
  fail("Rangy can only run in a browser");
@@ -660,6 +766,24 @@ wysihtml5.polyfills(window, document);
660
766
  }
661
767
  }
662
768
 
769
+ function deprecationNotice(deprecated, replacement, module) {
770
+ if (module) {
771
+ deprecated += " in module " + module.name;
772
+ }
773
+ api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
774
+ replacement + " instead.");
775
+ }
776
+
777
+ function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
778
+ owner[deprecated] = function() {
779
+ deprecationNotice(deprecated, replacement, module);
780
+ return owner[replacement].apply(owner, util.toArray(arguments));
781
+ };
782
+ }
783
+
784
+ util.deprecationNotice = deprecationNotice;
785
+ util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
786
+
663
787
  // Allow external scripts to initialize this library in case it's loaded after the document has loaded
664
788
  api.init = init;
665
789
 
@@ -690,6 +814,7 @@ wysihtml5.polyfills(window, document);
690
814
 
691
815
  if (isBrowser) {
692
816
  api.shim = api.createMissingNativeApi = shim;
817
+ createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
693
818
  }
694
819
 
695
820
  function Module(name, dependencies, initializer) {
@@ -717,15 +842,15 @@ wysihtml5.polyfills(window, document);
717
842
  throw new Error("required module '" + moduleName + "' not supported");
718
843
  }
719
844
  }
720
-
845
+
721
846
  // Now run initializer
722
847
  this.initializer(this);
723
848
  },
724
-
849
+
725
850
  fail: function(reason) {
726
851
  this.initialized = true;
727
852
  this.supported = false;
728
- throw new Error("Module '" + this.name + "' failed to load: " + reason);
853
+ throw new Error(reason);
729
854
  },
730
855
 
731
856
  warn: function(msg) {
@@ -733,7 +858,7 @@ wysihtml5.polyfills(window, document);
733
858
  },
734
859
 
735
860
  deprecationNotice: function(deprecated, replacement) {
736
- api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " +
861
+ api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
737
862
  replacement + " instead");
738
863
  },
739
864
 
@@ -741,7 +866,7 @@ wysihtml5.polyfills(window, document);
741
866
  return new Error("Error in Rangy " + this.name + " module: " + msg);
742
867
  }
743
868
  };
744
-
869
+
745
870
  function createModule(name, dependencies, initFunc) {
746
871
  var newModule = new Module(name, dependencies, function(module) {
747
872
  if (!module.initialized) {
@@ -802,6 +927,7 @@ wysihtml5.polyfills(window, document);
802
927
  api.createCoreModule("DomUtil", [], function(api, module) {
803
928
  var UNDEF = "undefined";
804
929
  var util = api.util;
930
+ var getBody = util.getBody;
805
931
 
806
932
  // Perform feature tests
807
933
  if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
@@ -1113,7 +1239,7 @@ wysihtml5.polyfills(window, document);
1113
1239
  var el = document.createElement("b");
1114
1240
  el.innerHTML = "1";
1115
1241
  var textNode = el.firstChild;
1116
- el.innerHTML = "<br>";
1242
+ el.innerHTML = "<br />";
1117
1243
  crashyTextNodes = isBrokenNode(textNode);
1118
1244
 
1119
1245
  api.features.crashyTextNodes = crashyTextNodes;
@@ -1153,12 +1279,35 @@ wysihtml5.polyfills(window, document);
1153
1279
  };
1154
1280
  } else if (typeof document.documentElement.currentStyle != UNDEF) {
1155
1281
  getComputedStyleProperty = function(el, propName) {
1156
- return el.currentStyle[propName];
1282
+ return el.currentStyle ? el.currentStyle[propName] : "";
1157
1283
  };
1158
1284
  } else {
1159
1285
  module.fail("No means of obtaining computed style properties found");
1160
1286
  }
1161
1287
 
1288
+ function createTestElement(doc, html, contentEditable) {
1289
+ var body = getBody(doc);
1290
+ var el = doc.createElement("div");
1291
+ el.contentEditable = "" + !!contentEditable;
1292
+ if (html) {
1293
+ el.innerHTML = html;
1294
+ }
1295
+
1296
+ // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
1297
+ var bodyFirstChild = body.firstChild;
1298
+ if (bodyFirstChild) {
1299
+ body.insertBefore(el, bodyFirstChild);
1300
+ } else {
1301
+ body.appendChild(el);
1302
+ }
1303
+
1304
+ return el;
1305
+ }
1306
+
1307
+ function removeNode(node) {
1308
+ return node.parentNode.removeChild(node);
1309
+ }
1310
+
1162
1311
  function NodeIterator(root) {
1163
1312
  this.root = root;
1164
1313
  this._next = root;
@@ -1256,7 +1405,7 @@ wysihtml5.polyfills(window, document);
1256
1405
  getWindow: getWindow,
1257
1406
  getIframeWindow: getIframeWindow,
1258
1407
  getIframeDocument: getIframeDocument,
1259
- getBody: util.getBody,
1408
+ getBody: getBody,
1260
1409
  isWindow: isWindow,
1261
1410
  getContentDocument: getContentDocument,
1262
1411
  getRootContainer: getRootContainer,
@@ -1264,6 +1413,8 @@ wysihtml5.polyfills(window, document);
1264
1413
  isBrokenNode: isBrokenNode,
1265
1414
  inspectNode: inspectNode,
1266
1415
  getComputedStyleProperty: getComputedStyleProperty,
1416
+ createTestElement: createTestElement,
1417
+ removeNode: removeNode,
1267
1418
  fragmentFromNodeChildren: fragmentFromNodeChildren,
1268
1419
  createIterator: createIterator,
1269
1420
  DomPosition: DomPosition
@@ -1293,6 +1444,8 @@ wysihtml5.polyfills(window, document);
1293
1444
  var getRootContainer = dom.getRootContainer;
1294
1445
  var crashyTextNodes = api.features.crashyTextNodes;
1295
1446
 
1447
+ var removeNode = dom.removeNode;
1448
+
1296
1449
  /*----------------------------------------------------------------------------------------------------------------*/
1297
1450
 
1298
1451
  // Utility functions
@@ -1306,6 +1459,10 @@ wysihtml5.polyfills(window, document);
1306
1459
  return range.document || getDocument(range.startContainer);
1307
1460
  }
1308
1461
 
1462
+ function getRangeRoot(range) {
1463
+ return getRootContainer(range.startContainer);
1464
+ }
1465
+
1309
1466
  function getBoundaryBeforeNode(node) {
1310
1467
  return new DomPosition(node.parentNode, getNodeIndex(node));
1311
1468
  }
@@ -1540,7 +1697,7 @@ wysihtml5.polyfills(window, document);
1540
1697
  }
1541
1698
  } else {
1542
1699
  if (current.parentNode) {
1543
- current.parentNode.removeChild(current);
1700
+ removeNode(current);
1544
1701
  } else {
1545
1702
  }
1546
1703
  }
@@ -1643,26 +1800,21 @@ wysihtml5.polyfills(window, document);
1643
1800
  }
1644
1801
  }
1645
1802
 
1646
- function isOrphan(node) {
1647
- return (crashyTextNodes && dom.isBrokenNode(node)) ||
1648
- !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
1649
- }
1650
-
1651
1803
  function isValidOffset(node, offset) {
1652
1804
  return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1653
1805
  }
1654
1806
 
1655
1807
  function isRangeValid(range) {
1656
1808
  return (!!range.startContainer && !!range.endContainer &&
1657
- !isOrphan(range.startContainer) &&
1658
- !isOrphan(range.endContainer) &&
1809
+ !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1810
+ getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1659
1811
  isValidOffset(range.startContainer, range.startOffset) &&
1660
1812
  isValidOffset(range.endContainer, range.endOffset));
1661
1813
  }
1662
1814
 
1663
1815
  function assertRangeValid(range) {
1664
1816
  if (!isRangeValid(range)) {
1665
- throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
1817
+ throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1666
1818
  }
1667
1819
  }
1668
1820
 
@@ -1773,7 +1925,7 @@ wysihtml5.polyfills(window, document);
1773
1925
  }
1774
1926
  range.setStartAndEnd(sc, so, ec, eo);
1775
1927
  }
1776
-
1928
+
1777
1929
  function rangeToHtml(range) {
1778
1930
  assertRangeValid(range);
1779
1931
  var container = range.commonAncestorContainer.parentNode.cloneNode(false);
@@ -1956,13 +2108,14 @@ wysihtml5.polyfills(window, document);
1956
2108
  // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1957
2109
  intersectsNode: function(node, touchingIsIntersecting) {
1958
2110
  assertRangeValid(this);
1959
- assertNode(node, "NOT_FOUND_ERR");
1960
- if (getDocument(node) !== getRangeDocument(this)) {
2111
+ if (getRootContainer(node) != getRangeRoot(this)) {
1961
2112
  return false;
1962
2113
  }
1963
2114
 
1964
2115
  var parent = node.parentNode, offset = getNodeIndex(node);
1965
- assertNode(parent, "NOT_FOUND_ERR");
2116
+ if (!parent) {
2117
+ return true;
2118
+ }
1966
2119
 
1967
2120
  var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1968
2121
  endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
@@ -2072,7 +2225,7 @@ wysihtml5.polyfills(window, document);
2072
2225
  this.setStartAfter(node);
2073
2226
  this.collapse(true);
2074
2227
  },
2075
-
2228
+
2076
2229
  getBookmark: function(containerNode) {
2077
2230
  var doc = getRangeDocument(this);
2078
2231
  var preSelectionRange = api.createRange(doc);
@@ -2092,7 +2245,7 @@ wysihtml5.polyfills(window, document);
2092
2245
  containerNode: containerNode
2093
2246
  };
2094
2247
  },
2095
-
2248
+
2096
2249
  moveToBookmark: function(bookmark) {
2097
2250
  var containerNode = bookmark.containerNode;
2098
2251
  var charIndex = 0;
@@ -2134,11 +2287,11 @@ wysihtml5.polyfills(window, document);
2134
2287
  isValid: function() {
2135
2288
  return isRangeValid(this);
2136
2289
  },
2137
-
2290
+
2138
2291
  inspect: function() {
2139
2292
  return inspect(this);
2140
2293
  },
2141
-
2294
+
2142
2295
  detach: function() {
2143
2296
  // In DOM4, detach() is now a no-op.
2144
2297
  }
@@ -2275,7 +2428,7 @@ wysihtml5.polyfills(window, document);
2275
2428
 
2276
2429
  boundaryUpdater(this, sc, so, ec, eo);
2277
2430
  },
2278
-
2431
+
2279
2432
  setBoundary: function(node, offset, isStart) {
2280
2433
  this["set" + (isStart ? "Start" : "End")](node, offset);
2281
2434
  },
@@ -2345,7 +2498,7 @@ wysihtml5.polyfills(window, document);
2345
2498
  ec = node;
2346
2499
  eo = node.length;
2347
2500
  node.appendData(sibling.data);
2348
- sibling.parentNode.removeChild(sibling);
2501
+ removeNode(sibling);
2349
2502
  }
2350
2503
  };
2351
2504
 
@@ -2356,7 +2509,7 @@ wysihtml5.polyfills(window, document);
2356
2509
  var nodeLength = node.length;
2357
2510
  so = sibling.length;
2358
2511
  node.insertData(0, sibling.data);
2359
- sibling.parentNode.removeChild(sibling);
2512
+ removeNode(sibling);
2360
2513
  if (sc == ec) {
2361
2514
  eo += so;
2362
2515
  ec = sc;
@@ -2373,10 +2526,22 @@ wysihtml5.polyfills(window, document);
2373
2526
  };
2374
2527
 
2375
2528
  var normalizeStart = true;
2529
+ var sibling;
2376
2530
 
2377
2531
  if (isCharacterDataNode(ec)) {
2378
- if (ec.length == eo) {
2532
+ if (eo == ec.length) {
2379
2533
  mergeForward(ec);
2534
+ } else if (eo == 0) {
2535
+ sibling = ec.previousSibling;
2536
+ if (sibling && sibling.nodeType == ec.nodeType) {
2537
+ eo = sibling.length;
2538
+ if (sc == ec) {
2539
+ normalizeStart = false;
2540
+ }
2541
+ sibling.appendData(ec.data);
2542
+ removeNode(ec);
2543
+ ec = sibling;
2544
+ }
2380
2545
  }
2381
2546
  } else {
2382
2547
  if (eo > 0) {
@@ -2392,6 +2557,16 @@ wysihtml5.polyfills(window, document);
2392
2557
  if (isCharacterDataNode(sc)) {
2393
2558
  if (so == 0) {
2394
2559
  mergeBackward(sc);
2560
+ } else if (so == sc.length) {
2561
+ sibling = sc.nextSibling;
2562
+ if (sibling && sibling.nodeType == sc.nodeType) {
2563
+ if (ec == sibling) {
2564
+ ec = sc;
2565
+ eo += sc.length;
2566
+ }
2567
+ sc.appendData(sibling.data);
2568
+ removeNode(sibling);
2569
+ }
2395
2570
  }
2396
2571
  } else {
2397
2572
  if (so < sc.childNodes.length) {
@@ -2470,7 +2645,7 @@ wysihtml5.polyfills(window, document);
2470
2645
 
2471
2646
  /*----------------------------------------------------------------------------------------------------------------*/
2472
2647
 
2473
- // Wrappers for the browser's native DOM Range and/or TextRange implementation
2648
+ // Wrappers for the browser's native DOM Range and/or TextRange implementation
2474
2649
  api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2475
2650
  var WrappedRange, WrappedTextRange;
2476
2651
  var dom = api.dom;
@@ -2736,7 +2911,7 @@ wysihtml5.polyfills(window, document);
2736
2911
  };
2737
2912
  })();
2738
2913
  }
2739
-
2914
+
2740
2915
  if (api.features.implementsTextRange) {
2741
2916
  /*
2742
2917
  This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
@@ -2803,7 +2978,7 @@ wysihtml5.polyfills(window, document);
2803
2978
  // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2804
2979
  // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2805
2980
  if (workingNode.parentNode) {
2806
- workingNode.parentNode.removeChild(workingNode);
2981
+ dom.removeNode(workingNode);
2807
2982
  }
2808
2983
 
2809
2984
  var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
@@ -2858,11 +3033,11 @@ wysihtml5.polyfills(window, document);
2858
3033
  For the particular case of a boundary within a text node containing rendered line breaks (within a
2859
3034
  <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2860
3035
  IE. The facts:
2861
-
3036
+
2862
3037
  - Each line break is represented as \r in the text node's data/nodeValue properties
2863
3038
  - Each line break is represented as \r\n in the TextRange's 'text' property
2864
3039
  - The 'text' property of the TextRange does not contain trailing line breaks
2865
-
3040
+
2866
3041
  To get round the problem presented by the final fact above, we can use the fact that TextRange's
2867
3042
  moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2868
3043
  necessarily the same as the number of characters it was instructed to move. The simplest approach is
@@ -2871,13 +3046,13 @@ wysihtml5.polyfills(window, document);
2871
3046
  "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2872
3047
  the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2873
3048
  the end of the document) has the same problem.
2874
-
3049
+
2875
3050
  Another approach that works is to use moveStart() to move the start boundary of the range up to the
2876
3051
  end boundary one character at a time and incrementing a counter with the value returned by the
2877
3052
  moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2878
3053
  expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2879
3054
  by the location of the range within the document).
2880
-
3055
+
2881
3056
  The approach used below is a hybrid of the two methods above. It uses the fact that a string
2882
3057
  containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2883
3058
  be longer than the text of the TextRange, so the start of the range is moved that length initially
@@ -2912,7 +3087,7 @@ wysihtml5.polyfills(window, document);
2912
3087
  }
2913
3088
 
2914
3089
  // Clean up
2915
- workingNode.parentNode.removeChild(workingNode);
3090
+ dom.removeNode(workingNode);
2916
3091
 
2917
3092
  return {
2918
3093
  boundaryPosition: boundaryPosition,
@@ -3061,15 +3236,8 @@ wysihtml5.polyfills(window, document);
3061
3236
  return new DomRange(doc);
3062
3237
  };
3063
3238
 
3064
- api.createIframeRange = function(iframeEl) {
3065
- module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
3066
- return api.createRange(iframeEl);
3067
- };
3068
-
3069
- api.createIframeRangyRange = function(iframeEl) {
3070
- module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
3071
- return api.createRangyRange(iframeEl);
3072
- };
3239
+ util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
3240
+ util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
3073
3241
 
3074
3242
  api.addShimListener(function(win) {
3075
3243
  var doc = win.document;
@@ -3107,8 +3275,8 @@ wysihtml5.polyfills(window, document);
3107
3275
  var rangesEqual = DomRange.rangesEqual;
3108
3276
 
3109
3277
 
3110
- // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
3111
- // Boolean (true for backwards).
3278
+ // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
3279
+ // "forward" or "forwards") or a Boolean (true for backwards).
3112
3280
  function isDirectionBackward(dir) {
3113
3281
  return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
3114
3282
  }
@@ -3133,7 +3301,7 @@ wysihtml5.polyfills(window, document);
3133
3301
  function getDocSelection(winParam) {
3134
3302
  return getWindow(winParam, "getDocSelection").document.selection;
3135
3303
  }
3136
-
3304
+
3137
3305
  function winSelectionIsBackward(sel) {
3138
3306
  var backward = false;
3139
3307
  if (sel.anchorNode) {
@@ -3167,11 +3335,19 @@ wysihtml5.polyfills(window, document);
3167
3335
  };
3168
3336
  } else {
3169
3337
  module.fail("Neither document.selection or window.getSelection() detected.");
3338
+ return false;
3170
3339
  }
3171
3340
 
3172
3341
  api.getNativeSelection = getNativeSelection;
3173
3342
 
3174
3343
  var testSelection = getNativeSelection();
3344
+
3345
+ // In Firefox, the selection is null in an iframe with display: none. See issue #138.
3346
+ if (!testSelection) {
3347
+ module.fail("Native selection was null (possibly issue 138?)");
3348
+ return false;
3349
+ }
3350
+
3175
3351
  var testRange = api.createNativeRange(document);
3176
3352
  var body = getBody(document);
3177
3353
 
@@ -3184,7 +3360,7 @@ wysihtml5.polyfills(window, document);
3184
3360
  // Test for existence of native selection extend() method
3185
3361
  var selectionHasExtend = isHostMethod(testSelection, "extend");
3186
3362
  features.selectionHasExtend = selectionHasExtend;
3187
-
3363
+
3188
3364
  // Test if rangeCount exists
3189
3365
  var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
3190
3366
  features.selectionHasRangeCount = selectionHasRangeCount;
@@ -3208,25 +3384,22 @@ wysihtml5.polyfills(window, document);
3208
3384
  // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
3209
3385
  // performed on the current document's selection. See issue 109.
3210
3386
 
3211
- // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
3212
- // because initialization usually happens when the document loads, but could be a problem for a script that
3213
- // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
3214
- // selection.
3387
+ // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
3388
+ // will result in the selection direction begin reversed if the original selection was backwards and the
3389
+ // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
3215
3390
  var sel = window.getSelection();
3216
3391
  if (sel) {
3217
3392
  // Store the current selection
3218
3393
  var originalSelectionRangeCount = sel.rangeCount;
3219
3394
  var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
3220
3395
  var originalSelectionRanges = [];
3221
- var originalSelectionBackward = winSelectionIsBackward(sel);
3396
+ var originalSelectionBackward = winSelectionIsBackward(sel);
3222
3397
  for (var i = 0; i < originalSelectionRangeCount; ++i) {
3223
3398
  originalSelectionRanges[i] = sel.getRangeAt(i);
3224
3399
  }
3225
-
3400
+
3226
3401
  // Create some test elements
3227
- var body = getBody(document);
3228
- var testEl = body.appendChild( document.createElement("div") );
3229
- testEl.contentEditable = "false";
3402
+ var testEl = dom.createTestElement(document, "", false);
3230
3403
  var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
3231
3404
 
3232
3405
  // Test whether the native selection will allow a collapsed selection within a non-editable element
@@ -3234,6 +3407,7 @@ wysihtml5.polyfills(window, document);
3234
3407
 
3235
3408
  r1.setStart(textNode, 1);
3236
3409
  r1.collapse(true);
3410
+ sel.removeAllRanges();
3237
3411
  sel.addRange(r1);
3238
3412
  collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
3239
3413
  sel.removeAllRanges();
@@ -3260,7 +3434,7 @@ wysihtml5.polyfills(window, document);
3260
3434
  }
3261
3435
 
3262
3436
  // Clean up
3263
- body.removeChild(testEl);
3437
+ dom.removeNode(testEl);
3264
3438
  sel.removeAllRanges();
3265
3439
 
3266
3440
  for (i = 0; i < originalSelectionRangeCount; ++i) {
@@ -3518,10 +3692,7 @@ wysihtml5.polyfills(window, document);
3518
3692
 
3519
3693
  api.getSelection = getSelection;
3520
3694
 
3521
- api.getIframeSelection = function(iframeEl) {
3522
- module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
3523
- return api.getSelection(dom.getIframeWindow(iframeEl));
3524
- };
3695
+ util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
3525
3696
 
3526
3697
  var selProto = WrappedSelection.prototype;
3527
3698
 
@@ -3882,8 +4053,8 @@ wysihtml5.polyfills(window, document);
3882
4053
  }
3883
4054
  };
3884
4055
 
3885
- // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
3886
- // never used by Rangy.
4056
+ // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
4057
+ // specified so the native implementation is never used by Rangy.
3887
4058
  selProto.selectAllChildren = function(node) {
3888
4059
  assertNodeInSameDocument(this, node);
3889
4060
  var range = api.createRange(node);
@@ -3899,7 +4070,7 @@ wysihtml5.polyfills(window, document);
3899
4070
  while (controlRange.length) {
3900
4071
  element = controlRange.item(0);
3901
4072
  controlRange.remove(element);
3902
- element.parentNode.removeChild(element);
4073
+ dom.removeNode(element);
3903
4074
  }
3904
4075
  this.refresh();
3905
4076
  } else if (this.rangeCount) {
@@ -3941,11 +4112,11 @@ wysihtml5.polyfills(window, document);
3941
4112
  selProto.callMethodOnEachRange = function(methodName, params) {
3942
4113
  var results = [];
3943
4114
  this.eachRange( function(range) {
3944
- results.push( range[methodName].apply(range, params) );
4115
+ results.push( range[methodName].apply(range, params || []) );
3945
4116
  } );
3946
4117
  return results;
3947
4118
  };
3948
-
4119
+
3949
4120
  function createStartOrEndSetter(isStart) {
3950
4121
  return function(node, offset) {
3951
4122
  var range;
@@ -3962,7 +4133,7 @@ wysihtml5.polyfills(window, document);
3962
4133
 
3963
4134
  selProto.setStart = createStartOrEndSetter(true);
3964
4135
  selProto.setEnd = createStartOrEndSetter(false);
3965
-
4136
+
3966
4137
  // Add select() method to Range prototype. Any existing selection will be removed.
3967
4138
  api.rangePrototype.select = function(direction) {
3968
4139
  getSelection( this.getDocument() ).setSingleRange(this, direction);
@@ -4012,6 +4183,20 @@ wysihtml5.polyfills(window, document);
4012
4183
  }
4013
4184
  };
4014
4185
 
4186
+ selProto.saveRanges = function() {
4187
+ return {
4188
+ backward: this.isBackward(),
4189
+ ranges: this.callMethodOnEachRange("cloneRange")
4190
+ };
4191
+ };
4192
+
4193
+ selProto.restoreRanges = function(selRanges) {
4194
+ this.removeAllRanges();
4195
+ for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
4196
+ this.addRange(range, (selRanges.backward && i == 0));
4197
+ }
4198
+ };
4199
+
4015
4200
  selProto.toHtml = function() {
4016
4201
  var rangeHtmls = [];
4017
4202
  this.eachRange(function(range) {
@@ -4028,7 +4213,7 @@ wysihtml5.polyfills(window, document);
4028
4213
  if (isTextRange(range)) {
4029
4214
  return range;
4030
4215
  } else {
4031
- throw module.createError("getNativeTextRange: selection is a control selection");
4216
+ throw module.createError("getNativeTextRange: selection is a control selection");
4032
4217
  }
4033
4218
  } else if (this.rangeCount > 0) {
4034
4219
  return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
@@ -4126,10 +4311,10 @@ wysihtml5.polyfills(window, document);
4126
4311
  *
4127
4312
  * Depends on Rangy core.
4128
4313
  *
4129
- * Copyright 2014, Tim Down
4314
+ * Copyright 2015, Tim Down
4130
4315
  * Licensed under the MIT license.
4131
- * Version: 1.3.0-alpha.20140921
4132
- * Build date: 21 September 2014
4316
+ * Version: 1.3.0
4317
+ * Build date: 10 May 2015
4133
4318
  */
4134
4319
  (function(factory, root) {
4135
4320
  if (typeof define == "function" && define.amd) {
@@ -4145,7 +4330,8 @@ wysihtml5.polyfills(window, document);
4145
4330
  })(function(rangy) {
4146
4331
  rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
4147
4332
  var dom = api.dom;
4148
-
4333
+ var removeNode = dom.removeNode;
4334
+ var isDirectionBackward = api.Selection.isDirectionBackward;
4149
4335
  var markerTextChar = "\ufeff";
4150
4336
 
4151
4337
  function gEBI(id, doc) {
@@ -4177,7 +4363,7 @@ wysihtml5.polyfills(window, document);
4177
4363
  var markerEl = gEBI(markerId, doc);
4178
4364
  if (markerEl) {
4179
4365
  range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
4180
- markerEl.parentNode.removeChild(markerEl);
4366
+ removeNode(markerEl);
4181
4367
  } else {
4182
4368
  module.warn("Marker element has been removed. Cannot restore selection.");
4183
4369
  }
@@ -4187,8 +4373,9 @@ wysihtml5.polyfills(window, document);
4187
4373
  return r2.compareBoundaryPoints(r1.START_TO_START, r1);
4188
4374
  }
4189
4375
 
4190
- function saveRange(range, backward) {
4376
+ function saveRange(range, direction) {
4191
4377
  var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
4378
+ var backward = isDirectionBackward(direction);
4192
4379
 
4193
4380
  if (range.collapsed) {
4194
4381
  endEl = insertRangeBoundaryMarker(range, false);
@@ -4228,11 +4415,11 @@ wysihtml5.polyfills(window, document);
4228
4415
 
4229
4416
  // Workaround for issue 17
4230
4417
  if (previousNode && previousNode.nodeType == 3) {
4231
- markerEl.parentNode.removeChild(markerEl);
4418
+ removeNode(markerEl);
4232
4419
  range.collapseToPoint(previousNode, previousNode.length);
4233
4420
  } else {
4234
4421
  range.collapseBefore(markerEl);
4235
- markerEl.parentNode.removeChild(markerEl);
4422
+ removeNode(markerEl);
4236
4423
  }
4237
4424
  } else {
4238
4425
  module.warn("Marker element has been removed. Cannot restore selection.");
@@ -4249,8 +4436,9 @@ wysihtml5.polyfills(window, document);
4249
4436
  return range;
4250
4437
  }
4251
4438
 
4252
- function saveRanges(ranges, backward) {
4439
+ function saveRanges(ranges, direction) {
4253
4440
  var rangeInfos = [], range, doc;
4441
+ var backward = isDirectionBackward(direction);
4254
4442
 
4255
4443
  // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
4256
4444
  ranges = ranges.slice(0);
@@ -4289,7 +4477,7 @@ wysihtml5.polyfills(window, document);
4289
4477
 
4290
4478
  // Ensure current selection is unaffected
4291
4479
  if (backward) {
4292
- sel.setSingleRange(ranges[0], "backward");
4480
+ sel.setSingleRange(ranges[0], backward);
4293
4481
  } else {
4294
4482
  sel.setRanges(ranges);
4295
4483
  }
@@ -4335,7 +4523,7 @@ wysihtml5.polyfills(window, document);
4335
4523
  function removeMarkerElement(doc, markerId) {
4336
4524
  var markerEl = gEBI(markerId, doc);
4337
4525
  if (markerEl) {
4338
- markerEl.parentNode.removeChild(markerEl);
4526
+ removeNode(markerEl);
4339
4527
  }
4340
4528
  }
4341
4529
 
@@ -4364,6 +4552,7 @@ wysihtml5.polyfills(window, document);
4364
4552
  });
4365
4553
  });
4366
4554
 
4555
+ return rangy;
4367
4556
  }, this);;/*
4368
4557
  Base.js, version 1.1a
4369
4558
  Copyright 2006-2010, Dean Edwards
@@ -5096,6 +5285,29 @@ wysihtml5.browser = (function() {
5096
5285
  return this;
5097
5286
  },
5098
5287
 
5288
+ difference: function (otherObj) {
5289
+ var diffObj = {};
5290
+
5291
+ // Get old values not in comparing object
5292
+ for (var i in obj) {
5293
+ if (obj.hasOwnProperty(i)) {
5294
+ if (!otherObj.hasOwnProperty(i)) {
5295
+ diffObj[i] = obj[i];
5296
+ }
5297
+ }
5298
+ }
5299
+
5300
+ // Get new and different values in comparing object
5301
+ for (var o in otherObj) {
5302
+ if (otherObj.hasOwnProperty(o)) {
5303
+ if (!obj.hasOwnProperty(o) || obj[o] !== otherObj[o]) {
5304
+ diffObj[0] = obj[0];
5305
+ }
5306
+ }
5307
+ }
5308
+ return diffObj;
5309
+ },
5310
+
5099
5311
  get: function() {
5100
5312
  return obj;
5101
5313
  },
@@ -5147,6 +5359,20 @@ wysihtml5.browser = (function() {
5147
5359
 
5148
5360
  isPlainObject: function () {
5149
5361
  return obj && Object.prototype.toString.call(obj) === '[object Object]' && !(("Node" in window) ? obj instanceof Node : obj instanceof Element || obj instanceof Text);
5362
+ },
5363
+
5364
+ /**
5365
+ * @example
5366
+ * wysihtml5.lang.object({}).isEmpty();
5367
+ * // => true
5368
+ */
5369
+ isEmpty: function() {
5370
+ for (var i in obj) {
5371
+ if (obj.hasOwnProperty(i)) {
5372
+ return false;
5373
+ }
5374
+ }
5375
+ return true;
5150
5376
  }
5151
5377
  };
5152
5378
  };
@@ -5403,6 +5629,9 @@ wysihtml5.browser = (function() {
5403
5629
  if (documentElement.contains) {
5404
5630
  return function(container, element) {
5405
5631
  if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
5632
+ if (element.parentNode === container) {
5633
+ return true;
5634
+ }
5406
5635
  element = element.parentNode;
5407
5636
  }
5408
5637
  return container !== element && container.contains(element);
@@ -5663,12 +5892,15 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5663
5892
  wysihtml5.dom.domNode = function(node) {
5664
5893
  var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE];
5665
5894
 
5666
- var _isBlankText = function(node) {
5667
- return node.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/g).test(node.data);
5668
- };
5669
-
5670
5895
  return {
5671
5896
 
5897
+ is: {
5898
+ emptyTextNode: function(ignoreWhitespace) {
5899
+ var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g);
5900
+ return node.nodeType === wysihtml5.TEXT_NODE && (regx).test(node.data);
5901
+ }
5902
+ },
5903
+
5672
5904
  // var node = wysihtml5.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
5673
5905
  prev: function(options) {
5674
5906
  var prevNode = node.previousSibling,
@@ -5680,7 +5912,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5680
5912
 
5681
5913
  if (
5682
5914
  (!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
5683
- (options && options.ignoreBlankTexts && _isBlankText(prevNode)) // Blank text nodes bypassed if set
5915
+ (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set
5684
5916
  ) {
5685
5917
  return wysihtml5.dom.domNode(prevNode).prev(options);
5686
5918
  }
@@ -5699,7 +5931,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5699
5931
 
5700
5932
  if (
5701
5933
  (!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
5702
- (options && options.ignoreBlankTexts && _isBlankText(nextNode)) // blank text nodes bypassed if set
5934
+ (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set
5703
5935
  ) {
5704
5936
  return wysihtml5.dom.domNode(nextNode).next(options);
5705
5937
  }
@@ -5736,6 +5968,67 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5736
5968
  return wysihtml5.dom.domNode(lastChild).lastLeafNode(options);
5737
5969
  },
5738
5970
 
5971
+ // Splits element at childnode and extracts the childNode out of the element context
5972
+ // Example:
5973
+ // var node = wysihtml5.dom.domNode(node).escapeParent(parentNode);
5974
+ escapeParent: function(element, newWrapper) {
5975
+ var parent, split2, nodeWrap,
5976
+ curNode = node;
5977
+
5978
+ // Stop if node is not a descendant of element
5979
+ if (!wysihtml5.dom.contains(element, node)) {
5980
+ throw new Error("Child is not a descendant of node.");
5981
+ }
5982
+
5983
+ // Climb up the node tree untill node is reached
5984
+ do {
5985
+ // Get current parent of node
5986
+ parent = curNode.parentNode;
5987
+
5988
+ // Move after nodes to new clone wrapper
5989
+ split2 = parent.cloneNode(false);
5990
+ while (parent.lastChild && parent.lastChild !== curNode) {
5991
+ split2.insertBefore(parent.lastChild, split2.firstChild);
5992
+ }
5993
+
5994
+ // Move node up a level. If parent is not yet the container to escape, clone the parent around node, so inner nodes are escaped out too
5995
+ if (parent !== element) {
5996
+ nodeWrap = parent.cloneNode(false);
5997
+ nodeWrap.appendChild(curNode);
5998
+ curNode = nodeWrap;
5999
+ }
6000
+ parent.parentNode.insertBefore(curNode, parent.nextSibling);
6001
+
6002
+ // Add after nodes (unless empty)
6003
+ if (split2.innerHTML !== '') {
6004
+ // if contents are empty insert without wrap
6005
+ if ((/^\s+$/).test(split2.innerHTML)) {
6006
+ while (split2.lastChild) {
6007
+ parent.parentNode.insertBefore(split2.lastChild, curNode.nextSibling);
6008
+ }
6009
+ } else {
6010
+ parent.parentNode.insertBefore(split2, curNode.nextSibling);
6011
+ }
6012
+ }
6013
+
6014
+ // If the node left behind before the split (parent) is now empty then remove
6015
+ if (parent.innerHTML === '') {
6016
+ parent.parentNode.removeChild(parent);
6017
+ } else if ((/^\s+$/).test(parent.innerHTML)) {
6018
+ while (parent.firstChild) {
6019
+ parent.parentNode.insertBefore(parent.firstChild, parent);
6020
+ }
6021
+ parent.parentNode.removeChild(parent);
6022
+ }
6023
+
6024
+ } while (parent && parent !== element);
6025
+
6026
+ if (newWrapper && curNode) {
6027
+ curNode.parentNode.insertBefore(newWrapper, curNode);
6028
+ newWrapper.appendChild(curNode);
6029
+ }
6030
+ },
6031
+
5739
6032
  /*
5740
6033
  Tests a node against properties, and returns true if matches.
5741
6034
  Tests on principle that all properties defined must have at least one match.
@@ -5814,7 +6107,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5814
6107
  }
5815
6108
  } else {
5816
6109
  // style value as string
5817
- if (properties.styleValue === node.style[prop].trim()) {
6110
+ if (properties.styleValue === node.style[prop].trim().replace(/, /g, ",")) {
5818
6111
  hasOneStyle = true;
5819
6112
  break;
5820
6113
  }
@@ -5830,6 +6123,37 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5830
6123
  }
5831
6124
  }
5832
6125
 
6126
+ if (properties.attribute) {
6127
+ var attr = wysihtml5.dom.getAttributes(node),
6128
+ attrList = [],
6129
+ hasOneAttribute = false;
6130
+
6131
+ if (Array.isArray(properties.attribute)) {
6132
+ attrList = properties.attribute;
6133
+ } else {
6134
+ attrList[properties.attribute] = properties.attributeValue;
6135
+ }
6136
+
6137
+ for (var a in attrList) {
6138
+ if (attrList.hasOwnProperty(a)) {
6139
+ if (typeof attrList[a] === "undefined") {
6140
+ if (typeof attr[a] !== "undefined") {
6141
+ hasOneAttribute = true;
6142
+ break;
6143
+ }
6144
+ } else if (attr[a] === attrList[a]) {
6145
+ hasOneAttribute = true;
6146
+ break;
6147
+ }
6148
+ }
6149
+ }
6150
+
6151
+ if (!hasOneAttribute) {
6152
+ return false;
6153
+ }
6154
+
6155
+ }
6156
+
5833
6157
  return true;
5834
6158
  }
5835
6159
 
@@ -9253,7 +9577,6 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9253
9577
  */
9254
9578
  getBookmark: function() {
9255
9579
  var range = this.getRange();
9256
- if (range) expandRangeToSurround(range);
9257
9580
  return range && range.cloneRange();
9258
9581
  },
9259
9582
 
@@ -9359,13 +9682,15 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9359
9682
  * callback is an optional parameter accepting a function to execute when selection ahs been set
9360
9683
  */
9361
9684
  setAfter: function(node, notVisual, callback) {
9362
- var range = rangy.createRange(this.doc),
9685
+ var win = this.win,
9686
+ range = rangy.createRange(this.doc),
9363
9687
  fixWebkitSelection = function() {
9364
9688
  // Webkit fails to add selection if there are no textnodes in that region
9365
9689
  // (like an uneditable container at the end of content).
9366
9690
  var parent = node.parentNode,
9367
9691
  lastSibling = parent ? parent.childNodes[parent.childNodes.length - 1] : null;
9368
- if (!sel || (lastSibling === node && this.win.getComputedStyle(node).display === "block")) {
9692
+
9693
+ if (!sel || (lastSibling === node && node.nodeType === 1 && win.getComputedStyle(node).display === "block")) {
9369
9694
  if (notVisual) {
9370
9695
  // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation
9371
9696
  // and remove itself in call stack end instead on user interaction
@@ -9876,7 +10201,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9876
10201
  splitElementAtCaret: function (element, insertNode) {
9877
10202
  var sel = this.getSelection(),
9878
10203
  range, contentAfterRangeStart,
9879
- firstChild, lastChild;
10204
+ firstChild, lastChild, childNodes;
9880
10205
 
9881
10206
  if (sel.rangeCount > 0) {
9882
10207
  range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with
@@ -9884,19 +10209,43 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9884
10209
  range.setEndAfter(element); // Place the end of the range after the element
9885
10210
  contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment
9886
10211
 
10212
+ childNodes = contentAfterRangeStart.childNodes;
10213
+
10214
+ // Empty elements are cleaned up from extracted content
10215
+ for (var i = childNodes.length; i --;) {
10216
+ if (childNodes[i].nodeType === 1 && (/^\s*$/).test(childNodes[i].innerHTML)) {
10217
+ contentAfterRangeStart.removeChild(childNodes[i]);
10218
+ }
10219
+ }
10220
+
9887
10221
  element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling);
9888
10222
 
9889
- firstChild = insertNode.firstChild;
9890
- lastChild = insertNode.lastChild;
10223
+ if (insertNode) {
10224
+ firstChild = insertNode.firstChild || insertNode;
10225
+ lastChild = insertNode.lastChild || insertNode;
9891
10226
 
9892
- element.parentNode.insertBefore(insertNode, element.nextSibling);
10227
+ element.parentNode.insertBefore(insertNode, element.nextSibling);
10228
+
10229
+ // Select inserted node contents
10230
+ if (firstChild && lastChild) {
10231
+ range.setStartBefore(firstChild);
10232
+ range.setEndAfter(lastChild);
10233
+ this.setSelection(range);
10234
+ }
10235
+ } else {
10236
+ range.setStartAfter(element);
10237
+ range.setEndAfter(element);
10238
+ }
9893
10239
 
9894
- // Select inserted node contents
9895
- if (firstChild && lastChild) {
9896
- range.setStartBefore(firstChild);
9897
- range.setEndAfter(lastChild);
9898
- this.setSelection(range);
10240
+ if ((/^\s*$/).test(element.innerHTML)) {
10241
+ if (element.innerHTML === '') {
10242
+ element.parentNode.removeChild(element);
10243
+ } else {
10244
+ wysihtml5.dom.unwrap(element);
10245
+ }
9899
10246
  }
10247
+
10248
+
9900
10249
  }
9901
10250
  },
9902
10251
 
@@ -10098,6 +10447,24 @@ wysihtml5.quirks.ensureProperClearing = (function() {
10098
10447
  }
10099
10448
  },
10100
10449
 
10450
+ // Gets all the elements in selection with nodeType
10451
+ // Ignores the elements not belonging to current editable area
10452
+ // If filter is defined nodes must pass the filter function with true to be included in list
10453
+ getOwnNodes: function(nodeType, filter, splitBounds) {
10454
+ var ranges = this.getOwnRanges(),
10455
+ nodes = [];
10456
+ for (var r = 0, rmax = ranges.length; r < rmax; r++) {
10457
+ if (ranges[r]) {
10458
+ if (splitBounds) {
10459
+ ranges[r].splitBoundaries();
10460
+ }
10461
+ nodes = nodes.concat(ranges[r].getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter));
10462
+ }
10463
+ }
10464
+
10465
+ return nodes;
10466
+ },
10467
+
10101
10468
  fixRangeOverflow: function(range) {
10102
10469
  if (this.contain && this.contain.firstChild && range) {
10103
10470
  var containment = range.compareNode(this.contain);
@@ -10982,6 +11349,16 @@ wysihtml5.Commands = Base.extend(
10982
11349
  return result;
10983
11350
  },
10984
11351
 
11352
+ remove: function(command, commandValue) {
11353
+ var obj = wysihtml5.commands[command],
11354
+ args = wysihtml5.lang.array(arguments).get(),
11355
+ method = obj && obj.remove;
11356
+ if (method) {
11357
+ args.unshift(this.composer);
11358
+ return method.apply(obj, args);
11359
+ }
11360
+ },
11361
+
10985
11362
  /**
10986
11363
  * Check whether the current command is active
10987
11364
  * If the caret is within a bold text, then calling this with command "bold" should return true
@@ -11022,216 +11399,106 @@ wysihtml5.Commands = Base.extend(
11022
11399
  }
11023
11400
  }
11024
11401
  });
11025
- ;(function(wysihtml5){
11402
+ ;(function(wysihtml5) {
11403
+
11404
+ var nodeOptions = {
11405
+ nodeName: "B",
11406
+ toggle: true
11407
+ };
11408
+
11026
11409
  wysihtml5.commands.bold = {
11027
11410
  exec: function(composer, command) {
11028
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "b");
11411
+ wysihtml5.commands.formatInline.exec(composer, command, nodeOptions);
11029
11412
  },
11030
11413
 
11031
11414
  state: function(composer, command) {
11032
- // element.ownerDocument.queryCommandState("bold") results:
11033
- // firefox: only <b>
11034
- // chrome: <b>, <strong>, <h1>, <h2>, ...
11035
- // ie: <b>, <strong>
11036
- // opera: <b>, <strong>
11037
- return wysihtml5.commands.formatInline.state(composer, command, "b");
11415
+ return wysihtml5.commands.formatInline.state(composer, command, nodeOptions);
11038
11416
  }
11039
11417
  };
11418
+
11040
11419
  }(wysihtml5));
11041
11420
  ;(function(wysihtml5) {
11042
- var undef,
11043
- NODE_NAME = "A",
11044
- dom = wysihtml5.dom;
11045
-
11046
- function _format(composer, attributes) {
11047
- var doc = composer.doc,
11048
- tempClass = "_wysihtml5-temp-" + (+new Date()),
11049
- tempClassRegExp = /non-matching-class/g,
11050
- i = 0,
11051
- length,
11052
- anchors,
11053
- anchor,
11054
- hasElementChild,
11055
- isEmpty,
11056
- elementToSetCaretAfter,
11057
- textContent,
11058
- whiteSpace,
11059
- j;
11060
- wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp, undef, undef, true, true);
11061
- anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
11062
- length = anchors.length;
11063
- for (; i<length; i++) {
11064
- anchor = anchors[i];
11065
- anchor.removeAttribute("class");
11066
- for (j in attributes) {
11067
- // Do not set attribute "text" as it is meant for setting string value if created link has no textual data
11068
- if (j !== "text") {
11069
- anchor.setAttribute(j, attributes[j]);
11070
- }
11071
- }
11072
- }
11073
-
11074
- elementToSetCaretAfter = anchor;
11075
- if (length === 1) {
11076
- textContent = dom.getTextContent(anchor);
11077
- hasElementChild = !!anchor.querySelector("*");
11078
- isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
11079
- if (!hasElementChild && isEmpty) {
11080
- dom.setTextContent(anchor, attributes.text || anchor.href);
11081
- whiteSpace = doc.createTextNode(" ");
11082
- composer.selection.setAfter(anchor);
11083
- dom.insert(whiteSpace).after(anchor);
11084
- elementToSetCaretAfter = whiteSpace;
11085
- }
11086
- }
11087
- composer.selection.setAfter(elementToSetCaretAfter);
11088
- }
11089
-
11090
- // Changes attributes of links
11091
- function _changeLinks(composer, anchors, attributes) {
11092
- var oldAttrs;
11093
- for (var a = anchors.length; a--;) {
11094
-
11095
- // Remove all old attributes
11096
- oldAttrs = anchors[a].attributes;
11097
- for (var oa = oldAttrs.length; oa--;) {
11098
- anchors[a].removeAttribute(oldAttrs.item(oa).name);
11099
- }
11100
11421
 
11101
- // Set new attributes
11102
- for (var j in attributes) {
11103
- if (attributes.hasOwnProperty(j)) {
11104
- anchors[a].setAttribute(j, attributes[j]);
11105
- }
11106
- }
11422
+ var nodeOptions = {
11423
+ nodeName: "A",
11424
+ toggle: false
11425
+ };
11107
11426
 
11108
- }
11427
+ function getOptions(value) {
11428
+ var options = typeof value === 'object' ? value : {'href': value};
11429
+ return wysihtml5.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get();
11109
11430
  }
11110
11431
 
11111
- wysihtml5.commands.createLink = {
11112
- /**
11113
- * TODO: Use HTMLApplier or formatInline here
11114
- *
11115
- * Turns selection into a link
11116
- * If selection is already a link, it just changes the attributes
11117
- *
11118
- * @example
11119
- * // either ...
11120
- * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
11121
- * // ... or ...
11122
- * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
11123
- */
11432
+ wysihtml5.commands.createLink = {
11124
11433
  exec: function(composer, command, value) {
11125
- var anchors = this.state(composer, command);
11126
- if (anchors) {
11127
- // remove <a> tag if there's no attributes provided.
11128
- if ((!value || !value.href) && anchors.length !== null && anchors.length !== undefined && anchors.length > 0)
11129
- {
11130
- for(var i=0; i < anchors.length; i++)
11131
- {
11132
- wysihtml5.dom.unwrap(anchors[i]);
11133
- }
11134
- return;
11135
- }
11434
+ var opts = getOptions(value);
11136
11435
 
11137
- // Selection contains links then change attributes of these links
11138
- composer.selection.executeAndRestore(function() {
11139
- _changeLinks(composer, anchors, value);
11140
- });
11141
- } else {
11142
- // Create links
11143
- if (value && value.href) {
11144
- value = typeof(value) === "object" ? value : { href: value };
11145
- _format(composer, value);
11146
- }
11436
+ if (composer.selection.isCollapsed() && !this.state(composer, command)) {
11437
+ var textNode = composer.doc.createTextNode(opts.attribute.href);
11438
+ composer.selection.insertNode(textNode);
11439
+ composer.selection.selectNode(textNode);
11147
11440
  }
11441
+ wysihtml5.commands.formatInline.exec(composer, command, opts);
11148
11442
  },
11149
11443
 
11150
11444
  state: function(composer, command) {
11151
- return wysihtml5.commands.formatInline.state(composer, command, "a");
11445
+ return wysihtml5.commands.formatInline.state(composer, command, nodeOptions);
11152
11446
  }
11153
11447
  };
11448
+
11154
11449
  })(wysihtml5);
11155
11450
  ;(function(wysihtml5) {
11156
- var dom = wysihtml5.dom;
11157
11451
 
11158
- function _removeFormat(composer, anchors) {
11159
- var length = anchors.length,
11160
- i = 0,
11161
- anchor,
11162
- codeElement,
11163
- textContent;
11164
- for (; i<length; i++) {
11165
- anchor = anchors[i];
11166
- codeElement = dom.getParentElement(anchor, { query: "code" });
11167
- textContent = dom.getTextContent(anchor);
11168
-
11169
- // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
11170
- // else replace <a> with its childNodes
11171
- if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
11172
- // <code> element is used to prevent later auto-linking of the content
11173
- codeElement = dom.renameElement(anchor, "code");
11174
- } else {
11175
- dom.replaceWithChildNodes(anchor);
11176
- }
11177
- }
11178
- }
11452
+ var nodeOptions = {
11453
+ nodeName: "A"
11454
+ };
11179
11455
 
11180
11456
  wysihtml5.commands.removeLink = {
11181
- /*
11182
- * If selection is a link, it removes the link and wraps it with a <code> element
11183
- * The <code> element is needed to avoid auto linking
11184
- *
11185
- * @example
11186
- * wysihtml5.commands.createLink.exec(composer, "removeLink");
11187
- */
11188
-
11189
11457
  exec: function(composer, command) {
11190
- var anchors = this.state(composer, command);
11191
- if (anchors) {
11192
- composer.selection.executeAndRestore(function() {
11193
- _removeFormat(composer, anchors);
11194
- });
11195
- }
11458
+ wysihtml5.commands.formatInline.remove(composer, command, nodeOptions);
11196
11459
  },
11197
11460
 
11198
11461
  state: function(composer, command) {
11199
- return wysihtml5.commands.formatInline.state(composer, command, "A");
11462
+ return wysihtml5.commands.formatInline.state(composer, command, nodeOptions);
11200
11463
  }
11201
11464
  };
11465
+
11202
11466
  })(wysihtml5);
11203
11467
  ;/**
11204
- * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
11205
- * which we don't want
11206
- * Instead we set a css class
11468
+ * Set font size css class
11207
11469
  */
11208
11470
  (function(wysihtml5) {
11209
11471
  var REG_EXP = /wysiwyg-font-size-[0-9a-z\-]+/g;
11210
11472
 
11211
11473
  wysihtml5.commands.fontSize = {
11212
11474
  exec: function(composer, command, size) {
11213
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
11475
+ wysihtml5.commands.formatInline.exec(composer, command, {className: "wysiwyg-font-size-" + size, classRegExp: REG_EXP, toggle: true});
11214
11476
  },
11215
11477
 
11216
11478
  state: function(composer, command, size) {
11217
- return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
11479
+ return wysihtml5.commands.formatInline.state(composer, command, {className: "wysiwyg-font-size-" + size});
11218
11480
  }
11219
11481
  };
11220
11482
  })(wysihtml5);
11221
- ;/* In case font size adjustment to any number defined by user is preferred, we cannot use classes and must use inline styles. */
11483
+ ;/**
11484
+ * Set font size by inline style
11485
+ */
11222
11486
  (function(wysihtml5) {
11223
- var REG_EXP = /(\s|^)font-size\s*:\s*[^;\s]+;?/gi;
11224
11487
 
11225
11488
  wysihtml5.commands.fontSizeStyle = {
11226
11489
  exec: function(composer, command, size) {
11227
- size = (typeof(size) == "object") ? size.size : size;
11490
+ size = size.size || size;
11228
11491
  if (!(/^\s*$/).test(size)) {
11229
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, "font-size:" + size, REG_EXP);
11492
+ wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: "fontSize", styleValue: size, toggle: true});
11230
11493
  }
11231
11494
  },
11232
11495
 
11233
11496
  state: function(composer, command, size) {
11234
- return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "font-size", REG_EXP);
11497
+ return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: "fontSize", styleValue: size});
11498
+ },
11499
+
11500
+ remove: function(composer, command) {
11501
+ return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: "fontSize"});
11235
11502
  },
11236
11503
 
11237
11504
  stateValue: function(composer, command) {
@@ -11253,52 +11520,57 @@ wysihtml5.Commands = Base.extend(
11253
11520
  };
11254
11521
  })(wysihtml5);
11255
11522
  ;/**
11256
- * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
11257
- * which we don't want
11258
- * Instead we set a css class
11523
+ * Set color css class
11259
11524
  */
11260
11525
  (function(wysihtml5) {
11261
11526
  var REG_EXP = /wysiwyg-color-[0-9a-z]+/g;
11262
11527
 
11263
11528
  wysihtml5.commands.foreColor = {
11264
11529
  exec: function(composer, command, color) {
11265
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
11530
+ wysihtml5.commands.formatInline.exec(composer, command, {className: "wysiwyg-color-" + color, classRegExp: REG_EXP, toggle: true});
11266
11531
  },
11267
11532
 
11268
11533
  state: function(composer, command, color) {
11269
- return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
11534
+ return wysihtml5.commands.formatInline.state(composer, command, {className: "wysiwyg-color-" + color});
11270
11535
  }
11271
11536
  };
11272
11537
  })(wysihtml5);
11273
11538
  ;/**
11274
- * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
11275
- * which we don't want
11276
- * Instead we set a css class
11539
+ * Sets text color by inline styles
11277
11540
  */
11278
11541
  (function(wysihtml5) {
11279
- var REG_EXP = /(\s|^)color\s*:\s*[^;\s]+;?/gi;
11280
11542
 
11281
11543
  wysihtml5.commands.foreColorStyle = {
11282
11544
  exec: function(composer, command, color) {
11283
- var colorVals = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "color:" + color.color : "color:" + color, "color"),
11545
+ var colorVals = wysihtml5.quirks.styleParser.parseColor("color:" + (color.color || color), "color"),
11284
11546
  colString;
11285
11547
 
11286
11548
  if (colorVals) {
11287
- colString = "color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');';
11288
- if (colorVals[3] !== 1) {
11289
- colString += "color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');';
11290
- }
11291
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
11549
+ colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')';
11550
+ wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: 'color', styleValue: colString});
11292
11551
  }
11293
11552
  },
11294
11553
 
11295
- state: function(composer, command) {
11296
- return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "color", REG_EXP);
11554
+ state: function(composer, command, color) {
11555
+ var colorVals = color ? wysihtml5.quirks.styleParser.parseColor("color:" + (color.color || color), "color") : null,
11556
+ colString;
11557
+
11558
+
11559
+ if (colorVals) {
11560
+ colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')';
11561
+ }
11562
+
11563
+ return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: 'color', styleValue: colString});
11564
+ },
11565
+
11566
+ remove: function(composer, command) {
11567
+ return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: 'color'});
11297
11568
  },
11298
11569
 
11299
11570
  stateValue: function(composer, command, props) {
11300
11571
  var st = this.state(composer, command),
11301
- colorStr;
11572
+ colorStr,
11573
+ val = false;
11302
11574
 
11303
11575
  if (st && wysihtml5.lang.object(st).isArray()) {
11304
11576
  st = st[0];
@@ -11307,10 +11579,8 @@ wysihtml5.Commands = Base.extend(
11307
11579
  if (st) {
11308
11580
  colorStr = st.getAttribute('style');
11309
11581
  if (colorStr) {
11310
- if (colorStr) {
11311
- val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color");
11312
- return wysihtml5.quirks.styleParser.unparseColor(val, props);
11313
- }
11582
+ val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color");
11583
+ return wysihtml5.quirks.styleParser.unparseColor(val, props);
11314
11584
  }
11315
11585
  }
11316
11586
  return false;
@@ -11318,26 +11588,36 @@ wysihtml5.Commands = Base.extend(
11318
11588
 
11319
11589
  };
11320
11590
  })(wysihtml5);
11321
- ;/* In case background adjustment to any color defined by user is preferred, we cannot use classes and must use inline styles. */
11591
+ ;/**
11592
+ * Sets text background color by inline styles
11593
+ */
11322
11594
  (function(wysihtml5) {
11323
- var REG_EXP = /(\s|^)background-color\s*:\s*[^;\s]+;?/gi;
11324
11595
 
11325
11596
  wysihtml5.commands.bgColorStyle = {
11326
11597
  exec: function(composer, command, color) {
11327
- var colorVals = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "background-color:" + color.color : "background-color:" + color, "background-color"),
11598
+ var colorVals = wysihtml5.quirks.styleParser.parseColor("background-color:" + (color.color || color), "background-color"),
11328
11599
  colString;
11329
11600
 
11330
11601
  if (colorVals) {
11331
- colString = "background-color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');';
11332
- if (colorVals[3] !== 1) {
11333
- colString += "background-color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');';
11334
- }
11335
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
11602
+ colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')';
11603
+ wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: 'backgroundColor', styleValue: colString});
11336
11604
  }
11337
11605
  },
11338
11606
 
11339
- state: function(composer, command) {
11340
- return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "background-color", REG_EXP);
11607
+ state: function(composer, command, color) {
11608
+ var colorVals = color ? wysihtml5.quirks.styleParser.parseColor("background-color:" + (color.color || color), "background-color") : null,
11609
+ colString;
11610
+
11611
+
11612
+ if (colorVals) {
11613
+ colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')';
11614
+ }
11615
+
11616
+ return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: 'backgroundColor', styleValue: colString});
11617
+ },
11618
+
11619
+ remove: function(composer, command) {
11620
+ return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: 'backgroundColor'});
11341
11621
  },
11342
11622
 
11343
11623
  stateValue: function(composer, command, props) {
@@ -11780,154 +12060,665 @@ wysihtml5.Commands = Base.extend(
11780
12060
  };
11781
12061
  }(wysihtml5));
11782
12062
  ;/**
11783
- * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
11784
- *
11785
- * #1 caret in unformatted text:
11786
- * abcdefg|
11787
- * output:
11788
- * abcdefg<b>|</b>
11789
- *
11790
- * #2 unformatted text selected:
11791
- * abc|deg|h
11792
- * output:
11793
- * abc<b>|deg|</b>h
11794
- *
11795
- * #3 unformatted text selected across boundaries:
11796
- * ab|c <span>defg|h</span>
11797
- * output:
11798
- * ab<b>|c </b><span><b>defg</b>|h</span>
11799
- *
11800
- * #4 formatted text entirely selected
11801
- * <b>|abc|</b>
11802
- * output:
11803
- * |abc|
11804
- *
11805
- * #5 formatted text partially selected
11806
- * <b>ab|c|</b>
11807
- * output:
11808
- * <b>ab</b>|c|
11809
- *
11810
- * #6 formatted text selected across boundaries
11811
- * <span>ab|c</span> <b>de|fgh</b>
11812
- * output:
11813
- * <span>ab|c</span> de|<b>fgh</b>
12063
+ * Unifies all inline tags additions and removals
12064
+ * See https://github.com/Voog/wysihtml/pull/169 for specification of action
11814
12065
  */
12066
+
11815
12067
  (function(wysihtml5) {
11816
- var // Treat <b> as <strong> and vice versa
11817
- ALIAS_MAPPING = {
11818
- "strong": "b",
11819
- "em": "i",
11820
- "b": "strong",
11821
- "i": "em"
11822
- },
11823
- htmlApplier = {};
11824
12068
 
11825
- function _getTagNames(tagName) {
11826
- var alias = ALIAS_MAPPING[tagName];
11827
- return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
12069
+ var defaultTag = "SPAN",
12070
+ 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",
12071
+ queryAliasMap = {
12072
+ "b": "b, strong",
12073
+ "strong": "b, strong",
12074
+ "em": "em, i",
12075
+ "i": "em, i"
12076
+ };
12077
+
12078
+ function hasNoClass(element) {
12079
+ return (/^\s*$/).test(element.className);
11828
12080
  }
11829
12081
 
11830
- function _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, container) {
11831
- var identifier = tagName;
11832
-
11833
- if (className) {
11834
- identifier += ":" + className;
12082
+ function hasNoStyle(element) {
12083
+ return !element.getAttribute('style') || (/^\s*$/).test(element.getAttribute('style'));
12084
+ }
12085
+
12086
+ // Associative arrays in javascript are really objects and do not have length defined
12087
+ // Thus have to check emptyness in a different way
12088
+ function hasNoAttributes(element) {
12089
+ var attr = wysihtml5.dom.getAttributes(element);
12090
+ return wysihtml5.lang.object(attr).isEmpty();
12091
+ }
12092
+
12093
+ // compares two nodes if they are semantically the same
12094
+ // Used in cleanup to find consequent semantically similar elements for merge
12095
+ function isSameNode(element1, element2) {
12096
+ var classes1, classes2,
12097
+ attr1, attr2;
12098
+
12099
+ if (element1.nodeType !== 1 || element2.nodeType !== 1) {
12100
+ return false;
11835
12101
  }
11836
- if (cssStyle) {
11837
- identifier += ":" + cssStyle;
12102
+
12103
+ if (element1.nodeName !== element2.nodeName) {
12104
+ return false;
11838
12105
  }
11839
12106
 
11840
- if (!htmlApplier[identifier]) {
11841
- htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true, cssStyle, styleRegExp, container);
12107
+ classes1 = element1.className.trim().replace(/\s+/g, ' ').split(' ');
12108
+ classes2 = element2.className.trim().replace(/\s+/g, ' ').split(' ');
12109
+ if (wysihtml5.lang.array(classes1).without(classes2).length > 0) {
12110
+ return false;
11842
12111
  }
11843
12112
 
11844
- return htmlApplier[identifier];
12113
+ attr1 = wysihtml5.dom.getAttributes(element1);
12114
+ attr2 = wysihtml5.dom.getAttributes(element2);
12115
+
12116
+ if (attr1.length !== attr2.length || !wysihtml5.lang.object(wysihtml5.lang.object(attr1).difference(attr2)).isEmpty()) {
12117
+ return false;
12118
+ }
12119
+
12120
+ return true;
11845
12121
  }
11846
12122
 
11847
- wysihtml5.commands.formatInline = {
11848
- exec: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, dontRestoreSelect, noCleanup) {
11849
- var range = composer.selection.createRange(),
11850
- ownRanges = composer.selection.getOwnRanges();
12123
+ function createWrapNode(textNode, options) {
12124
+ var nodeName = options && options.nodeName || defaultTag,
12125
+ element = textNode.ownerDocument.createElement(nodeName);
11851
12126
 
11852
- if (!ownRanges || ownRanges.length == 0) {
11853
- return false;
12127
+ // Remove similar classes before applying className
12128
+ if (options.classRegExp) {
12129
+ element.className = element.className.replace(options.classRegExp, "");
12130
+ }
12131
+
12132
+ if (options.className) {
12133
+ element.classList.add(options.className);
12134
+ }
12135
+
12136
+ if (options.styleProperty && typeof options.styleValue !== "undefined") {
12137
+ element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
12138
+ }
12139
+
12140
+ if (options.attribute) {
12141
+ if (typeof options.attribute === "object") {
12142
+ for (var a in options.attribute) {
12143
+ if (options.attribute.hasOwnProperty(a)) {
12144
+ element.setAttribute(a, options.attribute[a]);
12145
+ }
12146
+ }
12147
+ } else if (typeof options.attributeValue !== "undefined") {
12148
+ element.setAttribute(options.attribute, options.attributeValue);
11854
12149
  }
11855
- composer.selection.getSelection().removeAllRanges();
12150
+ }
11856
12151
 
11857
- _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).toggleRange(ownRanges);
12152
+ return element;
12153
+ }
11858
12154
 
11859
- if (!dontRestoreSelect) {
11860
- range.setStart(ownRanges[0].startContainer, ownRanges[0].startOffset);
11861
- range.setEnd(
11862
- ownRanges[ownRanges.length - 1].endContainer,
11863
- ownRanges[ownRanges.length - 1].endOffset
11864
- );
11865
- composer.selection.setSelection(range);
11866
- composer.selection.executeAndRestore(function() {
11867
- if (!noCleanup) {
11868
- composer.cleanUp();
12155
+ // Tests if attr2 list contains all attributes present in attr1
12156
+ // Note: attr 1 can have more attributes than attr2
12157
+ function containsSameAttributes(attr1, attr2) {
12158
+ for (var a in attr1) {
12159
+ if (attr1.hasOwnProperty(a)) {
12160
+ if (typeof attr2[a] === undefined || attr2[a] !== attr1[a]) {
12161
+ return false;
12162
+ }
12163
+ }
12164
+ }
12165
+ return true;
12166
+ }
12167
+
12168
+ // If attrbutes and values are the same > remove
12169
+ // if attributes or values
12170
+ function updateElementAttributes(element, newAttributes, toggle) {
12171
+ var attr = wysihtml5.dom.getAttributes(element),
12172
+ fullContain = containsSameAttributes(newAttributes, attr),
12173
+ attrDifference = wysihtml5.lang.object(attr).difference(newAttributes),
12174
+ a, b;
12175
+
12176
+ if (fullContain && toggle !== false) {
12177
+ for (a in newAttributes) {
12178
+ if (newAttributes.hasOwnProperty(a)) {
12179
+ element.removeAttribute(a);
12180
+ }
12181
+ }
12182
+ } else {
12183
+
12184
+ /*if (!wysihtml5.lang.object(attrDifference).isEmpty()) {
12185
+ for (b in attrDifference) {
12186
+ if (attrDifference.hasOwnProperty(b)) {
12187
+ element.removeAttribute(b);
11869
12188
  }
11870
- }, true, true);
11871
- } else if (!noCleanup) {
11872
- composer.cleanUp();
12189
+ }
12190
+ }*/
12191
+
12192
+ for (a in newAttributes) {
12193
+ if (newAttributes.hasOwnProperty(a)) {
12194
+ element.setAttribute(a, newAttributes[a]);
12195
+ }
11873
12196
  }
11874
- },
12197
+ }
12198
+ }
11875
12199
 
11876
- // Executes so that if collapsed caret is in a state and executing that state it should unformat that state
11877
- // It is achieved by selecting the entire state element before executing.
11878
- // This works on built in contenteditable inline format commands
11879
- execWithToggle: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) {
11880
- var that = this;
12200
+ function updateFormatOfElement(element, options) {
12201
+ var attr, newNode, a, newAttributes, nodeNameQuery;
11881
12202
 
11882
- if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) &&
11883
- composer.selection.isCollapsed() &&
11884
- !composer.selection.caretIsLastInSelection() &&
11885
- !composer.selection.caretIsFirstInSelection()
11886
- ) {
11887
- var state_element = that.state(composer, command, tagName, className, classRegExp)[0];
11888
- composer.selection.executeAndRestoreRangy(function() {
11889
- var parent = state_element.parentNode;
11890
- composer.selection.selectNode(state_element, true);
11891
- wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true, true);
11892
- });
12203
+
12204
+
12205
+ if (options.className) {
12206
+ if (options.toggle !== false && element.classList.contains(options.className)) {
12207
+ element.classList.remove(options.className);
11893
12208
  } else {
11894
- if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) && !composer.selection.isCollapsed()) {
11895
- composer.selection.executeAndRestoreRangy(function() {
11896
- wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true, true);
11897
- });
12209
+ element.classList.add(options.className);
12210
+ }
12211
+ if (hasNoClass(element)) {
12212
+ element.removeAttribute('class');
12213
+ }
12214
+ }
12215
+
12216
+ // change/remove style
12217
+ if (options.styleProperty) {
12218
+ if (options.toggle !== false && element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) {
12219
+ element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = '';
12220
+ } else {
12221
+ element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
12222
+ }
12223
+ }
12224
+ if (hasNoStyle(element)) {
12225
+ element.removeAttribute('style');
12226
+ }
12227
+
12228
+ if (options.attribute) {
12229
+ if (typeof options.attribute === "object") {
12230
+ newAttributes = options.attribute;
12231
+ } else {
12232
+ newAttributes = {};
12233
+ newAttributes[options.attribute] = options.attributeValue || '';
12234
+ }
12235
+ updateElementAttributes(element, newAttributes, options.toggle);
12236
+ }
12237
+
12238
+ // Handle similar semanticallys ame elements (queryAliasMap)
12239
+ nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null;
12240
+
12241
+ if ((options.nodeName && wysihtml5.dom.domNode(element).test({ query: nodeNameQuery })) || (!options.nodeName && element.nodeName === defaultTag)) {
12242
+
12243
+
12244
+ if (hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element)) {
12245
+ wysihtml5.dom.unwrap(element);
12246
+ } else if (!options.nodeName) {
12247
+ newNode = element.ownerDocument.createElement(defaultTag);
12248
+
12249
+ // pass present attributes
12250
+ attr = wysihtml5.dom.getAttributes(element);
12251
+ for (a in attr) {
12252
+ if (attr.hasOwnProperty(a)) {
12253
+ newNode.setAttribute(a, attr[a]);
12254
+ }
12255
+ }
12256
+
12257
+ while (element.firstChild) {
12258
+ newNode.appendChild(element.firstChild);
12259
+ }
12260
+ element.parentNode.insertBefore(newNode, element);
12261
+ element.parentNode.removeChild(element);
12262
+ }
12263
+
12264
+ }
12265
+ }
12266
+
12267
+ // Fetch all textnodes in selection
12268
+ // Empty textnodes are ignored except the one containing text caret
12269
+ function getSelectedTextNodes(selection, splitBounds) {
12270
+ var textNodes = [];
12271
+
12272
+ if (!selection.isCollapsed()) {
12273
+ textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) {
12274
+ // Exclude empty nodes except caret node
12275
+ return (!wysihtml5.dom.domNode(node).is.emptyTextNode());
12276
+ }, splitBounds));
12277
+ }
12278
+
12279
+ return textNodes;
12280
+ }
12281
+
12282
+ function findSimilarTextNodeWrapper(textNode, options, container, exact) {
12283
+ var node = textNode,
12284
+ similarOptions = exact ? options : correctOptionsForSimilarityCheck(options);
12285
+
12286
+ do {
12287
+ if (node.nodeType === 1 && isSimilarNode(node, similarOptions)) {
12288
+ return node;
12289
+ }
12290
+ node = node.parentNode;
12291
+ } while (node && node !== container);
12292
+
12293
+ return null;
12294
+ }
12295
+
12296
+ function correctOptionsForSimilarityCheck(options) {
12297
+ return {
12298
+ nodeName: options.nodeName || null,
12299
+ className: (!options.classRegExp) ? options.className || null : null,
12300
+ classRegExp: options.classRegExp || null,
12301
+ styleProperty: options.styleProperty || null
12302
+ };
12303
+ }
12304
+
12305
+ // Finds inline node with similar nodeName/style/className
12306
+ // If nodeName is specified inline node with the same (or alias) nodeName is expected to prove similar regardless of attributes
12307
+ function isSimilarNode(node, options) {
12308
+ var o;
12309
+ if (options.nodeName) {
12310
+ var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase();
12311
+ return wysihtml5.dom.domNode(node).test({ query: query });
12312
+ } else {
12313
+ o = wysihtml5.lang.object(options).clone();
12314
+ o.query = INLINE_ELEMENTS; // make sure only inline elements with styles and classes are counted
12315
+ return wysihtml5.dom.domNode(node).test(o);
12316
+ }
12317
+ }
12318
+
12319
+ function selectRange(composer, range) {
12320
+ var d = document.documentElement || document.body,
12321
+ oldScrollTop = d.scrollTop,
12322
+ oldScrollLeft = d.scrollLeft,
12323
+ selection = rangy.getSelection(composer.win);
12324
+
12325
+ rangy.getSelection(composer.win).removeAllRanges();
12326
+
12327
+ // IE looses focus of contenteditable on removeallranges and can not set new selection unless contenteditable is focused again
12328
+ try {
12329
+ rangy.getSelection(composer.win).addRange(range);
12330
+ } catch (e) {}
12331
+ if (!composer.doc.activeElement || !wysihtml5.dom.contains(composer.element, composer.doc.activeElement)) {
12332
+ composer.element.focus();
12333
+ d.scrollTop = oldScrollTop;
12334
+ d.scrollLeft = oldScrollLeft;
12335
+ rangy.getSelection(composer.win).addRange(range);
12336
+ }
12337
+ }
12338
+
12339
+ function selectTextNodes(textNodes, composer) {
12340
+ var range = rangy.createRange(composer.doc),
12341
+ lastText = textNodes[textNodes.length - 1];
12342
+
12343
+ if (textNodes[0] && lastText) {
12344
+ range.setStart(textNodes[0], 0);
12345
+ range.setEnd(lastText, lastText.length);
12346
+ selectRange(composer, range);
12347
+ }
12348
+
12349
+ }
12350
+
12351
+ function selectTextNode(composer, node, start, end) {
12352
+ var range = rangy.createRange(composer.doc);
12353
+ if (node) {
12354
+ range.setStart(node, start);
12355
+ range.setEnd(node, typeof end !== 'undefined' ? end : start);
12356
+ selectRange(composer, range);
12357
+ }
12358
+ }
12359
+
12360
+ function getState(composer, options, exact) {
12361
+ var searchNodes = getSelectedTextNodes(composer.selection),
12362
+ nodes = [],
12363
+ partial = false,
12364
+ node, range, caretNode;
12365
+
12366
+ if (searchNodes.length === 0 && composer.selection.isCollapsed()) {
12367
+ caretNode = composer.selection.getSelection().anchorNode;
12368
+ if (!caretNode) {
12369
+ // selection not in editor
12370
+ return {
12371
+ nodes: [],
12372
+ partial: false
12373
+ };
12374
+ }
12375
+ if (caretNode.nodeType === 3) {
12376
+ searchNodes = [caretNode];
12377
+ }
12378
+ }
12379
+
12380
+ // Handle collapsed selection caret
12381
+ if (!searchNodes.length) {
12382
+ range = composer.selection.getOwnRanges()[0];
12383
+ if (range) {
12384
+ searchNodes = [range.endContainer];
12385
+ }
12386
+ }
12387
+
12388
+ for (var i = 0, maxi = searchNodes.length; i < maxi; i++) {
12389
+ node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact);
12390
+ if (node) {
12391
+ nodes.push(node);
12392
+ } else {
12393
+ partial = true;
12394
+ }
12395
+ }
12396
+
12397
+ return {
12398
+ nodes: nodes,
12399
+ partial: partial
12400
+ };
12401
+ }
12402
+
12403
+ // Returns if caret is inside a word in textnode (not on boundary)
12404
+ // If selection anchornode is not text node, returns false
12405
+ function caretIsInsideWord(selection) {
12406
+ var anchor, offset, beforeChar, afterChar;
12407
+ if (selection) {
12408
+ anchor = selection.anchorNode;
12409
+ offset = selection.anchorOffset;
12410
+ if (anchor && anchor.nodeType === 3 && offset > 0 && offset < anchor.data.length) {
12411
+ beforeChar = anchor.data[offset - 1];
12412
+ afterChar = anchor.data[offset];
12413
+ return (/\w/).test(beforeChar) && (/\w/).test(afterChar);
12414
+ }
12415
+ }
12416
+ return false;
12417
+ }
12418
+
12419
+ // Returns a range and textnode containing object from caret position covering a whole word
12420
+ // wordOffsety describes the original position of caret in the new textNode
12421
+ // Caret has to be inside a textNode.
12422
+ function getRangeForWord(selection) {
12423
+ var anchor, offset, doc, range, offsetStart, offsetEnd, beforeChar, afterChar,
12424
+ txtNodes = [];
12425
+ if (selection) {
12426
+ anchor = selection.anchorNode;
12427
+ offset = offsetStart = offsetEnd = selection.anchorOffset;
12428
+ doc = anchor.ownerDocument;
12429
+ range = rangy.createRange(doc);
12430
+
12431
+ if (anchor && anchor.nodeType === 3) {
12432
+
12433
+ while (offsetStart > 0 && (/\w/).test(anchor.data[offsetStart - 1])) {
12434
+ offsetStart--;
12435
+ }
12436
+
12437
+ while (offsetEnd < anchor.data.length && (/\w/).test(anchor.data[offsetEnd])) {
12438
+ offsetEnd++;
12439
+ }
12440
+
12441
+ range.setStartAndEnd(anchor, offsetStart, offsetEnd);
12442
+ range.splitBoundaries();
12443
+ txtNodes = range.getNodes([3], function(node) {
12444
+ return (!wysihtml5.dom.domNode(node).is.emptyTextNode());
12445
+ });
12446
+
12447
+ return {
12448
+ wordOffset: offset - offsetStart,
12449
+ range: range,
12450
+ textNode: txtNodes[0]
12451
+ };
12452
+
12453
+ }
12454
+ }
12455
+ return false;
12456
+ }
12457
+
12458
+ // Contents of 2 elements are merged to fitst element. second element is removed as consequence
12459
+ function mergeContents(element1, element2) {
12460
+ while (element2.firstChild) {
12461
+ element1.appendChild(element2.firstChild);
12462
+ }
12463
+ element2.parentNode.removeChild(element2);
12464
+ }
12465
+
12466
+ function mergeConsequentSimilarElements(elements) {
12467
+ for (var i = elements.length; i--;) {
12468
+
12469
+ if (elements[i] && elements[i].parentNode) { // Test if node is not allready removed in cleanup
12470
+
12471
+ if (elements[i].nextSibling && isSameNode(elements[i], elements[i].nextSibling)) {
12472
+ mergeContents(elements[i], elements[i].nextSibling);
12473
+ }
12474
+
12475
+ if (elements[i].previousSibling && isSameNode(elements[i] , elements[i].previousSibling)) {
12476
+ mergeContents(elements[i].previousSibling, elements[i]);
12477
+ }
12478
+
12479
+ }
12480
+ }
12481
+ }
12482
+
12483
+ function cleanupAndSetSelection(composer, textNodes, options) {
12484
+ if (textNodes.length > 0) {
12485
+ selectTextNodes(textNodes, composer);
12486
+ }
12487
+ mergeConsequentSimilarElements(getState(composer, options).nodes);
12488
+ if (textNodes.length > 0) {
12489
+ selectTextNodes(textNodes, composer);
12490
+ }
12491
+ }
12492
+
12493
+ function cleanupAndSetCaret(composer, textNode, offset, options) {
12494
+ selectTextNode(composer, textNode, offset);
12495
+ mergeConsequentSimilarElements(getState(composer, options).nodes);
12496
+ selectTextNode(composer, textNode, offset);
12497
+ }
12498
+
12499
+ // Formats a textnode with given options
12500
+ function formatTextNode(textNode, options) {
12501
+ var wrapNode = createWrapNode(textNode, options);
12502
+
12503
+ textNode.parentNode.insertBefore(wrapNode, textNode);
12504
+ wrapNode.appendChild(textNode);
12505
+ }
12506
+
12507
+ // Changes/toggles format of a textnode
12508
+ function unformatTextNode(textNode, composer, options) {
12509
+ var container = composer.element,
12510
+ wrapNode = findSimilarTextNodeWrapper(textNode, options, container),
12511
+ newWrapNode;
12512
+
12513
+ if (wrapNode) {
12514
+ newWrapNode = wrapNode.cloneNode(false);
12515
+
12516
+ wysihtml5.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode);
12517
+ updateFormatOfElement(newWrapNode, options);
12518
+ }
12519
+ }
12520
+
12521
+ // Removes the format around textnode
12522
+ function removeFormatFromTextNode(textNode, composer, options) {
12523
+ var container = composer.element,
12524
+ wrapNode = findSimilarTextNodeWrapper(textNode, options, container);
12525
+
12526
+ if (wrapNode) {
12527
+ wysihtml5.dom.domNode(textNode).escapeParent(wrapNode);
12528
+ }
12529
+ }
12530
+
12531
+ // Creates node around caret formated with options
12532
+ function formatTextRange(range, composer, options) {
12533
+ var wrapNode = createWrapNode(range.endContainer, options);
12534
+
12535
+ range.surroundContents(wrapNode);
12536
+ composer.selection.selectNode(wrapNode);
12537
+ }
12538
+
12539
+ // Changes/toggles format of whole selection
12540
+ function updateFormat(composer, textNodes, state, options) {
12541
+ var exactState = getState(composer, options, true),
12542
+ selection = composer.selection.getSelection(),
12543
+ wordObj, textNode, newNode, i;
12544
+
12545
+ if (!textNodes.length) {
12546
+ // Selection is caret
12547
+
12548
+
12549
+ if (options.toggle !== false) {
12550
+ if (caretIsInsideWord(selection)) {
12551
+
12552
+ // Unformat whole word
12553
+ wordObj = getRangeForWord(selection);
12554
+ textNode = wordObj.textNode;
12555
+ unformatTextNode(wordObj.textNode, composer, options);
12556
+ cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
12557
+
11898
12558
  } else {
11899
- wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp);
12559
+
12560
+ // Escape caret out of format
12561
+ textNode = composer.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
12562
+ newNode = state.nodes[0].cloneNode(false);
12563
+ newNode.appendChild(textNode);
12564
+ composer.selection.splitElementAtCaret(state.nodes[0], newNode);
12565
+ updateFormatOfElement(newNode, options);
12566
+ cleanupAndSetSelection(composer, [textNode], options);
12567
+ var s = composer.selection.getSelection();
12568
+ if (s.anchorNode && s.focusNode) {
12569
+ // Has an error in IE when collapsing selection. probably from rangy
12570
+ try {
12571
+ s.collapseToEnd();
12572
+ } catch (e) {}
12573
+ }
12574
+ }
12575
+ } else {
12576
+ // In non-toggle mode the closest state element has to be found and the state updated differently
12577
+ for (i = state.nodes.length; i--;) {
12578
+ updateFormatOfElement(state.nodes[i], options);
11900
12579
  }
11901
12580
  }
11902
- },
11903
12581
 
11904
- state: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) {
11905
- var doc = composer.doc,
11906
- aliasTagName = ALIAS_MAPPING[tagName] || tagName,
11907
- ownRanges, isApplied;
12582
+ } else {
12583
+
12584
+ if (!exactState.partial && options.toggle !== false) {
12585
+
12586
+ // If whole selection (all textnodes) are in the applied format
12587
+ // remove the format from selection
12588
+ // Non-toggle mode never removes. Remove has to be called explicitly
12589
+ for (i = textNodes.length; i--;) {
12590
+ unformatTextNode(textNodes[i], composer, options);
12591
+ }
12592
+
12593
+ } else {
12594
+
12595
+ // Selection is partially in format
12596
+ // change it to new if format if textnode allreafy in similar state
12597
+ // else just apply
12598
+
12599
+ for (i = textNodes.length; i--;) {
12600
+
12601
+ if (findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
12602
+ unformatTextNode(textNodes[i], composer, options);
12603
+ }
12604
+
12605
+ if (!findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
12606
+ formatTextNode(textNodes[i], options);
12607
+ }
12608
+ }
11908
12609
 
11909
- // Check whether the document contains a node with the desired tagName
11910
- if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
11911
- !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
11912
- return false;
11913
12610
  }
11914
12611
 
11915
- // Check whether the document contains a node with the desired className
11916
- if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
11917
- return false;
12612
+ cleanupAndSetSelection(composer, textNodes, options);
12613
+ }
12614
+ }
12615
+
12616
+ // Removes format from selection
12617
+ function removeFormat(composer, textNodes, state, options) {
12618
+ var textNode, textOffset, newNode, i,
12619
+ selection = composer.selection.getSelection();
12620
+
12621
+ if (!textNodes.length) {
12622
+ textNode = selection.anchorNode;
12623
+ textOffset = selection.anchorOffset;
12624
+
12625
+ for (i = state.nodes.length; i--;) {
12626
+ wysihtml5.dom.unwrap(state.nodes[i]);
11918
12627
  }
11919
12628
 
11920
- ownRanges = composer.selection.getOwnRanges();
12629
+ cleanupAndSetCaret(composer, textNode, textOffset, options);
12630
+ } else {
12631
+ for (i = textNodes.length; i--;) {
12632
+ removeFormatFromTextNode(textNodes[i], composer, options);
12633
+ }
12634
+ cleanupAndSetSelection(composer, textNodes, options);
12635
+ }
12636
+ }
11921
12637
 
11922
- if (!ownRanges || ownRanges.length === 0) {
11923
- return false;
12638
+ // Adds format to selection
12639
+ function applyFormat(composer, textNodes, options) {
12640
+ var wordObj, i,
12641
+ selection = composer.selection.getSelection();
12642
+
12643
+ if (!textNodes.length) {
12644
+ // Handle collapsed selection caret and return
12645
+ if (caretIsInsideWord(selection)) {
12646
+
12647
+ wordObj = getRangeForWord(selection);
12648
+ formatTextNode(wordObj.textNode, options);
12649
+ cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
12650
+
12651
+ } else {
12652
+ var r = composer.selection.getOwnRanges()[0];
12653
+ if (r) {
12654
+ formatTextRange(r, composer, options);
12655
+ }
12656
+ }
12657
+
12658
+ } else {
12659
+ // Handle textnodes in selection and apply format
12660
+ for (i = textNodes.length; i--;) {
12661
+ formatTextNode(textNodes[i], options);
11924
12662
  }
12663
+ cleanupAndSetSelection(composer, textNodes, options);
12664
+ }
12665
+ }
12666
+
12667
+ // If properties is passed as a string, correct options with that nodeName
12668
+ function fixOptions(options) {
12669
+ options = (typeof options === "string") ? { nodeName: options } : options;
12670
+ if (options.nodeName) { options.nodeName = options.nodeName.toUpperCase(); }
12671
+ return options;
12672
+ }
12673
+
12674
+ wysihtml5.commands.formatInline = {
12675
+
12676
+ // Basics:
12677
+ // In case of plain text or inline state not set wrap all non-empty textnodes with
12678
+ // In case a similar inline wrapper node is detected on one of textnodes, the wrapper node is changed (if fully contained) or split and changed (partially contained)
12679
+ // In case of changing mode every textnode is addressed separatly
12680
+ exec: function(composer, command, options) {
12681
+ options = fixOptions(options);
12682
+
12683
+ // Join adjactent textnodes first
12684
+ composer.element.normalize();
12685
+
12686
+ var textNodes = getSelectedTextNodes(composer.selection, true),
12687
+ state = getState(composer, options);
12688
+ if (state.nodes.length > 0) {
12689
+ // Text allready has the format applied
12690
+ updateFormat(composer, textNodes, state, options);
12691
+ } else {
12692
+ // Selection is not in the applied format
12693
+ applyFormat(composer, textNodes, options);
12694
+ }
12695
+ composer.element.normalize();
12696
+ },
12697
+
12698
+ remove: function(composer, command, options) {
12699
+ options = fixOptions(options);
12700
+ composer.element.normalize();
11925
12701
 
11926
- isApplied = _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).isAppliedToRange(ownRanges);
12702
+ var textNodes = getSelectedTextNodes(composer.selection, true),
12703
+ state = getState(composer, options);
11927
12704
 
11928
- return (isApplied && isApplied.elements) ? isApplied.elements : false;
12705
+ if (state.nodes.length > 0) {
12706
+ // Text allready has the format applied
12707
+ removeFormat(composer, textNodes, state, options);
12708
+ }
12709
+
12710
+ composer.element.normalize();
12711
+ },
12712
+
12713
+ state: function(composer, command, options) {
12714
+ options = fixOptions(options);
12715
+
12716
+ var nodes = getState(composer, options, true).nodes;
12717
+
12718
+ return (nodes.length === 0) ? false : nodes;
11929
12719
  }
11930
12720
  };
12721
+
11931
12722
  })(wysihtml5);
11932
12723
  ;(function(wysihtml5) {
11933
12724
 
@@ -12275,20 +13066,22 @@ wysihtml5.Commands = Base.extend(
12275
13066
 
12276
13067
  })(wysihtml5);
12277
13068
  ;(function(wysihtml5){
13069
+
13070
+ var nodeOptions = {
13071
+ nodeName: "I",
13072
+ toggle: true
13073
+ };
13074
+
12278
13075
  wysihtml5.commands.italic = {
12279
13076
  exec: function(composer, command) {
12280
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "i");
13077
+ wysihtml5.commands.formatInline.exec(composer, command, nodeOptions);
12281
13078
  },
12282
13079
 
12283
13080
  state: function(composer, command) {
12284
- // element.ownerDocument.queryCommandState("italic") results:
12285
- // firefox: only <i>
12286
- // chrome: <i>, <em>, <blockquote>, ...
12287
- // ie: <i>, <em>
12288
- // opera: only <i>
12289
- return wysihtml5.commands.formatInline.state(composer, command, "i");
13081
+ return wysihtml5.commands.formatInline.state(composer, command, nodeOptions);
12290
13082
  }
12291
13083
  };
13084
+
12292
13085
  }(wysihtml5));
12293
13086
  ;(function(wysihtml5) {
12294
13087
 
@@ -12431,15 +13224,22 @@ wysihtml5.Commands = Base.extend(
12431
13224
  };
12432
13225
  }(wysihtml5));
12433
13226
  ;(function(wysihtml5){
13227
+
13228
+ var nodeOptions = {
13229
+ nodeName: "U",
13230
+ toggle: true
13231
+ };
13232
+
12434
13233
  wysihtml5.commands.underline = {
12435
13234
  exec: function(composer, command) {
12436
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "u");
13235
+ wysihtml5.commands.formatInline.exec(composer, command, nodeOptions);
12437
13236
  },
12438
13237
 
12439
13238
  state: function(composer, command) {
12440
- return wysihtml5.commands.formatInline.state(composer, command, "u");
13239
+ return wysihtml5.commands.formatInline.state(composer, command, nodeOptions);
12441
13240
  }
12442
13241
  };
13242
+
12443
13243
  }(wysihtml5));
12444
13244
  ;(function(wysihtml5){
12445
13245
  wysihtml5.commands.undo = {
@@ -12703,24 +13503,36 @@ wysihtml5.Commands = Base.extend(
12703
13503
  };
12704
13504
  }(wysihtml5));
12705
13505
  ;(function(wysihtml5){
13506
+
13507
+ var nodeOptions = {
13508
+ nodeName: "SUB",
13509
+ toggle: true
13510
+ };
13511
+
12706
13512
  wysihtml5.commands.subscript = {
12707
13513
  exec: function(composer, command) {
12708
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "sub");
13514
+ wysihtml5.commands.formatInline.exec(composer, command, nodeOptions);
12709
13515
  },
12710
13516
 
12711
13517
  state: function(composer, command) {
12712
- return wysihtml5.commands.formatInline.state(composer, command, "sub");
13518
+ return wysihtml5.commands.formatInline.state(composer, command, nodeOptions);
12713
13519
  }
12714
13520
  };
12715
13521
  }(wysihtml5));
12716
- ;(function(wysihtml5){
13522
+ ;(function(wysihtml5) {
13523
+
13524
+ var nodeOptions = {
13525
+ nodeName: "SUP",
13526
+ toggle: true
13527
+ };
13528
+
12717
13529
  wysihtml5.commands.superscript = {
12718
13530
  exec: function(composer, command) {
12719
- wysihtml5.commands.formatInline.execWithToggle(composer, command, "sup");
13531
+ wysihtml5.commands.formatInline.exec(composer, command, nodeOptions);
12720
13532
  },
12721
13533
 
12722
13534
  state: function(composer, command) {
12723
- return wysihtml5.commands.formatInline.state(composer, command, "sup");
13535
+ return wysihtml5.commands.formatInline.state(composer, command, nodeOptions);
12724
13536
  }
12725
13537
  };
12726
13538
  }(wysihtml5));