From b9c338dfb09e2923b11990d3c6a98cb2a719fd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 12 Oct 2016 14:23:14 +0200 Subject: [PATCH] fix(jqLite): separate jqLite/compile/sce camelCasing logic jqLite needs camelCase for it's css method; it should only convert one dash followed by a lowercase letter to an uppercase one; it shouldn't touch underscores, colons or collapse multiple dashes into one. This is behavior of jQuery 3 as well. This commit separates jqLite camelCasing from the $compile one (and $sce but that's an internal-only use). The $compile version should behave as before. Also, jqLite's css camelCasing logic was put in a separate function and refactored: now the properties starting from an uppercase letter are used by default (i.e. Webkit, not webkit) and the only exception is for the -ms- prefix that is converted to ms, not Ms. This makes the logic clearer as we're just always changing a dash followed by a lowercase letter by an uppercase one; this is also how it works in jQuery. Ref #15126 Fix #7744 BREAKING CHANGE: before, when Angular was used without jQuery, the key passed to the css method was more heavily camelCased; now only a single (!) hyphen followed by a lowercase letter is getting transformed. This also affects APIs that rely on the css method, like ngStyle. If you use Angular with jQuery, it already behaved in this way so no changes are needed on your part. To migrate the code follow the example below: Before: HTML: // All five versions used to be equivalent.
JS: // All five versions used to be equivalent. elem.css('background_color', 'blue'); elem.css('background:color', 'blue'); elem.css('background-color', 'blue'); elem.css('background--color', 'blue'); elem.css('backgroundColor', 'blue'); // All five versions used to be equivalent. var bgColor = elem.css('background_color'); var bgColor = elem.css('background:color'); var bgColor = elem.css('background-color'); var bgColor = elem.css('background--color'); var bgColor = elem.css('backgroundColor'); After: HTML: // Previous five versions are no longer equivalent but these two still are.
JS: // Previous five versions are no longer equivalent but these two still are. elem.css('background-color', 'blue'); elem.css('backgroundColor', 'blue'); // Previous five versions are no longer equivalent but these two still are. var bgColor = elem.css('background-color'); var bgColor = elem.css('backgroundColor'); --- src/.eslintrc.json | 2 +- src/jqLite.js | 31 +++++++---- src/ng/compile.js | 6 +- src/ng/sce.js | 13 ++++- test/.eslintrc.json | 3 +- test/jqLiteSpec.js | 133 ++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 159 insertions(+), 29 deletions(-) diff --git a/src/.eslintrc.json b/src/.eslintrc.json index a5d959535803..6022ac25baa9 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -123,7 +123,7 @@ "BOOLEAN_ATTR": false, "ALIASED_ATTR": false, "jqNextId": false, - "camelCase": false, + "fnCamelCaseReplace": false, "jqLitePatchJQueryRemove": false, "JQLite": false, "jqLiteClone": false, diff --git a/src/jqLite.js b/src/jqLite.js index df8565e90ad2..4b5c58304fce 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -136,22 +136,31 @@ JQLite._data = function(node) { function jqNextId() { return ++jqId; } -var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; -var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var DASH_LOWERCASE_REGEXP = /-([a-z])/g; +var MS_HACK_REGEXP = /^-ms-/; var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. * @param name Name to normalize */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); +function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); +} + +function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); +} + +/** + * Converts kebab-case to camelCase. + * @param name Name to normalize + */ +function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; @@ -633,7 +642,7 @@ forEach({ hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; diff --git a/src/ng/compile.js b/src/ng/compile.js index 6c9cf801f8bc..bc1cae5bfca6 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -3590,12 +3590,16 @@ SimpleChange.prototype.isFirstChange = function() { return this.previousValue == var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; +var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace); } /** diff --git a/src/ng/sce.js b/src/ng/sce.js index 074c13d84579..0201656c6ea7 100644 --- a/src/ng/sce.js +++ b/src/ng/sce.js @@ -27,6 +27,13 @@ var SCE_CONTEXTS = { // Helper functions follow. +var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + +function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); +} + function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -1054,13 +1061,13 @@ function $SceProvider() { forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase('parse_as_' + lName)] = function(expr) { + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase('get_trusted_' + lName)] = function(value) { + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase('trust_as_' + lName)] = function(value) { + sce[snakeToCamel('trust_as_' + lName)] = function(value) { return trustAs(enumValue, value); }; }); diff --git a/test/.eslintrc.json b/test/.eslintrc.json index ca882335b492..026e7f2ed18f 100644 --- a/test/.eslintrc.json +++ b/test/.eslintrc.json @@ -118,7 +118,8 @@ /* jqLite.js */ "BOOLEAN_ATTR": false, "jqNextId": false, - "camelCase": false, + "kebabToCamel": false, + "fnCamelCaseReplace": false, "jqLitePatchJQueryRemove": false, "JQLite": false, "jqLiteClone": false, diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index f4a3fbfc16c5..0b54c233d43c 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -1055,6 +1055,105 @@ describe('jqLite', function() { expect(jqA.css('z-index')).toBeOneOf('7', 7); expect(jqA.css('zIndex')).toBeOneOf('7', 7); }); + + it('should leave non-dashed strings alone', function() { + var jqA = jqLite(a); + + jqA.css('foo', 'foo'); + jqA.css('fooBar', 'bar'); + + expect(a.style.foo).toBe('foo'); + expect(a.style.fooBar).toBe('bar'); + }); + + it('should convert dash-separated strings to camelCase', function() { + var jqA = jqLite(a); + + jqA.css('foo-bar', 'foo'); + jqA.css('foo-bar-baz', 'bar'); + jqA.css('foo:bar_baz', 'baz'); + + expect(a.style.fooBar).toBe('foo'); + expect(a.style.fooBarBaz).toBe('bar'); + expect(a.style['foo:bar_baz']).toBe('baz'); + }); + + it('should convert leading dashes followed by a lowercase letter', function() { + var jqA = jqLite(a); + + jqA.css('-foo-bar', 'foo'); + + expect(a.style.FooBar).toBe('foo'); + }); + + it('should not convert slashes followed by a non-letter', function() { + // jQuery 2.x had different behavior; skip the test. + if (isJQuery2x()) return; + + var jqA = jqLite(a); + + jqA.css('foo-42- -a-B', 'foo'); + + expect(a.style['foo-42- A-B']).toBe('foo'); + }); + + it('should convert the -ms- prefix to ms instead of Ms', function() { + var jqA = jqLite(a); + + jqA.css('-ms-foo-bar', 'foo'); + jqA.css('-moz-foo-bar', 'bar'); + jqA.css('-webkit-foo-bar', 'baz'); + + expect(a.style.msFooBar).toBe('foo'); + expect(a.style.MozFooBar).toBe('bar'); + expect(a.style.WebkitFooBar).toBe('baz'); + }); + + it('should not collapse sequences of dashes', function() { + var jqA = jqLite(a); + + jqA.css('foo---bar-baz--qaz', 'foo'); + + expect(a.style['foo--BarBaz-Qaz']).toBe('foo'); + }); + + + it('should read vendor prefixes with the special -ms- exception', function() { + // jQuery uses getComputedStyle() in a css getter so these tests would fail there. + if (!_jqLiteMode) return; + + var jqA = jqLite(a); + + a.style.WebkitFooBar = 'webkit-uppercase'; + a.style.webkitFooBar = 'webkit-lowercase'; + + a.style.MozFooBaz = 'moz-uppercase'; + a.style.mozFooBaz = 'moz-lowercase'; + + a.style.MsFooQaz = 'ms-uppercase'; + a.style.msFooQaz = 'ms-lowercase'; + + expect(jqA.css('-webkit-foo-bar')).toBe('webkit-uppercase'); + expect(jqA.css('-moz-foo-baz')).toBe('moz-uppercase'); + expect(jqA.css('-ms-foo-qaz')).toBe('ms-lowercase'); + }); + + it('should write vendor prefixes with the special -ms- exception', function() { + var jqA = jqLite(a); + + jqA.css('-webkit-foo-bar', 'webkit'); + jqA.css('-moz-foo-baz', 'moz'); + jqA.css('-ms-foo-qaz', 'ms'); + + expect(a.style.WebkitFooBar).toBe('webkit'); + expect(a.style.webkitFooBar).not.toBeDefined(); + + expect(a.style.MozFooBaz).toBe('moz'); + expect(a.style.mozFooBaz).not.toBeDefined(); + + expect(a.style.MsFooQaz).not.toBeDefined(); + expect(a.style.msFooQaz).toBe('ms'); + }); }); @@ -2267,25 +2366,35 @@ describe('jqLite', function() { }); - describe('camelCase', function() { + describe('kebabToCamel', function() { it('should leave non-dashed strings alone', function() { - expect(camelCase('foo')).toBe('foo'); - expect(camelCase('')).toBe(''); - expect(camelCase('fooBar')).toBe('fooBar'); + expect(kebabToCamel('foo')).toBe('foo'); + expect(kebabToCamel('')).toBe(''); + expect(kebabToCamel('fooBar')).toBe('fooBar'); + }); + + it('should convert dash-separated strings to camelCase', function() { + expect(kebabToCamel('foo-bar')).toBe('fooBar'); + expect(kebabToCamel('foo-bar-baz')).toBe('fooBarBaz'); + expect(kebabToCamel('foo:bar_baz')).toBe('foo:bar_baz'); }); + it('should convert leading dashes followed by a lowercase letter', function() { + expect(kebabToCamel('-foo-bar')).toBe('FooBar'); + }); - it('should covert dash-separated strings to camelCase', function() { - expect(camelCase('foo-bar')).toBe('fooBar'); - expect(camelCase('foo-bar-baz')).toBe('fooBarBaz'); - expect(camelCase('foo:bar_baz')).toBe('fooBarBaz'); + it('should not convert dashes followed by a non-letter', function() { + expect(kebabToCamel('foo-42- -a-B')).toBe('foo-42- A-B'); }); + it('should not convert browser specific css properties in a special way', function() { + expect(kebabToCamel('-ms-foo-bar')).toBe('MsFooBar'); + expect(kebabToCamel('-moz-foo-bar')).toBe('MozFooBar'); + expect(kebabToCamel('-webkit-foo-bar')).toBe('WebkitFooBar'); + }); - it('should covert browser specific css properties', function() { - expect(camelCase('-moz-foo-bar')).toBe('MozFooBar'); - expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar'); - expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar'); + it('should not collapse sequences of dashes', function() { + expect(kebabToCamel('foo---bar-baz--qaz')).toBe('foo--BarBaz-Qaz'); }); });