wysihtml5x-rails 0.4.17 → 0.5.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8b1f43eeac26ae1738b1861e4eda9180911bf2e
4
- data.tar.gz: 591e2337f0b6730db35a2ee2134530ffac88d085
3
+ metadata.gz: a91d08305f9e4988813586289aab3aecd9de35d3
4
+ data.tar.gz: 073f79d4f0eb77987f9d6e7f0869dbcbb4d1b19f
5
5
  SHA512:
6
- metadata.gz: 4de851b6e42d4bc378fd054c730b001d4f6cab03007c198df769a70d6e9402fc553f00ca1a44d10ff879982cbd8e0874bf51d378f91978c6b8b040a862a4ddc2
7
- data.tar.gz: 03ee2235f5dd67043c433dc3111f23b39b47ec7587bd0eb5222f77789138504f0a0f886a919bcfc69931aa399b1db3ea3db44b1d92ee3cb08a9904a299dc08d2
6
+ metadata.gz: c6b1f8f01e075964d9b42ba38d8a0787d1b822973120824e7d1186a8d13c0e467869ac2f175b4fbf52fc9222cd1b2ec6451992ab4ce9e8b2ccd044ef36121345
7
+ data.tar.gz: 167c5ea2b1b989c3dc14887fde9bd4164004a917990aac2176b7a1ce1d959a993f67d67fad184f3dc36d65fbab9e1ce7df86170f970c568f2b8fb221f7346545
@@ -1,5 +1,5 @@
1
1
  module Wysihtml5x
2
2
  module Rails
3
- VERSION = "0.4.17"
3
+ VERSION = "0.5.0.beta1"
4
4
  end
5
5
  end
@@ -106,7 +106,8 @@ var wysihtml5ParserRules = {
106
106
  * - src: allows something like "/foobar.jpg", "http://google.com", ...
107
107
  * - href: allows something like "mailto:bert@foo.com", "http://google.com", "/foobar.jpg"
108
108
  * - alt: strips unwanted characters. if the attribute is not set, then it gets set (to ensure valid and compatible HTML)
109
- * - numbers: ensures that the attribute only contains numeric characters
109
+ * - numbers: ensures that the attribute only contains numeric (integer) characters (no float values or units)
110
+ * - dimension: for with/height attributes where floating point numbrs and percentages are allowed
110
111
  * - any: allows anything to pass
111
112
  */
112
113
  "tags": {
@@ -197,10 +198,10 @@ var wysihtml5ParserRules = {
197
198
  },
198
199
  "img": {
199
200
  "check_attributes": {
200
- "width": "numbers",
201
+ "width": "dimension",
201
202
  "alt": "alt",
202
203
  "src": "url", // if you compiled master manually then change this from 'url' to 'src'
203
- "height": "numbers"
204
+ "height": "dimension"
204
205
  },
205
206
  "add_class": {
206
207
  "align": "align_img"
@@ -184,7 +184,8 @@ var wysihtml5ParserRules = {
184
184
  * - src: allows something like "/foobar.jpg", "http://google.com", ...
185
185
  * - href: allows something like "mailto:bert@foo.com", "http://google.com", "/foobar.jpg"
186
186
  * - alt: strips unwanted characters. if the attribute is not set, then it gets set (to ensure valid and compatible HTML)
187
- * - numbers: ensures that the attribute only contains numeric characters
187
+ * - numbers: ensures that the attribute only contains numeric (integer) characters (no float values or units)
188
+ * - dimension: for with/height attributes where floating point numbrs and percentages are allowed
188
189
  * - any: allows anything to pass
189
190
  */
190
191
  "tags": {
@@ -278,10 +279,10 @@ var wysihtml5ParserRules = {
278
279
  "valid_image_src": 1
279
280
  },
280
281
  "check_attributes": {
281
- "width": "numbers",
282
+ "width": "dimension",
282
283
  "alt": "alt",
283
284
  "src": "src", // if you compiled master manually then change this from 'url' to 'src'
284
- "height": "numbers"
285
+ "height": "dimension"
285
286
  },
286
287
  "add_class": {
287
288
  "align": "align_img"
@@ -3,6 +3,17 @@
3
3
  // IE8 SUPPORT BLOCK
4
4
  // You can compile wuthout all this if IE8 is not needed
5
5
 
6
+ // String trim for ie8
7
+ if (!String.prototype.trim) {
8
+ (function() {
9
+ // Make sure we trim BOM and NBSP
10
+ var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
11
+ String.prototype.trim = function() {
12
+ return this.replace(rtrim, '');
13
+ };
14
+ })();
15
+ }
16
+
6
17
  // addEventListener, removeEventListener
7
18
  // TODO: make usage of wysihtml5.dom.observe obsolete
8
19
  (function() {
@@ -102,6 +113,14 @@ if(!Array.isArray) {
102
113
  };
103
114
  }
104
115
 
116
+ // Array indexOf for ie8
117
+ if (!Array.prototype.indexOf) {
118
+ Array.prototype.indexOf = function(a,f) {
119
+ for(var c=this.length,r=-1,d=f>>>0; ~(c-d); r=this[--c]===a?c:r);
120
+ return r;
121
+ };
122
+ }
123
+
105
124
  // Function.prototype.bind()
106
125
  // TODO: clean the code from variable 'that' as it can be confusing
107
126
  if (!Function.prototype.bind) {
@@ -127,8 +146,237 @@ if (!Function.prototype.bind) {
127
146
 
128
147
  return fBound;
129
148
  };
130
- };/**
131
- * @license wysihtml5x v0.4.17
149
+ }
150
+
151
+ // Element.matches Adds ie8 support and unifies nonstandard function names in other browsers
152
+ this.Element && function(ElementPrototype) {
153
+ ElementPrototype.matches = ElementPrototype.matches ||
154
+ ElementPrototype.matchesSelector ||
155
+ ElementPrototype.mozMatchesSelector ||
156
+ ElementPrototype.msMatchesSelector ||
157
+ ElementPrototype.oMatchesSelector ||
158
+ ElementPrototype.webkitMatchesSelector ||
159
+ function (selector) {
160
+ var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
161
+ while (nodes[++i] && nodes[i] != node);
162
+ return !!nodes[i];
163
+ };
164
+ }(Element.prototype);
165
+
166
+ // Element.classList for ie8-9 (toggle all IE)
167
+ // source http://purl.eligrey.com/github/classList.js/blob/master/classList.js
168
+
169
+ if ("document" in self) {
170
+ // Full polyfill for browsers with no classList support
171
+ if (!("classList" in document.createElement("_"))) {
172
+ (function(view) {
173
+ "use strict";
174
+ if (!('Element' in view)) return;
175
+
176
+ var
177
+ classListProp = "classList",
178
+ protoProp = "prototype",
179
+ elemCtrProto = view.Element[protoProp],
180
+ objCtr = Object,
181
+ strTrim = String[protoProp].trim || function() {
182
+ return this.replace(/^\s+|\s+$/g, "");
183
+ },
184
+ arrIndexOf = Array[protoProp].indexOf || function(item) {
185
+ var
186
+ i = 0,
187
+ len = this.length;
188
+ for (; i < len; i++) {
189
+ if (i in this && this[i] === item) {
190
+ return i;
191
+ }
192
+ }
193
+ return -1;
194
+ }, // Vendors: please allow content code to instantiate DOMExceptions
195
+ DOMEx = function(type, message) {
196
+ this.name = type;
197
+ this.code = DOMException[type];
198
+ this.message = message;
199
+ },
200
+ checkTokenAndGetIndex = function(classList, token) {
201
+ if (token === "") {
202
+ throw new DOMEx(
203
+ "SYNTAX_ERR", "An invalid or illegal string was specified"
204
+ );
205
+ }
206
+ if (/\s/.test(token)) {
207
+ throw new DOMEx(
208
+ "INVALID_CHARACTER_ERR", "String contains an invalid character"
209
+ );
210
+ }
211
+ return arrIndexOf.call(classList, token);
212
+ },
213
+ ClassList = function(elem) {
214
+ var
215
+ trimmedClasses = strTrim.call(elem.getAttribute("class") || ""),
216
+ classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [],
217
+ i = 0,
218
+ len = classes.length;
219
+ for (; i < len; i++) {
220
+ this.push(classes[i]);
221
+ }
222
+ this._updateClassName = function() {
223
+ elem.setAttribute("class", this.toString());
224
+ };
225
+ },
226
+ classListProto = ClassList[protoProp] = [],
227
+ classListGetter = function() {
228
+ return new ClassList(this);
229
+ };
230
+ // Most DOMException implementations don't allow calling DOMException's toString()
231
+ // on non-DOMExceptions. Error's toString() is sufficient here.
232
+ DOMEx[protoProp] = Error[protoProp];
233
+ classListProto.item = function(i) {
234
+ return this[i] || null;
235
+ };
236
+ classListProto.contains = function(token) {
237
+ token += "";
238
+ return checkTokenAndGetIndex(this, token) !== -1;
239
+ };
240
+ classListProto.add = function() {
241
+ var
242
+ tokens = arguments,
243
+ i = 0,
244
+ l = tokens.length,
245
+ token, updated = false;
246
+ do {
247
+ token = tokens[i] + "";
248
+ if (checkTokenAndGetIndex(this, token) === -1) {
249
+ this.push(token);
250
+ updated = true;
251
+ }
252
+ }
253
+ while (++i < l);
254
+
255
+ if (updated) {
256
+ this._updateClassName();
257
+ }
258
+ };
259
+ classListProto.remove = function() {
260
+ var
261
+ tokens = arguments,
262
+ i = 0,
263
+ l = tokens.length,
264
+ token, updated = false,
265
+ index;
266
+ do {
267
+ token = tokens[i] + "";
268
+ index = checkTokenAndGetIndex(this, token);
269
+ while (index !== -1) {
270
+ this.splice(index, 1);
271
+ updated = true;
272
+ index = checkTokenAndGetIndex(this, token);
273
+ }
274
+ }
275
+ while (++i < l);
276
+
277
+ if (updated) {
278
+ this._updateClassName();
279
+ }
280
+ };
281
+ classListProto.toggle = function(token, force) {
282
+ token += "";
283
+
284
+ var
285
+ result = this.contains(token),
286
+ method = result ?
287
+ force !== true && "remove" :
288
+ force !== false && "add";
289
+
290
+ if (method) {
291
+ this[method](token);
292
+ }
293
+
294
+ if (force === true || force === false) {
295
+ return force;
296
+ } else {
297
+ return !result;
298
+ }
299
+ };
300
+ classListProto.toString = function() {
301
+ return this.join(" ");
302
+ };
303
+
304
+ if (objCtr.defineProperty) {
305
+ var classListPropDesc = {
306
+ get: classListGetter,
307
+ enumerable: true,
308
+ configurable: true
309
+ };
310
+ try {
311
+ objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
312
+ } catch (ex) { // IE 8 doesn't support enumerable:true
313
+ if (ex.number === -0x7FF5EC54) {
314
+ classListPropDesc.enumerable = false;
315
+ objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
316
+ }
317
+ }
318
+ } else if (objCtr[protoProp].__defineGetter__) {
319
+ elemCtrProto.__defineGetter__(classListProp, classListGetter);
320
+ }
321
+
322
+ }(self));
323
+
324
+ } else {
325
+ // There is full or partial native classList support, so just check if we need
326
+ // to normalize the add/remove and toggle APIs.
327
+
328
+ (function() {
329
+ "use strict";
330
+
331
+ var testElement = document.createElement("_");
332
+
333
+ testElement.classList.add("c1", "c2");
334
+
335
+ // Polyfill for IE 10/11 and Firefox <26, where classList.add and
336
+ // classList.remove exist but support only one argument at a time.
337
+ if (!testElement.classList.contains("c2")) {
338
+ var createMethod = function(method) {
339
+ var original = DOMTokenList.prototype[method];
340
+
341
+ DOMTokenList.prototype[method] = function(token) {
342
+ var i, len = arguments.length;
343
+
344
+ for (i = 0; i < len; i++) {
345
+ token = arguments[i];
346
+ original.call(this, token);
347
+ }
348
+ };
349
+ };
350
+ createMethod('add');
351
+ createMethod('remove');
352
+ }
353
+
354
+ testElement.classList.toggle("c3", false);
355
+
356
+ // Polyfill for IE 10 and Firefox <24, where classList.toggle does not
357
+ // support the second argument.
358
+ if (testElement.classList.contains("c3")) {
359
+ var _toggle = DOMTokenList.prototype.toggle;
360
+
361
+ DOMTokenList.prototype.toggle = function(token, force) {
362
+ if (1 in arguments && !this.contains(token) === !force) {
363
+ return force;
364
+ } else {
365
+ return _toggle.call(this, token);
366
+ }
367
+ };
368
+
369
+ }
370
+
371
+ testElement = null;
372
+ }());
373
+
374
+ }
375
+
376
+ }
377
+
378
+ ;/**
379
+ * @license wysihtml5x v0.5.0-beta1
132
380
  * https://github.com/Edicy/wysihtml5
133
381
  *
134
382
  * Author: Christopher Blum (https://github.com/tiff)
@@ -139,7 +387,7 @@ if (!Function.prototype.bind) {
139
387
  *
140
388
  */
141
389
  var wysihtml5 = {
142
- version: "0.4.17",
390
+ version: "0.5.0-beta1",
143
391
 
144
392
  // namespaces
145
393
  commands: {},
@@ -4688,6 +4936,15 @@ wysihtml5.browser = (function() {
4688
4936
  */
4689
4937
  supportsModenPaste: function () {
4690
4938
  return !("clipboardData" in window);
4939
+ },
4940
+
4941
+ // Unifies the property names of element.style by returning the suitable property name for current browser
4942
+ // Input property key must be the standard
4943
+ fixStyleKey: function(key) {
4944
+ if (key === "cssFloat") {
4945
+ return ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat";
4946
+ }
4947
+ return key;
4691
4948
  }
4692
4949
  };
4693
4950
  })();
@@ -5422,22 +5679,26 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5422
5679
  * });
5423
5680
  */
5424
5681
  (function(wysihtml5) {
5425
-
5426
5682
  wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
5427
- return wysihtml5.dom.observe(container, eventName, function(event) {
5428
- var target = event.target,
5429
- match = wysihtml5.lang.array(container.querySelectorAll(selector));
5683
+ var callback = function(event) {
5684
+ var target = event.target,
5685
+ element = (target.nodeType === 3) ? target.parentNode : target, // IE has .contains only seeing elements not textnodes
5686
+ matches = container.querySelectorAll(selector);
5430
5687
 
5431
- while (target && target !== container) {
5432
- if (match.contains(target)) {
5433
- handler.call(target, event);
5434
- break;
5688
+ for (var i = 0, max = matches.length; i < max; i++) {
5689
+ if (matches[i].contains(element)) {
5690
+ handler.call(matches[i], event);
5435
5691
  }
5436
- target = target.parentNode;
5437
5692
  }
5438
- });
5439
- };
5693
+ };
5440
5694
 
5695
+ container.addEventListener(eventName, callback, false);
5696
+ return {
5697
+ stop: function() {
5698
+ container.removeEventListener(eventName, callback, false);
5699
+ }
5700
+ };
5701
+ };
5441
5702
  })(wysihtml5);
5442
5703
  ;// TODO: Refactor dom tree traversing here
5443
5704
  (function(wysihtml5) {
@@ -5515,6 +5776,103 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5515
5776
  }
5516
5777
 
5517
5778
  return wysihtml5.dom.domNode(lastChild).lastLeafNode(options);
5779
+ },
5780
+
5781
+ /*
5782
+ Tests a node against properties, and returns true if matches.
5783
+ Tests on principle that all properties defined must have at least one match.
5784
+ styleValue parameter works in context of styleProperty and has no effect otherwise.
5785
+ Returns true if element matches and false if it does not.
5786
+
5787
+ Properties for filtering element:
5788
+ {
5789
+ query: selector string,
5790
+ nodeName: string (uppercase),
5791
+ className: string,
5792
+ classRegExp: regex,
5793
+ styleProperty: string or [],
5794
+ styleValue: string, [] or regex
5795
+ }
5796
+
5797
+ Example:
5798
+ var node = wysihtml5.dom.domNode(element).test({})
5799
+ */
5800
+ test: function(properties) {
5801
+ var prop;
5802
+
5803
+ // retuern false if properties object is not defined
5804
+ if (!properties) {
5805
+ return false;
5806
+ }
5807
+
5808
+ // Only element nodes can be tested for these properties
5809
+ if (node.nodeType !== 1) {
5810
+ return false;
5811
+ }
5812
+
5813
+ if (properties.query) {
5814
+ if (!node.matches(properties.query)) {
5815
+ return false;
5816
+ }
5817
+ }
5818
+
5819
+ if (properties.nodeName && node.nodeName !== properties.nodeName) {
5820
+ return false;
5821
+ }
5822
+
5823
+ if (properties.className && !node.classList.contains(properties.className)) {
5824
+ return false;
5825
+ }
5826
+
5827
+ // classRegExp check (useful for classname begins with logic)
5828
+ if (properties.classRegExp) {
5829
+ var matches = (node.className || "").match(properties.classRegExp) || [];
5830
+ if (matches.length === 0) {
5831
+ return false;
5832
+ }
5833
+ }
5834
+
5835
+ // styleProperty check
5836
+ if (properties.styleProperty && properties.styleProperty.length > 0) {
5837
+ var hasOneStyle = false,
5838
+ styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty];
5839
+ for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) {
5840
+ // Some old IE-s have different property name for cssFloat
5841
+ prop = wysihtml5.browser.fixStyleKey(styles[j]);
5842
+ if (node.style[prop]) {
5843
+ if (properties.styleValue) {
5844
+ // Style value as additional parameter
5845
+ if (properties.styleValue instanceof RegExp) {
5846
+ // style value as Regexp
5847
+ if (node.style[prop].trim().match(properties.styleValue).length > 0) {
5848
+ hasOneStyle = true;
5849
+ break;
5850
+ }
5851
+ } else if (Array.isArray(properties.styleValue)) {
5852
+ // style value as array
5853
+ if (properties.styleValue.indexOf(node.style[prop].trim())) {
5854
+ hasOneStyle = true;
5855
+ break;
5856
+ }
5857
+ } else {
5858
+ // style value as string
5859
+ if (properties.styleValue === node.style[prop].trim()) {
5860
+ hasOneStyle = true;
5861
+ break;
5862
+ }
5863
+ }
5864
+ } else {
5865
+ hasOneStyle = true;
5866
+ break;
5867
+ }
5868
+ }
5869
+ if (!hasOneStyle) {
5870
+ return false;
5871
+ }
5872
+ }
5873
+ }
5874
+
5875
+ return true;
5518
5876
  }
5519
5877
 
5520
5878
  };
@@ -5585,77 +5943,35 @@ wysihtml5.dom.getAsDom = (function() {
5585
5943
  })();
5586
5944
  ;/**
5587
5945
  * Walks the dom tree from the given node up until it finds a match
5588
- * Designed for optimal performance.
5589
5946
  *
5590
5947
  * @param {Element} node The from which to check the parent nodes
5591
- * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
5948
+ * @param {Object} matchingSet Object to match against, Properties for filtering element:
5949
+ * {
5950
+ * query: selector string,
5951
+ * classRegExp: regex,
5952
+ * styleProperty: string or [],
5953
+ * styleValue: string, [] or regex
5954
+ * }
5592
5955
  * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
5956
+ * @param {Element} Optional, defines the container that limits the search
5957
+ *
5593
5958
  * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
5594
- * @example
5595
- * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
5596
- * // ... or ...
5597
- * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
5598
- * // ... or ...
5599
- * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
5600
- */
5601
- wysihtml5.dom.getParentElement = (function() {
5602
-
5603
- function _isSameNodeName(nodeName, desiredNodeNames) {
5604
- if (!desiredNodeNames || !desiredNodeNames.length) {
5605
- return true;
5606
- }
5607
-
5608
- if (typeof(desiredNodeNames) === "string") {
5609
- return nodeName === desiredNodeNames;
5610
- } else {
5611
- return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
5612
- }
5613
- }
5614
-
5615
- function _isElement(node) {
5616
- return node.nodeType === wysihtml5.ELEMENT_NODE;
5617
- }
5618
-
5619
- function _hasClassName(element, className, classRegExp) {
5620
- var classNames = (element.className || "").match(classRegExp) || [];
5621
- if (!className) {
5622
- return !!classNames.length;
5623
- }
5624
- return classNames[classNames.length - 1] === className;
5625
- }
5626
-
5627
- function _hasStyle(element, cssStyle, styleRegExp) {
5628
- var styles = (element.getAttribute('style') || "").match(styleRegExp) || [];
5629
- if (!cssStyle) {
5630
- return !!styles.length;
5631
- }
5632
- return styles[styles.length - 1] === cssStyle;
5633
- }
5634
-
5635
- return function(node, matchingSet, levels, container) {
5636
- var findByStyle = (matchingSet.cssStyle || matchingSet.styleRegExp),
5637
- findByClass = (matchingSet.className || matchingSet.classRegExp);
5638
-
5639
- levels = levels || 50; // Go max 50 nodes upwards from current node
5959
+ */
5640
5960
 
5641
- // make the matching class regex from class name if omitted
5642
- if (findByClass && !matchingSet.classRegExp) {
5643
- matchingSet.classRegExp = new RegExp(matchingSet.className);
5644
- }
5961
+ wysihtml5.dom.getParentElement = (function() {
5645
5962
 
5963
+ return function(node, properties, levels, container) {
5964
+ levels = levels || 50;
5646
5965
  while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) {
5647
- if (_isElement(node) && (!matchingSet.nodeName || _isSameNodeName(node.nodeName, matchingSet.nodeName)) &&
5648
- (!findByStyle || _hasStyle(node, matchingSet.cssStyle, matchingSet.styleRegExp)) &&
5649
- (!findByClass || _hasClassName(node, matchingSet.className, matchingSet.classRegExp))
5650
- ) {
5966
+ if (wysihtml5.dom.domNode(node).test(properties)) {
5651
5967
  return node;
5652
5968
  }
5653
5969
  node = node.parentNode;
5654
5970
  }
5655
5971
  return null;
5656
5972
  };
5657
- })();
5658
- ;/**
5973
+
5974
+ })();;/**
5659
5975
  * Get element's style for a specific css property
5660
5976
  *
5661
5977
  * @param {Element} element The element on which to retrieve the style
@@ -6653,7 +6969,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) {
6653
6969
  })(),
6654
6970
 
6655
6971
  href: (function() {
6656
- var REG_EXP = /^(#|\/|https?:\/\/|mailto:)/i;
6972
+ var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i;
6657
6973
  return function(attributeValue) {
6658
6974
  if (!attributeValue || !attributeValue.match(REG_EXP)) {
6659
6975
  return null;
@@ -6674,6 +6990,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) {
6674
6990
  };
6675
6991
  })(),
6676
6992
 
6993
+ // Integers. Does not work with floating point numbers and units
6677
6994
  numbers: (function() {
6678
6995
  var REG_EXP = /\D/g;
6679
6996
  return function(attributeValue) {
@@ -6682,6 +6999,15 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) {
6682
6999
  };
6683
7000
  })(),
6684
7001
 
7002
+ // Useful for with/height attributes where floating points and percentages are allowed
7003
+ dimension: (function() {
7004
+ var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/;
7005
+ return function(attributeValue) {
7006
+ attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3");
7007
+ return attributeValue || null;
7008
+ };
7009
+ })(),
7010
+
6685
7011
  any: (function() {
6686
7012
  return function(attributeValue) {
6687
7013
  return attributeValue;
@@ -7512,7 +7838,7 @@ wysihtml5.dom.isLoadedImage = function (node) {
7512
7838
  var TableModifyerByCell = function (cell, table) {
7513
7839
  if (cell) {
7514
7840
  this.cell = cell;
7515
- this.table = api.getParentElement(cell, { nodeName: ["TABLE"] });
7841
+ this.table = api.getParentElement(cell, { query: "table" });
7516
7842
  } else if (table) {
7517
7843
  this.table = table;
7518
7844
  this.cell = this.table.querySelectorAll('th, td')[0];
@@ -7791,7 +8117,7 @@ wysihtml5.dom.isLoadedImage = function (node) {
7791
8117
  for (var cidx = 0, cmax = this.map[idx.row].length; cidx < cmax; cidx++) {
7792
8118
  c = this.map[idx.row][cidx];
7793
8119
  if (c.isReal) {
7794
- r = api.getParentElement(c.el, { nodeName: ["TR"] });
8120
+ r = api.getParentElement(c.el, { query: "tr" });
7795
8121
  if (r) {
7796
8122
  return r;
7797
8123
  }
@@ -7799,7 +8125,7 @@ wysihtml5.dom.isLoadedImage = function (node) {
7799
8125
  }
7800
8126
 
7801
8127
  if (r === null && force) {
7802
- r = api.getParentElement(this.map[idx.row][idx.col].el, { nodeName: ["TR"] }) || null;
8128
+ r = api.getParentElement(this.map[idx.row][idx.col].el, { query: "tr" }) || null;
7803
8129
  }
7804
8130
 
7805
8131
  return r;
@@ -7819,7 +8145,7 @@ wysihtml5.dom.isLoadedImage = function (node) {
7819
8145
  } else {
7820
8146
  var rr = this.table.ownerDocument.createElement('tr');
7821
8147
  rr.appendChild(new_cells);
7822
- insertAfter(api.getParentElement(c.el, { nodeName: ["TR"] }), rr);
8148
+ insertAfter(api.getParentElement(c.el, { query: "tr" }), rr);
7823
8149
  }
7824
8150
  },
7825
8151
 
@@ -8091,7 +8417,7 @@ wysihtml5.dom.isLoadedImage = function (node) {
8091
8417
 
8092
8418
  // Removes the row of selected cell
8093
8419
  removeRow: function() {
8094
- var oldRow = api.getParentElement(this.cell, { nodeName: ["TR"] });
8420
+ var oldRow = api.getParentElement(this.cell, { query: "tr" });
8095
8421
  if (oldRow) {
8096
8422
  this.setTableMap();
8097
8423
  this.idx = this.getMapIndex(this.cell);
@@ -8173,7 +8499,7 @@ wysihtml5.dom.isLoadedImage = function (node) {
8173
8499
  insertAfter(this.getRealRowEl(true), newRow);
8174
8500
  break;
8175
8501
  case 'above':
8176
- var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { nodeName: ["TR"] });
8502
+ var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { query: "tr" });
8177
8503
  if (cr) {
8178
8504
  cr.parentNode.insertBefore(newRow, cr);
8179
8505
  }
@@ -8272,7 +8598,7 @@ wysihtml5.dom.isLoadedImage = function (node) {
8272
8598
 
8273
8599
  handleCellAddWithRowspan: function (cell, ridx, where) {
8274
8600
  var addRowsNr = parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1,
8275
- crow = api.getParentElement(cell.el, { nodeName: ["TR"] }),
8601
+ crow = api.getParentElement(cell.el, { query: "tr" }),
8276
8602
  cType = cell.el.tagName.toLowerCase(),
8277
8603
  cidx, temp_r_cells,
8278
8604
  doc = this.table.ownerDocument,
@@ -8452,13 +8778,22 @@ wysihtml5.dom.query = function(elements, query) {
8452
8778
  };
8453
8779
  }
8454
8780
  })();
8455
- ;wysihtml5.dom.unwrap = function(node) {
8781
+ ;/* Unwraps element and returns list of childNodes that the node contained.
8782
+ *
8783
+ * Example:
8784
+ * var childnodes = wysihtml5.dom.unwrap(document.querySelector('.unwrap-me'));
8785
+ */
8786
+
8787
+ wysihtml5.dom.unwrap = function(node) {
8788
+ var children = [];
8456
8789
  if (node.parentNode) {
8457
8790
  while (node.lastChild) {
8791
+ children.unshift(node.lastChild);
8458
8792
  wysihtml5.dom.insert(node.lastChild).after(node);
8459
8793
  }
8460
8794
  node.parentNode.removeChild(node);
8461
8795
  }
8796
+ return children;
8462
8797
  };;/*
8463
8798
  * Methods for fetching pasted html before it gets inserted into content
8464
8799
  **/
@@ -8499,6 +8834,11 @@ wysihtml5.dom.getPastedHtmlWithDiv = function (composer, f) {
8499
8834
  f(cleanerDiv.innerHTML);
8500
8835
  cleanerDiv.parentNode.removeChild(cleanerDiv);
8501
8836
  }, 0);
8837
+ };;wysihtml5.dom.removeInvisibleSpaces = function(node) {
8838
+ var textNodes = wysihtml5.dom.getTextNodes(node);
8839
+ for (var n = textNodes.length; n--;) {
8840
+ textNodes[n].nodeValue = textNodes[n].nodeValue.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, "");
8841
+ }
8502
8842
  };;/**
8503
8843
  * Fix most common html formatting misbehaviors of browsers implementation when inserting
8504
8844
  * content via copy & paste contentEditable
@@ -8668,7 +9008,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
8668
9008
  function init () {
8669
9009
 
8670
9010
  dom.observe(editable, "mousedown", function(event) {
8671
- var target = wysihtml5.dom.getParentElement(event.target, { nodeName: ["TD", "TH"] });
9011
+ var target = wysihtml5.dom.getParentElement(event.target, { query: "td, th" });
8672
9012
  if (target) {
8673
9013
  handleSelectionMousedown(target);
8674
9014
  }
@@ -8681,7 +9021,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
8681
9021
  select.start = target;
8682
9022
  select.end = target;
8683
9023
  select.cells = [target];
8684
- select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] });
9024
+ select.table = dom.getParentElement(select.start, { query: "table" });
8685
9025
 
8686
9026
  if (select.table) {
8687
9027
  removeCellSelections();
@@ -8712,11 +9052,11 @@ wysihtml5.quirks.ensureProperClearing = (function() {
8712
9052
 
8713
9053
  function handleMouseMove (event) {
8714
9054
  var curTable = null,
8715
- cell = dom.getParentElement(event.target, { nodeName: ["TD","TH"] }),
9055
+ cell = dom.getParentElement(event.target, { nodeName: "td, th" }),
8716
9056
  oldEnd;
8717
9057
 
8718
9058
  if (cell && select.table && select.start) {
8719
- curTable = dom.getParentElement(cell, { nodeName: ["TABLE"] });
9059
+ curTable = dom.getParentElement(cell, { query: "table" });
8720
9060
  if (curTable && curTable === select.table) {
8721
9061
  removeCellSelections();
8722
9062
  oldEnd = select.end;
@@ -8745,7 +9085,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
8745
9085
  function bindSideclick () {
8746
9086
  var sideClickHandler = dom.observe(editable.ownerDocument, "click", function(event) {
8747
9087
  sideClickHandler.stop();
8748
- if (dom.getParentElement(event.target, { nodeName: ["TABLE"] }) != select.table) {
9088
+ if (dom.getParentElement(event.target, { query: "table" }) != select.table) {
8749
9089
  removeCellSelections();
8750
9090
  select.table = null;
8751
9091
  select.start = null;
@@ -8758,7 +9098,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
8758
9098
  function selectCells (start, end) {
8759
9099
  select.start = start;
8760
9100
  select.end = end;
8761
- select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] });
9101
+ select.table = dom.getParentElement(select.start, { query: "table" });
8762
9102
  selectedCells = dom.table.getCellsBetween(select.start, select.end);
8763
9103
  addSelections(selectedCells);
8764
9104
  bindSideclick();
@@ -8960,7 +9300,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
8960
9300
 
8961
9301
  // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail.
8962
9302
  // Webkit has an issue with placing caret into places where there are no textnodes near by.
8963
- creteTemporaryCaretSpaceAfter: function (node) {
9303
+ createTemporaryCaretSpaceAfter: function (node) {
8964
9304
  var caretPlaceholder = this.doc.createElement('span'),
8965
9305
  caretPlaceholderText = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE),
8966
9306
  placeholderRemover = (function(event) {
@@ -9030,7 +9370,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9030
9370
  * @example
9031
9371
  * selection.setBefore(myElement);
9032
9372
  */
9033
- setAfter: function(node) {
9373
+ setAfter: function(node, notVisual) {
9034
9374
  var range = rangy.createRange(this.doc),
9035
9375
  originalScrollTop = this.doc.documentElement.scrollTop || this.doc.body.scrollTop || this.doc.defaultView.pageYOffset,
9036
9376
  originalScrollLeft = this.doc.documentElement.scrollLeft || this.doc.body.scrollLeft || this.doc.defaultView.pageXOffset,
@@ -9045,7 +9385,20 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9045
9385
  // Webkit fails to add selection if there are no textnodes in that region
9046
9386
  // (like an uneditable container at the end of content).
9047
9387
  if (!sel) {
9048
- this.creteTemporaryCaretSpaceAfter(node);
9388
+ if (notVisual) {
9389
+ // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation
9390
+ // and remove itself in call stack end instead on user interaction
9391
+ var caretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
9392
+ node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
9393
+ this.selectNode(caretPlaceholder);
9394
+ setTimeout(function() {
9395
+ if (caretPlaceholder && caretPlaceholder.parentNode) {
9396
+ caretPlaceholder.parentNode.removeChild(caretPlaceholder);
9397
+ }
9398
+ }, 0);
9399
+ } else {
9400
+ this.createTemporaryCaretSpaceAfter(node);
9401
+ }
9049
9402
  }
9050
9403
  return sel;
9051
9404
  },
@@ -9070,7 +9423,6 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9070
9423
  // Make sure that caret is visible in node by inserting a zero width no breaking space
9071
9424
  try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
9072
9425
  }
9073
-
9074
9426
  if (canHaveHTML) {
9075
9427
  range.selectNodeContents(node);
9076
9428
  } else {
@@ -9144,6 +9496,19 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9144
9496
  return nodes;
9145
9497
  },
9146
9498
 
9499
+ filterElements: function(filter) {
9500
+ var ranges = this.getOwnRanges(),
9501
+ nodes = [], curNodes;
9502
+
9503
+ for (var i = 0, maxi = ranges.length; i < maxi; i++) {
9504
+ curNodes = ranges[i].getNodes([1], function(element){
9505
+ return filter(element, ranges[i]);
9506
+ });
9507
+ nodes = nodes.concat(curNodes);
9508
+ }
9509
+ return nodes;
9510
+ },
9511
+
9147
9512
  containsUneditable: function() {
9148
9513
  var uneditables = this.getOwnUneditables(),
9149
9514
  selection = this.getSelection();
@@ -9164,10 +9529,10 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9164
9529
  startParent, endParent, uneditables, ev;
9165
9530
 
9166
9531
  if (this.unselectableClass) {
9167
- if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { className: this.unselectableClass }, false, this.contain))) {
9532
+ if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
9168
9533
  range.setStartBefore(startParent);
9169
9534
  }
9170
- if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { className: this.unselectableClass }, false, this.contain))) {
9535
+ if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
9171
9536
  range.setEndAfter(endParent);
9172
9537
  }
9173
9538
 
@@ -9237,7 +9602,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9237
9602
  curEl, parents = [];
9238
9603
 
9239
9604
  for (var i = 0, maxi = nodes.length; i < maxi; i++) {
9240
- curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { nodeName: ['LI']}, false, this.contain);
9605
+ curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain);
9241
9606
  if (curEl) {
9242
9607
  parents.push(curEl);
9243
9608
  }
@@ -9289,7 +9654,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9289
9654
  node = selection.anchorNode,
9290
9655
  offset = selection.anchorOffset;
9291
9656
  if (ofNode && node) {
9292
- return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { nodeName: ofNode }, 1)));
9657
+ return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { query: ofNode }, 1)));
9293
9658
  } else if (node) {
9294
9659
  return (offset === 0 && !this.getPreviousNode(node, true));
9295
9660
  }
@@ -9480,6 +9845,33 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9480
9845
  }
9481
9846
  },
9482
9847
 
9848
+ splitElementAtCaret: function (element, insertNode) {
9849
+ var sel = this.getSelection(),
9850
+ range, contentAfterRangeStart,
9851
+ firstChild, lastChild;
9852
+
9853
+ if (sel.rangeCount > 0) {
9854
+ range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with
9855
+
9856
+ range.setEndAfter(element); // Place the end of the range after the element
9857
+ contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment
9858
+
9859
+ element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling);
9860
+
9861
+ firstChild = insertNode.firstChild;
9862
+ lastChild = insertNode.lastChild;
9863
+
9864
+ element.parentNode.insertBefore(insertNode, element.nextSibling);
9865
+
9866
+ // Select inserted node contents
9867
+ if (firstChild && lastChild) {
9868
+ range.setStartBefore(firstChild);
9869
+ range.setEndAfter(lastChild);
9870
+ this.setSelection(range);
9871
+ }
9872
+ }
9873
+ },
9874
+
9483
9875
  /**
9484
9876
  * Wraps current selection with the given node
9485
9877
  *
@@ -9523,7 +9915,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9523
9915
 
9524
9916
  tempElement.className = nodeOptions.className;
9525
9917
 
9526
- this.composer.commands.exec("formatBlock", nodeOptions.nodeName, nodeOptions.className);
9918
+ this.composer.commands.exec("formatBlock", nodeOptions);
9527
9919
  tempDivElements = this.contain.querySelectorAll("." + nodeOptions.className);
9528
9920
  if (tempDivElements[0]) {
9529
9921
  tempDivElements[0].parentNode.insertBefore(tempElement, tempDivElements[0]);
@@ -9674,7 +10066,7 @@ wysihtml5.quirks.ensureProperClearing = (function() {
9674
10066
  getNodes: function(nodeType, filter) {
9675
10067
  var range = this.getRange();
9676
10068
  if (range) {
9677
- return range.getNodes([nodeType], filter);
10069
+ return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter);
9678
10070
  } else {
9679
10071
  return [];
9680
10072
  }
@@ -10706,19 +11098,31 @@ wysihtml5.Commands = Base.extend(
10706
11098
  exec: function(composer, command, value) {
10707
11099
  var anchors = this.state(composer, command);
10708
11100
  if (anchors) {
11101
+ // remove <a> tag if there's no attributes provided.
11102
+ if ((!value || !value.href) && anchors.length !== null && anchors.length !== undefined && anchors.length > 0)
11103
+ {
11104
+ for(var i=0; i < anchors.length; i++)
11105
+ {
11106
+ wysihtml5.dom.unwrap(anchors[i]);
11107
+ }
11108
+ return;
11109
+ }
11110
+
10709
11111
  // Selection contains links then change attributes of these links
10710
11112
  composer.selection.executeAndRestore(function() {
10711
11113
  _changeLinks(composer, anchors, value);
10712
11114
  });
10713
11115
  } else {
10714
11116
  // Create links
10715
- value = typeof(value) === "object" ? value : { href: value };
10716
- _format(composer, value);
11117
+ if (value && value.href) {
11118
+ value = typeof(value) === "object" ? value : { href: value };
11119
+ _format(composer, value);
11120
+ }
10717
11121
  }
10718
11122
  },
10719
11123
 
10720
11124
  state: function(composer, command) {
10721
- return wysihtml5.commands.formatInline.state(composer, command, "A");
11125
+ return wysihtml5.commands.formatInline.state(composer, command, "a");
10722
11126
  }
10723
11127
  };
10724
11128
  })(wysihtml5);
@@ -10733,7 +11137,7 @@ wysihtml5.Commands = Base.extend(
10733
11137
  textContent;
10734
11138
  for (; i<length; i++) {
10735
11139
  anchor = anchors[i];
10736
- codeElement = dom.getParentElement(anchor, { nodeName: "code" });
11140
+ codeElement = dom.getParentElement(anchor, { query: "code" });
10737
11141
  textContent = dom.getTextContent(anchor);
10738
11142
 
10739
11143
  // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
@@ -10931,224 +11335,368 @@ wysihtml5.Commands = Base.extend(
10931
11335
 
10932
11336
  };
10933
11337
  })(wysihtml5);
10934
- ;(function(wysihtml5) {
10935
- var dom = wysihtml5.dom,
10936
- // Following elements are grouped
10937
- // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
11338
+ ;/* Formatblock
11339
+ * Is used to insert block level elements
11340
+ * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...)
11341
+ *
11342
+ */
11343
+ (function(wysihtml5) {
11344
+
11345
+ var dom = wysihtml5.dom,
11346
+ // When the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
10938
11347
  // instead of creating a H4 within a H1 which would result in semantically invalid html
10939
- BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "PRE", "DIV"];
11348
+ UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre";
11349
+ BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote";
10940
11350
 
10941
- /**
10942
- * Remove similiar classes (based on classRegExp)
10943
- * and add the desired class name
10944
- */
10945
- function _addClass(element, className, classRegExp) {
10946
- if (element.className) {
10947
- _removeClass(element, classRegExp);
10948
- element.className = wysihtml5.lang.string(element.className + " " + className).trim();
10949
- } else {
10950
- element.className = className;
11351
+ // Removes empty block level elements
11352
+ function cleanup(composer) {
11353
+ var container = composer.element,
11354
+ allElements = container.querySelectorAll(BLOCK_ELEMENTS),
11355
+ uneditables = container.querySelectorAll(composer.config.uneditableContainerClassname),
11356
+ elements = wysihtml5.lang.array(allElements).without(uneditables);
11357
+
11358
+ for (var i = elements.length; i--;) {
11359
+ if (elements[i].innerHTML === "") {
11360
+ elements[i].parentNode.removeChild(elements[i]);
11361
+ }
10951
11362
  }
10952
11363
  }
10953
11364
 
10954
- function _addStyle(element, cssStyle, styleRegExp) {
10955
- _removeStyle(element, styleRegExp);
10956
- if (element.getAttribute('style')) {
10957
- element.setAttribute('style', wysihtml5.lang.string(element.getAttribute('style') + " " + cssStyle).trim());
10958
- } else {
10959
- element.setAttribute('style', cssStyle);
11365
+ function defaultNodeName(composer) {
11366
+ return composer.config.useLineBreaks ? "DIV" : "P";
11367
+ }
11368
+
11369
+ // The outermost un-nestable block element parent of from node
11370
+ function findOuterBlock(node, container, allBlocks) {
11371
+ var n = node,
11372
+ block = null;
11373
+
11374
+ while (n && container && n !== container) {
11375
+ if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) {
11376
+ block = n;
11377
+ }
11378
+ n = n.parentNode;
10960
11379
  }
11380
+
11381
+ return block;
10961
11382
  }
10962
11383
 
10963
- function _removeClass(element, classRegExp) {
10964
- var ret = classRegExp.test(element.className);
10965
- element.className = element.className.replace(classRegExp, "");
10966
- if (wysihtml5.lang.string(element.className).trim() == '') {
10967
- element.removeAttribute('class');
11384
+ // Formats an element according to options nodeName, className, styleProperty, styleValue
11385
+ // If element is not defined, creates new element
11386
+ // if opotions is null, remove format instead
11387
+ function applyOptionsToElement(element, options, composer) {
11388
+
11389
+ if (!element) {
11390
+ element = composer.doc.createElement(options.nodeName || defaultNodeName(composer));
11391
+ // Add invisible space as otherwise webkit cannot set selection or range to it correctly
11392
+ element.appendChild(composer.doc.createTextNode(wysihtml5.INVISIBLE_SPACE));
10968
11393
  }
10969
- return ret;
11394
+
11395
+ if (options.nodeName && element.nodeName !== options.nodeName) {
11396
+ element = dom.renameElement(element, options.nodeName);
11397
+ }
11398
+
11399
+ // Remove similar classes before applying className
11400
+ if (options.classRegExp) {
11401
+ element.className = element.className.replace(options.classRegExp, "");
11402
+ }
11403
+ if (options.className) {
11404
+ element.classList.add(options.className);
11405
+ }
11406
+
11407
+ if (options.styleProperty && typeof options.styleValue !== "undefined") {
11408
+ element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
11409
+ }
11410
+
11411
+ return element;
10970
11412
  }
10971
11413
 
10972
- function _removeStyle(element, styleRegExp) {
10973
- var ret = styleRegExp.test(element.getAttribute('style'));
10974
- element.setAttribute('style', (element.getAttribute('style') || "").replace(styleRegExp, ""));
10975
- if (wysihtml5.lang.string(element.getAttribute('style') || "").trim() == '') {
11414
+ // Unsets element properties by options
11415
+ // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
11416
+ function removeOptionsFromElement(element, options, composer) {
11417
+ var style, classes;
11418
+
11419
+ if (options.styleProperty) {
11420
+ element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = '';
11421
+ }
11422
+ if (options.className) {
11423
+ element.classList.remove(options.className);
11424
+ }
11425
+
11426
+ if (options.classRegExp) {
11427
+ element.className = element.className.replace(options.classRegExp, "");
11428
+ }
11429
+
11430
+ // Clean up blank class attribute
11431
+ if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") {
11432
+ element.removeAttribute('class');
11433
+ }
11434
+
11435
+ if (options.nodeName && element.nodeName === options.nodeName) {
11436
+ style = element.getAttribute('style');
11437
+ if (!style || style.trim() === '') {
11438
+ dom.unwrap(element);
11439
+ } else {
11440
+ element = dom.renameElement(element, defaultNodeName(composer));
11441
+ }
11442
+ }
11443
+
11444
+ // Clean up blank style attribute
11445
+ if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
10976
11446
  element.removeAttribute('style');
10977
11447
  }
10978
- return ret;
10979
11448
  }
10980
11449
 
10981
- function _removeLastChildIfLineBreak(node) {
10982
- var lastChild = node.lastChild;
10983
- if (lastChild && _isLineBreak(lastChild)) {
10984
- lastChild.parentNode.removeChild(lastChild);
11450
+ // Unwraps block level elements from inside content
11451
+ // Useful as not all block level elements can contain other block-levels
11452
+ function unwrapBlocksFromContent(element) {
11453
+ var contentBlocks = element.querySelectorAll(BLOCK_ELEMENTS) || []; // Find unnestable block elements in extracted contents
11454
+
11455
+ for (var i = contentBlocks.length; i--;) {
11456
+ if (!contentBlocks[i].nextSibling || contentBlocks[i].nextSibling.nodeType !== 1 || contentBlocks[i].nextSibling.nodeName !== 'BR') {
11457
+ if ((contentBlocks[i].innerHTML || contentBlocks[i].nodeValue).trim() !== "") {
11458
+ contentBlocks[i].parentNode.insertBefore(contentBlocks[i].ownerDocument.createElement('BR'), contentBlocks[i].nextSibling);
11459
+ }
11460
+ }
11461
+ wysihtml5.dom.unwrap(contentBlocks[i]);
10985
11462
  }
10986
11463
  }
10987
11464
 
10988
- function _isLineBreak(node) {
10989
- return node.nodeName === "BR";
10990
- }
11465
+ // Fix ranges that visually cover whole block element to actually cover the block
11466
+ function fixRangeCoverage(range, composer) {
11467
+ var node;
10991
11468
 
10992
- /**
10993
- * Execute native query command
10994
- * and if necessary modify the inserted node's className
10995
- */
10996
- function _execCommand(doc, composer, command, nodeName, className) {
10997
- var ranges = composer.selection.getOwnRanges();
10998
- for (var i = ranges.length; i--;){
10999
- composer.selection.getSelection().removeAllRanges();
11000
- composer.selection.setSelection(ranges[i]);
11001
- if (className) {
11002
- var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
11003
- var target = event.target,
11004
- displayStyle;
11005
- if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
11006
- return;
11007
- }
11008
- displayStyle = dom.getStyle("display").from(target);
11009
- if (displayStyle.substr(0, 6) !== "inline") {
11010
- // Make sure that only block elements receive the given class
11011
- target.className += " " + className;
11012
- }
11013
- });
11469
+ if (range.startContainer && range.startContainer.nodeType === 1 && range.startContainer === range.endContainer) {
11470
+ if (range.startContainer.firstChild === range.startContainer.lastChild && range.endOffset === 1) {
11471
+ if (range.startContainer !== composer.element) {
11472
+ range.setStartBefore(range.startContainer);
11473
+ range.setEndAfter(range.endContainer);
11474
+ }
11014
11475
  }
11015
- doc.execCommand(command, false, nodeName);
11476
+ return;
11477
+ }
11016
11478
 
11017
- if (eventListener) {
11018
- eventListener.stop();
11479
+ if (range.startContainer && range.startContainer.nodeType === 1 && range.endContainer.nodeType === 3) {
11480
+ if (range.startContainer.firstChild === range.endContainer && range.endOffset === 1) {
11481
+ if (range.startContainer !== composer.element) {
11482
+ range.setEndAfter(range.startContainer);
11483
+ }
11019
11484
  }
11485
+ return;
11020
11486
  }
11021
- }
11022
11487
 
11023
- function _selectionWrap(composer, options) {
11024
- if (composer.selection.isCollapsed()) {
11025
- composer.selection.selectLine();
11488
+ if (range.endContainer && range.endContainer.nodeType === 1 && range.startContainer.nodeType === 3) {
11489
+ if (range.endContainer.firstChild === range.startContainer && range.endOffset === 1) {
11490
+ if (range.endContainer !== composer.element) {
11491
+ range.setStartBefore(range.endContainer);
11492
+ }
11493
+ }
11494
+ return;
11026
11495
  }
11027
11496
 
11028
- var surroundedNodes = composer.selection.surround(options);
11029
- for (var i = 0, imax = surroundedNodes.length; i < imax; i++) {
11030
- wysihtml5.dom.lineBreaks(surroundedNodes[i]).remove();
11031
- _removeLastChildIfLineBreak(surroundedNodes[i]);
11032
- }
11033
11497
 
11034
- // rethink restoring selection
11035
- // composer.selection.selectNode(element, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly());
11498
+ if (range.startContainer && range.startContainer.nodeType === 3 && range.startContainer === range.endContainer && range.startContainer.parentNode) {
11499
+ if (range.startContainer.parentNode.firstChild === range.startContainer && range.endOffset == range.endContainer.length && range.startOffset === 0) {
11500
+ node = range.startContainer.parentNode;
11501
+ if (node !== composer.element) {
11502
+ range.setStartBefore(node);
11503
+ range.setEndAfter(node);
11504
+ }
11505
+ }
11506
+ return;
11507
+ }
11036
11508
  }
11037
11509
 
11038
- function _hasClasses(element) {
11039
- return !!wysihtml5.lang.string(element.className).trim();
11040
- }
11510
+ // Wrap the range with a block level element
11511
+ // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
11512
+ function wrapRangeWithElement(range, options, defaultName, composer) {
11513
+ var defaultOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null;
11514
+ if (defaultOptions) {
11515
+ defaultOptions.nodeName = defaultOptions.nodeName || defaultName || defaultNodeName(composer);
11516
+ }
11517
+ fixRangeCoverage(range, composer);
11518
+
11519
+ var r = range.cloneRange(),
11520
+ rangeStartContainer = r.startContainer,
11521
+ content = r.extractContents(),
11522
+ fragment = composer.doc.createDocumentFragment(),
11523
+ splitAllBlocks = !defaultOptions || (defaultName === "BLOCKQUOTE" && defaultOptions.nodeName && defaultOptions.nodeName === "BLOCKQUOTE"),
11524
+ firstOuterBlock = findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
11525
+ wrapper, blocks, children;
11526
+
11527
+ if (options && options.nodeName && options.nodeName === "BLOCKQUOTE") {
11528
+ var tmpEl = applyOptionsToElement(null, options, composer);
11529
+ tmpEl.appendChild(content);
11530
+ fragment.appendChild(tmpEl);
11531
+ blocks = [tmpEl];
11532
+ } else {
11041
11533
 
11042
- function _hasStyles(element) {
11043
- return !!wysihtml5.lang.string(element.getAttribute('style') || '').trim();
11044
- }
11534
+ if (!content.firstChild) {
11535
+ fragment.appendChild(applyOptionsToElement(null, options, composer));
11536
+ } else {
11045
11537
 
11046
- wysihtml5.commands.formatBlock = {
11047
- exec: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) {
11048
- var doc = composer.doc,
11049
- blockElements = this.state(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp),
11050
- useLineBreaks = composer.config.useLineBreaks,
11051
- defaultNodeName = useLineBreaks ? "DIV" : "P",
11052
- selectedNodes, classRemoveAction, blockRenameFound, styleRemoveAction, blockElement;
11053
- nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
11054
-
11055
- if (blockElements.length) {
11056
- composer.selection.executeAndRestoreRangy(function() {
11057
- for (var b = blockElements.length; b--;) {
11058
- if (classRegExp) {
11059
- classRemoveAction = _removeClass(blockElements[b], classRegExp);
11060
- }
11061
- if (styleRegExp) {
11062
- styleRemoveAction = _removeStyle(blockElements[b], styleRegExp);
11063
- }
11538
+ while(content.firstChild) {
11539
+
11540
+ if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
11541
+
11542
+ if (options) {
11543
+ // Escape(split) block formatting at caret
11544
+ applyOptionsToElement(content.firstChild, options, composer);
11545
+ if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
11546
+ unwrapBlocksFromContent(content.firstChild);
11547
+ }
11548
+ fragment.appendChild(content.firstChild);
11549
+
11550
+ } else {
11551
+ // Split block formating and add new block to wrap caret
11552
+ unwrapBlocksFromContent(content.firstChild);
11553
+ children = wysihtml5.dom.unwrap(content.firstChild);
11554
+ for (var c = 0, cmax = children.length; c < cmax; c++) {
11555
+ fragment.appendChild(children[c]);
11556
+ }
11064
11557
 
11065
- if ((styleRemoveAction || classRemoveAction) && nodeName === null && blockElements[b].nodeName != defaultNodeName) {
11066
- // dont rename or remove element when just setting block formating class or style
11067
- return;
11558
+ if (fragment.childNodes.length > 0) {
11559
+ fragment.appendChild(composer.doc.createElement('BR'));
11560
+ }
11068
11561
  }
11562
+ } else {
11069
11563
 
11070
- var hasClasses = _hasClasses(blockElements[b]),
11071
- hasStyles = _hasStyles(blockElements[b]);
11072
-
11073
- if (!hasClasses && !hasStyles && (useLineBreaks || nodeName === "P")) {
11074
- // Insert a line break afterwards and beforewards when there are siblings
11075
- // that are not of type line break or block element
11076
- wysihtml5.dom.lineBreaks(blockElements[b]).add();
11077
- dom.replaceWithChildNodes(blockElements[b]);
11564
+ if (options) {
11565
+ // Wrap subsequent non-block nodes inside new block element
11566
+ wrapper = applyOptionsToElement(null, defaultOptions, composer);
11567
+ while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
11568
+ if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
11569
+ unwrapBlocksFromContent(content.firstChild);
11570
+ }
11571
+ wrapper.appendChild(content.firstChild);
11572
+ }
11573
+ fragment.appendChild(wrapper);
11574
+
11078
11575
  } else {
11079
- // Make sure that styling is kept by renaming the element to a <div> or <p> and copying over the class name
11080
- dom.renameElement(blockElements[b], nodeName === "P" ? "DIV" : defaultNodeName);
11576
+ // Escape(split) block formatting at selection
11577
+ if (content.firstChild.nodeType == 1) {
11578
+ unwrapBlocksFromContent(content.firstChild);
11579
+ }
11580
+ fragment.appendChild(content.firstChild);
11081
11581
  }
11582
+
11082
11583
  }
11083
- });
11584
+ }
11585
+ }
11084
11586
 
11085
- return;
11587
+ blocks = wysihtml5.lang.array(fragment.childNodes).get();
11588
+ }
11589
+
11590
+ if (firstOuterBlock) {
11591
+ // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
11592
+ composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
11593
+ } else {
11594
+ // Otherwise just insert
11595
+ r.insertNode(fragment);
11596
+ }
11597
+
11598
+ return blocks;
11599
+ }
11600
+
11601
+ // Find closest block level element
11602
+ function getParentBlockNodeName(element, composer) {
11603
+ var parentNode = wysihtml5.dom.getParentElement(element, {
11604
+ query: BLOCK_ELEMENTS
11605
+ }, null, composer.element);
11606
+
11607
+ return (parentNode) ? parentNode.nodeName : null;
11608
+ }
11609
+
11610
+ wysihtml5.commands.formatBlock = {
11611
+ exec: function(composer, command, options) {
11612
+ var newBlockElements = [],
11613
+ placeholder, ranges, range, parent, bookmark, state;
11614
+
11615
+ // If properties is passed as a string, look for tag with that tagName/query
11616
+ if (typeof options === "string") {
11617
+ options = {
11618
+ nodeName: options.toUpperCase()
11619
+ };
11086
11620
  }
11087
11621
 
11088
- // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>)
11089
- if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
11090
- selectedNodes = composer.selection.findNodesInSelection(BLOCK_ELEMENTS_GROUP).concat(composer.selection.getSelectedOwnNodes());
11091
- composer.selection.executeAndRestoreRangy(function() {
11092
- for (var n = selectedNodes.length; n--;) {
11093
- blockElement = dom.getParentElement(selectedNodes[n], {
11094
- nodeName: BLOCK_ELEMENTS_GROUP
11095
- });
11096
- if (blockElement == composer.element) {
11097
- blockElement = null;
11098
- }
11099
- if (blockElement) {
11100
- // Rename current block element to new block element and add class
11101
- if (nodeName) {
11102
- blockElement = dom.renameElement(blockElement, nodeName);
11103
- }
11104
- if (className) {
11105
- _addClass(blockElement, className, classRegExp);
11106
- }
11107
- if (cssStyle) {
11108
- _addStyle(blockElement, cssStyle, styleRegExp);
11109
- }
11110
- blockRenameFound = true;
11111
- }
11622
+ // Remove state if toggle set and state on and selection is collapsed
11623
+ if (options && options.toggle) {
11624
+ state = this.state(composer, command, options);
11625
+ if (state) {
11626
+ bookmark = rangy.saveSelection(composer.doc.defaultView || composer.doc.parentWindow);
11627
+ for (var j in state) {
11628
+ removeOptionsFromElement(state[j], options, composer);
11112
11629
  }
11630
+ }
11631
+ }
11113
11632
 
11114
- });
11633
+ // Otherwise expand selection so it will cover closest block if option caretSelectsBlock is true and selection is collapsed
11634
+ if (!state) {
11115
11635
 
11116
- if (blockRenameFound) {
11117
- return;
11636
+ if (composer.selection.isCollapsed()) {
11637
+ parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
11638
+ query: BLOCK_ELEMENTS
11639
+ }, null, composer.element);
11640
+ if (parent) {
11641
+ bookmark = rangy.saveSelection(composer.doc.defaultView || composer.doc.parentWindow);
11642
+ range = composer.selection.createRange();
11643
+ range.selectNode(parent);
11644
+ composer.selection.setSelection(range);
11645
+ } else if (!composer.isEmpty()) {
11646
+ bookmark = rangy.saveSelection(composer.doc.defaultView || composer.doc.parentWindow);
11647
+ composer.selection.selectLine();
11648
+ }
11118
11649
  }
11650
+
11651
+ // And get all selection ranges of current composer and iterat
11652
+ ranges = composer.selection.getOwnRanges();
11653
+ for (var i = ranges.length; i--;) {
11654
+ newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, getParentBlockNodeName(ranges[i].startContainer, composer), composer));
11655
+ }
11656
+
11119
11657
  }
11120
11658
 
11121
- _selectionWrap(composer, {
11122
- "nodeName": (nodeName || defaultNodeName),
11123
- "className": className || null,
11124
- "cssStyle": cssStyle || null
11125
- });
11659
+ // Remove empty block elements that may be left behind
11660
+ cleanup(composer);
11661
+ // Restore correct selection
11662
+ if (bookmark) {
11663
+ rangy.restoreSelection(bookmark);
11664
+ } else {
11665
+ range = composer.selection.createRange();
11666
+ range.setStartBefore(newBlockElements[0]);
11667
+ range.setEndAfter(newBlockElements[newBlockElements.length - 1]);
11668
+ composer.selection.setSelection(range);
11669
+ }
11670
+
11671
+ wysihtml5.dom.removeInvisibleSpaces(composer.element);
11672
+
11126
11673
  },
11127
11674
 
11128
- state: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) {
11129
- var nodes = composer.selection.getSelectedOwnNodes(),
11130
- parents = [],
11131
- parent;
11675
+ // If properties as null is passed returns status describing all block level elements
11676
+ state: function(composer, command, properties) {
11677
+
11678
+ // If properties is passed as a string, look for tag with that tagName/query
11679
+ if (typeof properties === "string") {
11680
+ properties = {
11681
+ query: properties
11682
+ };
11683
+ }
11132
11684
 
11133
- nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
11685
+ var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
11686
+ return wysihtml5.dom.domNode(element).test(properties || { query: BLOCK_ELEMENTS });
11687
+ }).bind(this)),
11688
+ parentNodes = composer.selection.getSelectedOwnNodes(),
11689
+ parent;
11134
11690
 
11135
- //var selectedNode = composer.selection.getSelectedNode();
11136
- for (var i = 0, maxi = nodes.length; i < maxi; i++) {
11137
- parent = dom.getParentElement(nodes[i], {
11138
- nodeName: nodeName,
11139
- className: className,
11140
- classRegExp: classRegExp,
11141
- cssStyle: cssStyle,
11142
- styleRegExp: styleRegExp
11143
- });
11144
- if (parent && wysihtml5.lang.array(parents).indexOf(parent) == -1) {
11145
- parents.push(parent);
11691
+ // Finds matching elements that are parents of selection and adds to nodes list
11692
+ for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
11693
+ parent = dom.getParentElement(parentNodes[i], properties || { query: BLOCK_ELEMENTS }, null, composer.element);
11694
+ if (parent && nodes.indexOf(parent) === -1) {
11695
+ nodes.push(parent);
11146
11696
  }
11147
11697
  }
11148
- if (parents.length == 0) {
11149
- return false;
11150
- }
11151
- return parents;
11698
+
11699
+ return (nodes.length === 0) ? false : nodes;
11152
11700
  }
11153
11701
 
11154
11702
 
@@ -11200,7 +11748,7 @@ wysihtml5.commands.formatCode = {
11200
11748
  selectedNode.firstChild && selectedNode.firstChild.nodeName && selectedNode.firstChild.nodeName == "CODE") {
11201
11749
  return selectedNode;
11202
11750
  } else {
11203
- return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "CODE" }) && wysihtml5.dom.getParentElement(selectedNode, { nodeName: "PRE" });
11751
+ return wysihtml5.dom.getParentElement(selectedNode, { query: "pre code" });
11204
11752
  }
11205
11753
  }
11206
11754
  };;/**
@@ -11355,42 +11903,23 @@ wysihtml5.commands.formatCode = {
11355
11903
  })(wysihtml5);
11356
11904
  ;(function(wysihtml5) {
11357
11905
 
11906
+ var nodeOptions = {
11907
+ nodeName: "BLOCKQUOTE",
11908
+ toggle: true
11909
+ };
11910
+
11358
11911
  wysihtml5.commands.insertBlockQuote = {
11359
11912
  exec: function(composer, command) {
11360
- var state = this.state(composer, command),
11361
- endToEndParent = composer.selection.isEndToEndInNode(['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P']),
11362
- prevNode, nextNode;
11363
-
11364
- composer.selection.executeAndRestore(function() {
11365
- if (state) {
11366
- if (composer.config.useLineBreaks) {
11367
- wysihtml5.dom.lineBreaks(state).add();
11368
- }
11369
- wysihtml5.dom.unwrap(state);
11370
- } else {
11371
- if (composer.selection.isCollapsed()) {
11372
- composer.selection.selectLine();
11373
- }
11374
-
11375
- if (endToEndParent) {
11376
- var qouteEl = endToEndParent.ownerDocument.createElement('blockquote');
11377
- wysihtml5.dom.insert(qouteEl).after(endToEndParent);
11378
- qouteEl.appendChild(endToEndParent);
11379
- } else {
11380
- composer.selection.surround({nodeName: "blockquote"});
11381
- }
11382
- }
11383
- });
11913
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11384
11914
  },
11385
- state: function(composer, command) {
11386
- var selectedNode = composer.selection.getSelectedNode(),
11387
- node = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "BLOCKQUOTE" }, false, composer.element);
11388
11915
 
11389
- return (node) ? node : false;
11916
+ state: function(composer, command) {
11917
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11390
11918
  }
11391
11919
  };
11392
11920
 
11393
- })(wysihtml5);;wysihtml5.commands.insertHTML = {
11921
+ })(wysihtml5);
11922
+ ;wysihtml5.commands.insertHTML = {
11394
11923
  exec: function(composer, command, html) {
11395
11924
  if (composer.commands.support(command)) {
11396
11925
  composer.doc.execCommand(command, false, html);
@@ -11425,8 +11954,8 @@ wysihtml5.commands.formatCode = {
11425
11954
  textNode,
11426
11955
  parent;
11427
11956
 
11428
- if (image) {
11429
- // Image already selected, set the caret before it and delete it
11957
+ // If image is selected and src ie empty, set the caret before it and delete the image
11958
+ if (image && !value.src) {
11430
11959
  composer.selection.setBefore(image);
11431
11960
  parent = image.parentNode;
11432
11961
  parent.removeChild(image);
@@ -11443,6 +11972,17 @@ wysihtml5.commands.formatCode = {
11443
11972
  return;
11444
11973
  }
11445
11974
 
11975
+ // If image selected change attributes accordingly
11976
+ if (image) {
11977
+ for (var key in value) {
11978
+ if (value.hasOwnProperty(key)) {
11979
+ image.setAttribute(key === "className" ? "class" : key, value[key]);
11980
+ }
11981
+ }
11982
+ return;
11983
+ }
11984
+
11985
+ // Otherwise lets create the image
11446
11986
  image = doc.createElement(NODE_NAME);
11447
11987
 
11448
11988
  for (var i in value) {
@@ -11562,7 +12102,7 @@ wysihtml5.commands.formatCode = {
11562
12102
  };
11563
12103
 
11564
12104
  if (node) {
11565
- var parentLi = wysihtml5.dom.getParentElement(node, { nodeName: "LI" }),
12105
+ var parentLi = wysihtml5.dom.getParentElement(node, { query: "li" }),
11566
12106
  otherNodeName = (nodeName === "UL") ? "OL" : "UL";
11567
12107
 
11568
12108
  if (isNode(node, nodeName)) {
@@ -11712,102 +12252,133 @@ wysihtml5.commands.formatCode = {
11712
12252
  }
11713
12253
  };
11714
12254
  ;(function(wysihtml5) {
11715
- var CLASS_NAME = "wysiwyg-text-align-center",
11716
- REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
12255
+
12256
+ var nodeOptions = {
12257
+ className: "wysiwyg-text-align-center",
12258
+ classRegExp: /wysiwyg-text-align-[0-9a-z]+/g,
12259
+ toggle: true
12260
+ };
11717
12261
 
11718
12262
  wysihtml5.commands.justifyCenter = {
11719
12263
  exec: function(composer, command) {
11720
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12264
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11721
12265
  },
11722
12266
 
11723
12267
  state: function(composer, command) {
11724
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12268
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11725
12269
  }
11726
12270
  };
12271
+
11727
12272
  })(wysihtml5);
11728
12273
  ;(function(wysihtml5) {
11729
- var CLASS_NAME = "wysiwyg-text-align-left",
11730
- REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
12274
+
12275
+ var nodeOptions = {
12276
+ className: "wysiwyg-text-align-left",
12277
+ classRegExp: /wysiwyg-text-align-[0-9a-z]+/g,
12278
+ toggle: true
12279
+ };
11731
12280
 
11732
12281
  wysihtml5.commands.justifyLeft = {
11733
12282
  exec: function(composer, command) {
11734
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12283
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11735
12284
  },
11736
12285
 
11737
12286
  state: function(composer, command) {
11738
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12287
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11739
12288
  }
11740
12289
  };
11741
12290
  })(wysihtml5);
11742
12291
  ;(function(wysihtml5) {
11743
- var CLASS_NAME = "wysiwyg-text-align-right",
11744
- REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
12292
+
12293
+ var nodeOptions = {
12294
+ className: "wysiwyg-text-align-right",
12295
+ classRegExp: /wysiwyg-text-align-[0-9a-z]+/g,
12296
+ toggle: true
12297
+ };
11745
12298
 
11746
12299
  wysihtml5.commands.justifyRight = {
11747
12300
  exec: function(composer, command) {
11748
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12301
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11749
12302
  },
11750
12303
 
11751
12304
  state: function(composer, command) {
11752
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12305
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11753
12306
  }
11754
12307
  };
11755
12308
  })(wysihtml5);
11756
12309
  ;(function(wysihtml5) {
11757
- var CLASS_NAME = "wysiwyg-text-align-justify",
11758
- REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
12310
+
12311
+ var nodeOptions = {
12312
+ className: "wysiwyg-text-align-justify",
12313
+ classRegExp: /wysiwyg-text-align-[0-9a-z]+/g,
12314
+ toggle: true
12315
+ };
11759
12316
 
11760
12317
  wysihtml5.commands.justifyFull = {
11761
12318
  exec: function(composer, command) {
11762
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12319
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11763
12320
  },
11764
12321
 
11765
12322
  state: function(composer, command) {
11766
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
12323
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11767
12324
  }
11768
12325
  };
11769
12326
  })(wysihtml5);
11770
12327
  ;(function(wysihtml5) {
11771
- var STYLE_STR = "text-align: right;",
11772
- REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
12328
+
12329
+ var nodeOptions = {
12330
+ styleProperty: "textAlign",
12331
+ styleValue: "right",
12332
+ toggle: true
12333
+ };
11773
12334
 
11774
12335
  wysihtml5.commands.alignRightStyle = {
11775
12336
  exec: function(composer, command) {
11776
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
12337
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11777
12338
  },
11778
12339
 
11779
12340
  state: function(composer, command) {
11780
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
12341
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11781
12342
  }
11782
12343
  };
11783
12344
  })(wysihtml5);
11784
12345
  ;(function(wysihtml5) {
11785
- var STYLE_STR = "text-align: left;",
11786
- REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
12346
+
12347
+ var nodeOptions = {
12348
+ styleProperty: "textAlign",
12349
+ styleValue: "left",
12350
+ toggle: true
12351
+ };
11787
12352
 
11788
12353
  wysihtml5.commands.alignLeftStyle = {
11789
12354
  exec: function(composer, command) {
11790
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
12355
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11791
12356
  },
11792
12357
 
11793
12358
  state: function(composer, command) {
11794
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
12359
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11795
12360
  }
11796
12361
  };
12362
+
11797
12363
  })(wysihtml5);
11798
12364
  ;(function(wysihtml5) {
11799
- var STYLE_STR = "text-align: center;",
11800
- REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
12365
+
12366
+ var nodeOptions = {
12367
+ styleProperty: "textAlign",
12368
+ styleValue: "center",
12369
+ toggle: true
12370
+ };
11801
12371
 
11802
12372
  wysihtml5.commands.alignCenterStyle = {
11803
12373
  exec: function(composer, command) {
11804
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
12374
+ return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions);
11805
12375
  },
11806
12376
 
11807
12377
  state: function(composer, command) {
11808
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
12378
+ return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions);
11809
12379
  }
11810
12380
  };
12381
+
11811
12382
  })(wysihtml5);
11812
12383
  ;wysihtml5.commands.redo = {
11813
12384
  exec: function(composer) {
@@ -12025,8 +12596,8 @@ wysihtml5.commands.formatCode = {
12025
12596
  if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
12026
12597
  found = true;
12027
12598
 
12028
- outerListNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['OL', 'UL']}, false, composer.element);
12029
- outerLiNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['LI']}, false, composer.element);
12599
+ outerListNode = wysihtml5.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element);
12600
+ outerLiNode = wysihtml5.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element);
12030
12601
 
12031
12602
  if (outerListNode && outerLiNode) {
12032
12603
 
@@ -12648,7 +13219,7 @@ wysihtml5.views.View = Base.extend(
12648
13219
  }
12649
13220
 
12650
13221
  var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
12651
- link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
13222
+ link = dom.getParentElement(selectedNode, { query: "a" }, 4),
12652
13223
  textContent;
12653
13224
 
12654
13225
  if (!link) {
@@ -12713,11 +13284,11 @@ wysihtml5.views.View = Base.extend(
12713
13284
 
12714
13285
  _initLineBreaking: function() {
12715
13286
  var that = this,
12716
- USE_NATIVE_LINE_BREAK_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
12717
- LIST_TAGS = ["UL", "OL", "MENU"];
13287
+ USE_NATIVE_LINE_BREAK_INSIDE_TAGS = "li, p, h1, h2, h3, h4, h5, h6",
13288
+ LIST_TAGS = "ul, ol, menu";
12718
13289
 
12719
13290
  function adjust(selectedNode) {
12720
- var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);
13291
+ var parentElement = dom.getParentElement(selectedNode, { query: "p, div" }, 2);
12721
13292
  if (parentElement && dom.contains(that.element, parentElement)) {
12722
13293
  that.selection.executeAndRestore(function() {
12723
13294
  if (that.config.useLineBreaks) {
@@ -12766,7 +13337,7 @@ wysihtml5.views.View = Base.extend(
12766
13337
  if (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY) {
12767
13338
  return;
12768
13339
  }
12769
- var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { nodeName: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4);
13340
+ var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { query: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4);
12770
13341
  if (blockElement) {
12771
13342
  setTimeout(function() {
12772
13343
  // Unwrap paragraph after leaving a list or a H1-6
@@ -12778,7 +13349,7 @@ wysihtml5.views.View = Base.extend(
12778
13349
  return;
12779
13350
  }
12780
13351
 
12781
- list = dom.getParentElement(selectedNode, { nodeName: LIST_TAGS }, 2);
13352
+ list = dom.getParentElement(selectedNode, { query: LIST_TAGS }, 2);
12782
13353
 
12783
13354
  if (!list) {
12784
13355
  adjust(selectedNode);
@@ -13038,46 +13609,12 @@ wysihtml5.views.View = Base.extend(
13038
13609
  }
13039
13610
  };
13040
13611
 
13041
- var deleteAroundEditable = function(selection, uneditable, element) {
13042
- // merge node with previous node from uneditable
13043
- var prevNode = selection.getPreviousNode(uneditable, true),
13044
- curNode = selection.getSelectedNode();
13045
-
13046
- if (curNode.nodeType !== 1 && curNode.parentNode !== element) { curNode = curNode.parentNode; }
13047
- if (prevNode) {
13048
- if (curNode.nodeType == 1) {
13049
- var first = curNode.firstChild;
13050
-
13051
- if (prevNode.nodeType == 1) {
13052
- while (curNode.firstChild) {
13053
- prevNode.appendChild(curNode.firstChild);
13054
- }
13055
- } else {
13056
- while (curNode.firstChild) {
13057
- uneditable.parentNode.insertBefore(curNode.firstChild, uneditable);
13058
- }
13059
- }
13060
- if (curNode.parentNode) {
13061
- curNode.parentNode.removeChild(curNode);
13062
- }
13063
- selection.setBefore(first);
13064
- } else {
13065
- if (prevNode.nodeType == 1) {
13066
- prevNode.appendChild(curNode);
13067
- } else {
13068
- uneditable.parentNode.insertBefore(curNode, uneditable);
13069
- }
13070
- selection.setBefore(curNode);
13071
- }
13072
- }
13073
- };
13074
-
13075
13612
  var handleDeleteKeyPress = function(event, composer) {
13076
13613
  var selection = composer.selection,
13077
13614
  element = composer.element;
13078
13615
 
13079
13616
  if (selection.isCollapsed()) {
13080
- if (selection.caretIsInTheBeginnig('LI')) {
13617
+ if (selection.caretIsInTheBeginnig('li')) {
13081
13618
  event.preventDefault();
13082
13619
  composer.commands.exec('outdentList');
13083
13620
  } else if (selection.caretIsInTheBeginnig()) {
@@ -13126,7 +13663,7 @@ wysihtml5.views.View = Base.extend(
13126
13663
  var handleTabKeyDown = function(composer, element) {
13127
13664
  if (!composer.selection.isCollapsed()) {
13128
13665
  composer.selection.deleteContents();
13129
- } else if (composer.selection.caretIsInTheBeginnig('LI')) {
13666
+ } else if (composer.selection.caretIsInTheBeginnig('li')) {
13130
13667
  if (composer.commands.exec('indentList')) return;
13131
13668
  }
13132
13669
 
@@ -13238,7 +13775,7 @@ wysihtml5.views.View = Base.extend(
13238
13775
  if (this.config.uneditableContainerClassname) {
13239
13776
  // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text)
13240
13777
  // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour
13241
- var uneditable = wysihtml5.dom.getParentElement(event.target, { className: this.config.uneditableContainerClassname }, false, this.element);
13778
+ var uneditable = wysihtml5.dom.getParentElement(event.target, { query: "." + this.config.uneditableContainerClassname }, false, this.element);
13242
13779
  if (uneditable) {
13243
13780
  this.selection.setAfter(uneditable);
13244
13781
  }
@@ -13851,11 +14388,7 @@ wysihtml5.views.View = Base.extend(
13851
14388
  var that = this,
13852
14389
  callbackWrapper = function(event) {
13853
14390
  var attributes = that._serialize();
13854
- if (attributes == that.elementToChange) {
13855
- that.fire("edit", attributes);
13856
- } else {
13857
- that.fire("save", attributes);
13858
- }
14391
+ that.fire("save", attributes);
13859
14392
  that.hide();
13860
14393
  event.preventDefault();
13861
14394
  event.stopPropagation();
@@ -13903,7 +14436,7 @@ wysihtml5.views.View = Base.extend(
13903
14436
  * then gets returned
13904
14437
  */
13905
14438
  _serialize: function() {
13906
- var data = this.elementToChange || {},
14439
+ var data = {},
13907
14440
  fields = this.container.querySelectorAll(SELECTOR_FIELDS),
13908
14441
  length = fields.length,
13909
14442
  i = 0;
@@ -14154,11 +14687,14 @@ wysihtml5.views.View = Base.extend(
14154
14687
  group,
14155
14688
  name,
14156
14689
  value,
14157
- dialog;
14690
+ dialog,
14691
+ tracksBlankValue;
14692
+
14158
14693
  for (; i<length; i++) {
14159
14694
  link = links[i];
14160
14695
  name = link.getAttribute("data-wysihtml5-" + type);
14161
14696
  value = link.getAttribute("data-wysihtml5-" + type + "-value");
14697
+ tracksBlankValue = link.getAttribute("data-wysihtml5-" + type + "-blank-value");
14162
14698
  group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']");
14163
14699
  dialog = this._getDialog(link, name);
14164
14700
 
@@ -14167,6 +14703,7 @@ wysihtml5.views.View = Base.extend(
14167
14703
  group: group,
14168
14704
  name: name,
14169
14705
  value: value,
14706
+ tracksBlankValue: tracksBlankValue,
14170
14707
  dialog: dialog,
14171
14708
  state: false
14172
14709
  };
@@ -14327,8 +14864,9 @@ wysihtml5.views.View = Base.extend(
14327
14864
 
14328
14865
  _updateLinkStates: function() {
14329
14866
 
14330
- var commandMapping = this.commandMapping,
14331
- actionMapping = this.actionMapping,
14867
+ var commandMapping = this.commandMapping,
14868
+ commandblankMapping = this.commandblankMapping,
14869
+ actionMapping = this.actionMapping,
14332
14870
  i,
14333
14871
  state,
14334
14872
  action,
@@ -14352,39 +14890,47 @@ wysihtml5.views.View = Base.extend(
14352
14890
  dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED);
14353
14891
  }
14354
14892
  }
14355
- if (command.state === state) {
14893
+ if (command.state === state && !command.tracksBlankValue) {
14356
14894
  continue;
14357
14895
  }
14358
14896
 
14359
14897
  command.state = state;
14360
14898
  if (state) {
14361
- dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14362
- if (command.group) {
14363
- dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
14364
- }
14365
- if (command.dialog) {
14366
- if (typeof(state) === "object" || wysihtml5.lang.object(state).isArray()) {
14367
-
14368
- if (!command.dialog.multiselect && wysihtml5.lang.object(state).isArray()) {
14369
- // Grab first and only object/element in state array, otherwise convert state into boolean
14370
- // to avoid showing a dialog for multiple selected elements which may have different attributes
14371
- // eg. when two links with different href are selected, the state will be an array consisting of both link elements
14372
- // but the dialog interface can only update one
14373
- state = state.length === 1 ? state[0] : true;
14374
- command.state = state;
14899
+ if (command.tracksBlankValue) {
14900
+ dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14901
+ } else {
14902
+ dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14903
+ if (command.group) {
14904
+ dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
14905
+ }
14906
+ if (command.dialog) {
14907
+ if (typeof(state) === "object" || wysihtml5.lang.object(state).isArray()) {
14908
+
14909
+ if (!command.dialog.multiselect && wysihtml5.lang.object(state).isArray()) {
14910
+ // Grab first and only object/element in state array, otherwise convert state into boolean
14911
+ // to avoid showing a dialog for multiple selected elements which may have different attributes
14912
+ // eg. when two links with different href are selected, the state will be an array consisting of both link elements
14913
+ // but the dialog interface can only update one
14914
+ state = state.length === 1 ? state[0] : true;
14915
+ command.state = state;
14916
+ }
14917
+ command.dialog.show(state);
14918
+ } else {
14919
+ command.dialog.hide();
14375
14920
  }
14376
- command.dialog.show(state);
14377
- } else {
14378
- command.dialog.hide();
14379
14921
  }
14380
14922
  }
14381
14923
  } else {
14382
- dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14383
- if (command.group) {
14384
- dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
14385
- }
14386
- if (command.dialog) {
14387
- command.dialog.hide();
14924
+ if (command.tracksBlankValue) {
14925
+ dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14926
+ } else {
14927
+ dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14928
+ if (command.group) {
14929
+ dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
14930
+ }
14931
+ if (command.dialog) {
14932
+ command.dialog.hide();
14933
+ }
14388
14934
  }
14389
14935
  }
14390
14936
  }