wysihtml5x-rails 0.4.17 → 0.5.0.beta1

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: 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
  }