author | Nicolas Chevobbe <chevobbe.nicolas@gmail.com> |
Tue, 08 Mar 2016 23:04:54 +0100 | |
changeset 290341 | 7356a69124c90d98a417926455ca8d8d7d27691c |
parent 290340 | c96ec660af96a187b62c5dd5f9bce62c0f2ffeb6 |
child 290342 | a296e3d1019d2953f4595b7d4aec24c19c4c1ce6 |
push id | 30118 |
push user | ryanvm@gmail.com |
push date | Fri, 25 Mar 2016 15:37:11 +0000 |
treeherder | mozilla-central@b2dbee5ca727 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | miker |
bugs | 1250835 |
milestone | 48.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/client/inspector/rules/views/text-property-editor.js +++ b/devtools/client/inspector/rules/views/text-property-editor.js @@ -332,25 +332,28 @@ TextPropertyEditor.prototype = { } else { this.element.removeAttribute("dirty"); } const sharedSwatchClass = "ruleview-swatch "; const colorSwatchClass = "ruleview-colorswatch"; const bezierSwatchClass = "ruleview-bezierswatch"; const filterSwatchClass = "ruleview-filterswatch"; + const angleSwatchClass = "ruleview-angleswatch"; let outputParser = this.ruleView._outputParser; let parserOptions = { colorSwatchClass: sharedSwatchClass + colorSwatchClass, colorClass: "ruleview-color", bezierSwatchClass: sharedSwatchClass + bezierSwatchClass, bezierClass: "ruleview-bezier", filterSwatchClass: sharedSwatchClass + filterSwatchClass, filterClass: "ruleview-filter", + angleSwatchClass: sharedSwatchClass + angleSwatchClass, + angleClass: "ruleview-angle", defaultColorType: !propDirty, urlClass: "theme-link", baseURI: this.sheetURI }; let frag = outputParser.parseCssProperty(name, val, parserOptions); this.valueSpan.innerHTML = ""; this.valueSpan.appendChild(frag);
--- a/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js +++ b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js @@ -9,16 +9,17 @@ // This is more of a unit test than a mochitest-browser test, but can't be // tested with an xpcshell test as the output-parser requires the DOM to work. var {OutputParser} = require("devtools/client/shared/output-parser"); const COLOR_CLASS = "color-class"; const URL_CLASS = "url-class"; const CUBIC_BEZIER_CLASS = "bezier-class"; +const ANGLE_CLASS = "angle-class"; const TEST_DATA = [ { name: "width", value: "100%", test: fragment => { is(countAll(fragment), 0); is(fragment.textContent, "100%"); @@ -155,21 +156,24 @@ const TEST_DATA = [ is(allSwatches[3].textContent, "#F06"); is(allSwatches[4].textContent, "red"); } }, { name: "background", value: "-moz-radial-gradient(center 45deg, circle closest-side, orange 0%, red 100%)", test: fragment => { - is(countAll(fragment), 4); - let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS); - is(allSwatches.length, 2); - is(allSwatches[0].textContent, "orange"); - is(allSwatches[1].textContent, "red"); + is(countAll(fragment), 6); + let colorSwatches = fragment.querySelectorAll("." + COLOR_CLASS); + is(colorSwatches.length, 2); + is(colorSwatches[0].textContent, "orange"); + is(colorSwatches[1].textContent, "red"); + let angleSwatches = fragment.querySelectorAll("." + ANGLE_CLASS); + is(angleSwatches.length, 1); + is(angleSwatches[0].textContent, "45deg"); } }, { name: "background", value: "white url(http://test.com/wow_such_image.png) no-repeat top left", test: fragment => { is(countAll(fragment), 3); is(countUrls(fragment), 1); @@ -291,16 +295,17 @@ add_task(function*() { for (let i = 0; i < TEST_DATA.length; i++) { let data = TEST_DATA[i]; info("Output-parser test data " + i + ". {" + data.name + " : " + data.value + ";}"); data.test(parser.parseCssProperty(data.name, data.value, { colorClass: COLOR_CLASS, urlClass: URL_CLASS, bezierClass: CUBIC_BEZIER_CLASS, + angleClass: ANGLE_CLASS, defaultColorType: false })); } }); function countAll(fragment) { return fragment.querySelectorAll("*").length; }
--- a/devtools/client/jar.mn +++ b/devtools/client/jar.mn @@ -149,16 +149,17 @@ devtools.jar: skin/dark-theme.css (themes/dark-theme.css) skin/light-theme.css (themes/light-theme.css) skin/firebug-theme.css (themes/firebug-theme.css) skin/toolbars.css (themes/toolbars.css) skin/variables.css (themes/variables.css) skin/images/add.svg (themes/images/add.svg) skin/images/filters.svg (themes/images/filters.svg) skin/images/filter-swatch.svg (themes/images/filter-swatch.svg) + skin/images/angle-swatch.svg (themes/images/angle-swatch.svg) skin/images/pseudo-class.svg (themes/images/pseudo-class.svg) skin/images/controls.png (themes/images/controls.png) skin/images/controls@2x.png (themes/images/controls@2x.png) skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg) skin/images/performance-icons.svg (themes/images/performance-icons.svg) skin/widgets.css (themes/widgets.css) skin/images/power.svg (themes/images/power.svg) skin/images/filetypes/dir-close.svg (themes/images/filetypes/dir-close.svg)
--- a/devtools/client/shared/output-parser.js +++ b/devtools/client/shared/output-parser.js @@ -1,15 +1,16 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Cc, Ci, Cu} = require("chrome"); +const {angleUtils} = require("devtools/shared/css-angle"); const {colorUtils} = require("devtools/shared/css-color"); const Services = require("Services"); const HTML_NS = "http://www.w3.org/1999/xhtml"; const BEZIER_KEYWORDS = ["linear", "ease-in-out", "ease-in", "ease-out", "ease"]; @@ -19,16 +20,31 @@ const COLOR_TAKING_FUNCTIONS = ["linear- "repeating-linear-gradient", "-moz-repeating-linear-gradient", "radial-gradient", "-moz-radial-gradient", "repeating-radial-gradient", "-moz-repeating-radial-gradient", "drop-shadow"]; +// Functions that accept an angle argument. +const ANGLE_TAKING_FUNCTIONS = ["linear-gradient", + "-moz-linear-gradient", + "repeating-linear-gradient", + "-moz-repeating-linear-gradient", + "rotate", + "rotateX", + "rotateY", + "rotateZ", + "rotate3d", + "skew", + "skewX", + "skewY", + "hue-rotate"]; + loader.lazyGetter(this, "DOMUtils", function() { return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); }); /** * This module is used to process text for output by developer tools. This means * linking JS files with the debugger, CSS files with the style editor, JS * functions with the debugger, placing color swatches next to colors and @@ -43,17 +59,19 @@ loader.lazyGetter(this, "DOMUtils", func * let parser = new OutputParser(document); * * parser.parseCssProperty("color", "red"); // Returns document fragment. */ function OutputParser(document) { this.parsed = []; this.doc = document; this.colorSwatches = new WeakMap(); - this._onSwatchMouseDown = this._onSwatchMouseDown.bind(this); + this.angleSwatches = new WeakMap(); + this._onColorSwatchMouseDown = this._onColorSwatchMouseDown.bind(this); + this._onAngleSwatchMouseDown = this._onAngleSwatchMouseDown.bind(this); } exports.OutputParser = OutputParser; OutputParser.prototype = { /** * Parse a CSS property value given a property name. * @@ -62,17 +80,17 @@ OutputParser.prototype = { * @param {String} value * CSS Property value * @param {Object} [options] * Options object. For valid options and default values see * _mergeOptions(). * @return {DocumentFragment} * A document fragment containing color swatches etc. */ - parseCssProperty: function(name, value, options={}) { + parseCssProperty: function(name, value, options = {}) { options = this._mergeOptions(options); options.expectCubicBezier = safeCssPropertySupportsType(name, DOMUtils.TYPE_TIMING_FUNCTION); options.expectFilter = name === "filter"; options.supportsColor = safeCssPropertySupportsType(name, DOMUtils.TYPE_COLOR) || safeCssPropertySupportsType(name, DOMUtils.TYPE_GRADIENT); @@ -135,50 +153,56 @@ OutputParser.prototype = { * @param {String} text * Text to parse. * @param {Object} [options] * Options object. For valid options and default values see * _mergeOptions(). * @return {DocumentFragment} * A document fragment. */ - _parse: function(text, options={}) { + _parse: function(text, options = {}) { text = text.trim(); this.parsed.length = 0; let tokenStream = DOMUtils.getCSSLexer(text); let parenDepth = 0; let outerMostFunctionTakesColor = false; let colorOK = function() { return options.supportsColor || (options.expectFilter && parenDepth === 1 && outerMostFunctionTakesColor); }; + let angleOK = function(angle) { + return /^-?\d+\.?\d*(deg|rad|grad|turn)$/gi.test(angle); + }; + while (true) { let token = tokenStream.nextToken(); if (!token) { break; } if (token.tokenType === "comment") { continue; } switch (token.tokenType) { case "function": { - if (COLOR_TAKING_FUNCTIONS.indexOf(token.text) >= 0) { - // The function can accept a color argument, and we know - // it isn't special in some other way. So, we let it - // through to the ordinary parsing loop so that colors + if (COLOR_TAKING_FUNCTIONS.includes(token.text) || + ANGLE_TAKING_FUNCTIONS.includes(token.text)) { + // The function can accept a color or an angle argument, and we know + // it isn't special in some other way. So, we let it + // through to the ordinary parsing loop so that the value // can be handled in a single place. this._appendTextNode(text.substring(token.startOffset, token.endOffset)); if (parenDepth === 0) { - outerMostFunctionTakesColor = true; + outerMostFunctionTakesColor = COLOR_TAKING_FUNCTIONS.includes( + token.text); } ++parenDepth; } else { let functionText = this._collectFunctionText(token, text, tokenStream); if (options.expectCubicBezier && token.text === "cubic-bezier") { this._appendCubicBezier(functionText, options); @@ -192,49 +216,61 @@ OutputParser.prototype = { } case "ident": if (options.expectCubicBezier && BEZIER_KEYWORDS.indexOf(token.text) >= 0) { this._appendCubicBezier(token.text, options); } else if (colorOK() && DOMUtils.isValidCSSColor(token.text)) { this._appendColor(token.text, options); + } else if (angleOK(token.text)) { + this._appendAngle(token.text, options); } else { this._appendTextNode(text.substring(token.startOffset, token.endOffset)); } break; case "id": case "hash": { let original = text.substring(token.startOffset, token.endOffset); if (colorOK() && DOMUtils.isValidCSSColor(original)) { this._appendColor(original, options); } else { this._appendTextNode(original); } break; } - + case "dimension": + let value = text.substring(token.startOffset, token.endOffset); + if (angleOK(value)) { + this._appendAngle(value, options); + } else { + this._appendTextNode(value); + } + break; case "url": case "bad_url": this._appendURL(text.substring(token.startOffset, token.endOffset), token.text, options); break; case "symbol": if (token.text === "(") { ++parenDepth; - } else if (token.token === ")") { + } else if (token.text === ")") { --parenDepth; + if (parenDepth === 0) { + outerMostFunctionTakesColor = false; + } } // falls through default: - this._appendTextNode(text.substring(token.startOffset, - token.endOffset)); + this._appendTextNode( + text.substring(token.startOffset, token.endOffset)); break; } } let result = this._toDOM(); if (options.expectFilter && !options.filterSwatch) { result = this._wrapFilter(text, options, result); @@ -249,17 +285,17 @@ OutputParser.prototype = { * @param {String} bezier * The cubic-bezier timing function * @param {Object} options * Options object. For valid options and default values see * _mergeOptions() */ _appendCubicBezier: function(bezier, options) { let container = this._createNode("span", { - "data-bezier": bezier + "data-bezier": bezier }); if (options.bezierSwatchClass) { let swatch = this._createNode("span", { class: options.bezierSwatchClass }); container.appendChild(swatch); } @@ -268,16 +304,58 @@ OutputParser.prototype = { class: options.bezierClass }, bezier); container.appendChild(value); this.parsed.push(container); }, /** + * Append a angle value to the output + * + * @param {String} angle + * angle to append + * @param {Object} options + * Options object. For valid options and default values see + * _mergeOptions() + */ + _appendAngle: function(angle, options) { + let angleObj = new angleUtils.CssAngle(angle); + let container = this._createNode("span", { + "data-angle": angle + }); + + if (options.angleSwatchClass) { + let swatch = this._createNode("span", { + class: options.angleSwatchClass + }); + this.angleSwatches.set(swatch, angleObj); + swatch.addEventListener("mousedown", this._onAngleSwatchMouseDown, false); + + // Add click listener to stop event propagation when shift key is pressed + // in order to prevent the value input to be focused. + // Bug 711942 will add a tooltip to edit angle values and we should + // be able to move this listener to Tooltip.js when it'll be implemented. + swatch.addEventListener("click", function(event) { + if (event.shiftKey) { + event.stopPropagation(); + } + }, false); + container.appendChild(swatch); + } + + let value = this._createNode("span", { + class: options.angleClass + }, angle); + + container.appendChild(value); + this.parsed.push(container); + }, + + /** * Check if a CSS property supports a specific value. * * @param {String} name * CSS Property name to check * @param {String} value * CSS Property value to check */ _cssPropertySupportsValue: function(name, value) { @@ -312,17 +390,17 @@ OutputParser.prototype = { }); if (options.colorSwatchClass) { let swatch = this._createNode("span", { class: options.colorSwatchClass, style: "background-color:" + color }); this.colorSwatches.set(swatch, colorObj); - swatch.addEventListener("mousedown", this._onSwatchMouseDown, false); + swatch.addEventListener("mousedown", this._onColorSwatchMouseDown, false); container.appendChild(swatch); } if (options.defaultColorType) { color = colorObj.toString(); container.dataset.colorĀ = color; } @@ -366,31 +444,46 @@ OutputParser.prototype = { class: options.filterClass }); value.appendChild(nodes); container.appendChild(value); return container; }, - _onSwatchMouseDown: function(event) { + _onColorSwatchMouseDown: function(event) { // Prevent text selection in the case of shift-click or double-click. event.preventDefault(); if (!event.shiftKey) { return; } let swatch = event.target; let color = this.colorSwatches.get(swatch); let val = color.nextColorUnit(); swatch.nextElementSibling.textContent = val; }, + _onAngleSwatchMouseDown: function(event) { + // Prevent text selection in the case of shift-click or double-click. + event.preventDefault(); + + if (!event.shiftKey) { + return; + } + + let swatch = event.target; + let angle = this.angleSwatches.get(swatch); + let val = angle.nextAngleUnit(); + + swatch.nextElementSibling.textContent = val; + }, + /** * A helper function that sanitizes a possibly-unterminated URL. */ _sanitizeURL: function(url) { // Re-lex the URL and add any needed termination characters. let urlTokenizer = DOMUtils.getCSSLexer(url); // Just read until EOF; there will only be a single token. while (urlTokenizer.nextToken()) { @@ -538,16 +631,19 @@ OutputParser.prototype = { * - defaultColorType: true // Convert colors to the default type * // selected in the options panel. * - colorSwatchClass: "" // The class to use for color swatches. * - colorClass: "" // The class to use for the color value * // that follows the swatch. * - bezierSwatchClass: "" // The class to use for bezier swatches. * - bezierClass: "" // The class to use for the bezier value * // that follows the swatch. + * - angleSwatchClass: "" // The class to use for angle swatches. + * - angleClass: "" // The class to use for the angle value + * // that follows the swatch. * - supportsColor: false // Does the CSS property support colors? * - urlClass: "" // The class to be used for url() links. * - baseURI: "" // A string or nsIURI used to resolve * // relative links. * - filterSwatch: false // A special case for parsing a * // "filter" property, causing the * // parser to skip the call to * // _wrapFilter. Used only for @@ -557,16 +653,18 @@ OutputParser.prototype = { */ _mergeOptions: function(overrides) { let defaults = { defaultColorType: true, colorSwatchClass: "", colorClass: "", bezierSwatchClass: "", bezierClass: "", + angleSwatchClass: "", + angleClass: "", supportsColor: false, urlClass: "", baseURI: "", filterSwatch: false }; if (typeof overrides.baseURI === "string") { overrides.baseURI = Services.io.newURI(overrides.baseURI, null, null);
--- a/devtools/client/shared/test/browser.ini +++ b/devtools/client/shared/test/browser.ini @@ -14,16 +14,17 @@ support-files = html-mdn-css-no-summary.html html-mdn-css-no-summary-or-syntax.html html-mdn-css-no-syntax.html html-mdn-css-syntax-old-style.html leakhunt.js test-actor.js test-actor-registry.js +[browser_css_angle.js] [browser_css_color.js] [browser_cubic-bezier-01.js] [browser_cubic-bezier-02.js] [browser_cubic-bezier-03.js] [browser_cubic-bezier-04.js] [browser_cubic-bezier-05.js] [browser_cubic-bezier-06.js] [browser_filter-editor-01.js]
new file mode 100644 --- /dev/null +++ b/devtools/client/shared/test/browser_css_angle.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from head.js */ +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,browser_css_angle.js"; +var {angleUtils} = require("devtools/shared/css-angle"); + +add_task(function*() { + yield addTab("about:blank"); + let [host] = yield createHost("bottom", TEST_URI); + + info("Starting the test"); + testAngleUtils(); + + host.destroy(); + gBrowser.removeCurrentTab(); +}); + +function testAngleUtils() { + let data = getTestData(); + + for (let {authored, deg, rad, grad, turn} of data) { + let angle = new angleUtils.CssAngle(authored); + + // Check all values. + info("Checking values for " + authored); + is(angle.deg, deg, "color.deg === deg"); + is(angle.rad, rad, "color.rad === rad"); + is(angle.grad, grad, "color.grad === grad"); + is(angle.turn, turn, "color.turn === turn"); + + testToString(angle, deg, rad, grad, turn); + } +} + +function testToString(angle, deg, rad, grad, turn) { + angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.deg; + is(angle.toString(), deg, "toString() with deg type"); + + angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.rad; + is(angle.toString(), rad, "toString() with rad type"); + + angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.grad; + is(angle.toString(), grad, "toString() with grad type"); + + angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.turn; + is(angle.toString(), turn, "toString() with turn type"); +} + +function getTestData() { + return [{ + authored: "0deg", + deg: "0deg", + rad: "0rad", + grad: "0grad", + turn: "0turn" + }, { + authored: "180deg", + deg: "180deg", + rad: "3.14rad", + grad: "200grad", + turn: "0.5turn" + }, { + authored: "180DEG", + deg: "180DEG", + rad: "3.14RAD", + grad: "200GRAD", + turn: "0.5TURN" + }, { + authored: `-${Math.PI}rad`, + deg: "-180deg", + rad: `-${Math.PI}rad`, + grad: "-200grad", + turn: "-0.5turn" + }, { + authored: `-${Math.PI}RAD`, + deg: "-180DEG", + rad: `-${Math.PI}RAD`, + grad: "-200GRAD", + turn: "-0.5TURN" + }, { + authored: "100grad", + deg: "90deg", + rad: "1.57rad", + grad: "100grad", + turn: "0.25turn" + }, { + authored: "100GRAD", + deg: "90DEG", + rad: "1.57RAD", + grad: "100GRAD", + turn: "0.25TURN" + }, { + authored: "-1turn", + deg: "-360deg", + rad: "-6.28rad", + grad: "-400grad", + turn: "-1turn" + }, { + authored: "-10TURN", + deg: "-3600DEG", + rad: "-62.83RAD", + grad: "-4000GRAD", + turn: "-10TURN" + }, { + authored: "inherit", + deg: "inherit", + rad: "inherit", + grad: "inherit", + turn: "inherit" + }, { + authored: "initial", + deg: "initial", + rad: "initial", + grad: "initial", + turn: "initial" + }, { + authored: "unset", + deg: "unset", + rad: "unset", + grad: "unset", + turn: "unset" + }]; +}
--- a/devtools/client/shared/test/browser_outputparser.js +++ b/devtools/client/shared/test/browser_outputparser.js @@ -17,16 +17,17 @@ function* performTest() { let [host, , doc] = yield createHost("bottom", "data:text/html," + "<h1>browser_outputParser.js</h1><div></div>"); let parser = new OutputParser(doc); testParseCssProperty(doc, parser); testParseCssVar(doc, parser); testParseURL(doc, parser); testParseFilter(doc, parser); + testParseAngle(doc, parser); host.destroy(); } // Class name used in color swatch. var COLOR_TEST_CLASS = "test-class"; // Create a new CSS color-parsing test. |name| is the name of the CSS @@ -96,18 +97,18 @@ function testParseCssProperty(doc, parse "blur(1px) drop-shadow(0 0 0 ", {name: "blue"}, ") url(red.svg#blue)</span></span>"]), makeColorTest("color", "currentColor", ["currentColor"]), // Test a very long property. makeColorTest("background-image", - "linear-gradient(0deg, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)", - ["linear-gradient(0deg, ", {name: "transparent"}, + "linear-gradient(to left, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)", + ["linear-gradient(to left, ", {name: "transparent"}, " 0, ", {name: "transparent"}, " 5%,", {name: "#F00"}, " 0, ", {name: "#F00"}, " 10%,", {name: "#FF0"}, " 0, ", {name: "#FF0"}, " 15%,", {name: "#0F0"}, " 0, ", {name: "#0F0"}, " 20%,", {name: "#0FF"}, @@ -254,8 +255,25 @@ function testParseFilter(doc, parser) { let frag = parser.parseCssProperty("filter", "something invalid", { filterSwatchClass: "test-filterswatch" }); let swatchCount = frag.querySelectorAll(".test-filterswatch").length; is(swatchCount, 1, "filter swatch was created"); } +function testParseAngle(doc, parser) { + let frag = parser.parseCssProperty("image-orientation", "90deg", { + angleSwatchClass: "test-angleswatch" + }); + + let swatchCount = frag.querySelectorAll(".test-angleswatch").length; + is(swatchCount, 1, "angle swatch was created"); + + frag = parser.parseCssProperty("background-image", + "linear-gradient(90deg, red, blue", { + angleSwatchClass: "test-angleswatch" + }); + + swatchCount = frag.querySelectorAll(".test-angleswatch").length; + is(swatchCount, 1, "angle swatch was created"); +} +
new file mode 100644 --- /dev/null +++ b/devtools/client/themes/images/angle-swatch.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12px" height="12px"> + <mask id="angle-mask"> + <rect width="100%" height="100%" fill="#fff"/> + <polygon points="6 6, 12 12, 0 12, 0 0, 6 0, 6 6"/> + </mask> + <mask id="circle-mask"> + <circle cx="6" cy="6" r="6" fill="#fff"/> + </mask> + <circle cx="6" cy="6" r="6" fill="#fff"/> + <circle cx="6" cy="6" r="6" mask="url(#angle-mask)" fill="#aeb0b1"/> + <line x1="6" y1="0" x2="6" y2="6" stroke-width="0.5" stroke="rgba(0,0,0,0.5)"></line> + <line x1="6" y1="6" x2="12" y2="12" stroke-width="0.5" stroke="rgba(0,0,0,0.5)" mask="url(#circle-mask)"></line> +</svg>
--- a/devtools/client/themes/rules.css +++ b/devtools/client/themes/rules.css @@ -315,16 +315,21 @@ background-size: 1em; } .ruleview-filterswatch { background: url("chrome://devtools/skin/images/filter-swatch.svg"); background-size: 1em; } +.ruleview-angleswatch { + background: url("chrome://devtools/skin/images/angle-swatch.svg"); + background-size: 1em; +} + @media (min-resolution: 1.1dppx) { .ruleview-bezierswatch { background: url("chrome://devtools/skin/images/cubic-bezier-swatch@2x.png"); background-size: 1em; } } .ruleview-overridden {
new file mode 100644 --- /dev/null +++ b/devtools/shared/css-angle.js @@ -0,0 +1,348 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc, Ci} = require("chrome"); + +const SPECIALVALUES = new Set([ + "initial", + "inherit", + "unset" +]); + +/** + * This module is used to convert between various angle units. + * + * Usage: + * let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); + * let {angleUtils} = require("devtools/shared/css-angle"); + * let angle = new angleUtils.CssAngle("180deg"); + * + * angle.authored === "180deg" + * angle.valid === true + * angle.rad === "3,14rad" + * angle.grad === "200grad" + * angle.turn === "0.5turn" + * + * angle.toString() === "180deg"; // Outputs the angle value and its unit + * // Angle objects can be reused + * angle.newAngle("-1TURN") === "-1TURN"; // true + */ + +function CssAngle(angleValue) { + this.newAngle(angleValue); +} + +module.exports.angleUtils = { + CssAngle: CssAngle, + classifyAngle: classifyAngle +}; + +CssAngle.ANGLEUNIT = { + "deg": "deg", + "rad": "rad", + "grad": "grad", + "turn": "turn" +}; + +CssAngle.prototype = { + _angleUnit: null, + _angleUnitUppercase: false, + + // The value as-authored. + authored: null, + // A lower-cased copy of |authored|. + lowerCased: null, + + get angleUnit() { + if (this._angleUnit === null) { + this._angleUnit = classifyAngle(this.authored); + } + return this._angleUnit; + }, + + set angleUnit(unit) { + this._angleUnit = unit; + }, + + get valid() { + return /^-?\d+\.?\d*(deg|rad|grad|turn)$/gi.test(this.authored); + }, + + get specialValue() { + return SPECIALVALUES.has(this.lowerCased) ? this.authored : null; + }, + + get deg() { + let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + let angleUnit = classifyAngle(this.authored); + if (angleUnit === CssAngle.ANGLEUNIT.deg) { + // The angle is valid and is in degree. + return this.authored; + } + + let degValue; + if (angleUnit === CssAngle.ANGLEUNIT.rad) { + // The angle is valid and is in radian. + degValue = this.authoredAngleValue / (Math.PI / 180); + } + + if (angleUnit === CssAngle.ANGLEUNIT.grad) { + // The angle is valid and is in gradian. + degValue = this.authoredAngleValue * 0.9; + } + + if (angleUnit === CssAngle.ANGLEUNIT.turn) { + // The angle is valid and is in turn. + degValue = this.authoredAngleValue * 360; + } + + let unitStr = CssAngle.ANGLEUNIT.deg; + if (this._angleUnitUppercase === true) { + unitStr = unitStr.toUpperCase(); + } + return `${Math.round(degValue * 100) / 100}${unitStr}`; + }, + + get rad() { + let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + let unit = classifyAngle(this.authored); + if (unit === CssAngle.ANGLEUNIT.rad) { + // The angle is valid and is in radian. + return this.authored; + } + + let radValue; + if (unit === CssAngle.ANGLEUNIT.deg) { + // The angle is valid and is in degree. + radValue = this.authoredAngleValue * (Math.PI / 180); + } + + if (unit === CssAngle.ANGLEUNIT.grad) { + // The angle is valid and is in gradian. + radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180); + } + + if (unit === CssAngle.ANGLEUNIT.turn) { + // The angle is valid and is in turn. + radValue = this.authoredAngleValue * 360 * (Math.PI / 180); + } + + let unitStr = CssAngle.ANGLEUNIT.rad; + if (this._angleUnitUppercase === true) { + unitStr = unitStr.toUpperCase(); + } + return `${Math.round(radValue * 100) / 100}${unitStr}`; + }, + + get grad() { + let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + let unit = classifyAngle(this.authored); + if (unit === CssAngle.ANGLEUNIT.grad) { + // The angle is valid and is in gradian + return this.authored; + } + + let gradValue; + if (unit === CssAngle.ANGLEUNIT.deg) { + // The angle is valid and is in degree + gradValue = this.authoredAngleValue / 0.9; + } + + if (unit === CssAngle.ANGLEUNIT.rad) { + // The angle is valid and is in radian + gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180); + } + + if (unit === CssAngle.ANGLEUNIT.turn) { + // The angle is valid and is in turn + gradValue = this.authoredAngleValue * 400; + } + + let unitStr = CssAngle.ANGLEUNIT.grad; + if (this._angleUnitUppercase === true) { + unitStr = unitStr.toUpperCase(); + } + return `${Math.round(gradValue * 100) / 100}${unitStr}`; + }, + + get turn() { + let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + let unit = classifyAngle(this.authored); + if (unit === CssAngle.ANGLEUNIT.turn) { + // The angle is valid and is in turn + return this.authored; + } + + let turnValue; + if (unit === CssAngle.ANGLEUNIT.deg) { + // The angle is valid and is in degree + turnValue = this.authoredAngleValue / 360; + } + + if (unit === CssAngle.ANGLEUNIT.rad) { + // The angle is valid and is in radian + turnValue = (this.authoredAngleValue / (Math.PI / 180)) / 360; + } + + if (unit === CssAngle.ANGLEUNIT.grad) { + // The angle is valid and is in gradian + turnValue = this.authoredAngleValue / 400; + } + + let unitStr = CssAngle.ANGLEUNIT.turn; + if (this._angleUnitUppercase === true) { + unitStr = unitStr.toUpperCase(); + } + return `${Math.round(turnValue * 100) / 100}${unitStr}`; + }, + + /** + * Check whether the angle value is in the special list e.g. + * inherit or invalid. + * + * @return {String|Boolean} + * - If the current angle is a special value e.g. "inherit" then + * return the angle. + * - If the angle is invalid return an empty string. + * - If the angle is a regular angle e.g. 90deg so we return false + * to indicate that the angle is neither invalid nor special. + */ + _getInvalidOrSpecialValue: function() { + if (this.specialValue) { + return this.specialValue; + } + if (!this.valid) { + return ""; + } + return false; + }, + + /** + * Change angle + * + * @param {String} angle + * Any valid angle value + unit string + */ + newAngle: function(angle) { + // Store a lower-cased version of the angle to help with format + // testing. The original text is kept as well so it can be + // returned when needed. + this.lowerCased = angle.toLowerCase(); + this._angleUnitUppercase = (angle === angle.toUpperCase()); + this.authored = angle; + + let reg = new RegExp( + `(${Object.keys(CssAngle.ANGLEUNIT).join("|")})$`, "i"); + let unitStartIdx = angle.search(reg); + this.authoredAngleValue = angle.substring(0, unitStartIdx); + this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length); + + return this; + }, + + nextAngleUnit: function() { + // Get a reordered array from the formats object + // to have the current format at the front so we can cycle through. + let formats = Object.keys(CssAngle.ANGLEUNIT); + let putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit)); + formats = formats.concat(putOnEnd); + let currentDisplayedValue = this[formats[0]]; + + for (let format of formats) { + if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) { + this.angleUnit = CssAngle.ANGLEUNIT[format]; + break; + } + } + return this.toString(); + }, + + /** + * Return a string representing a angle + */ + toString: function() { + let angle; + + switch (this.angleUnit) { + case CssAngle.ANGLEUNIT.deg: + angle = this.deg; + break; + case CssAngle.ANGLEUNIT.rad: + angle = this.rad; + break; + case CssAngle.ANGLEUNIT.grad: + angle = this.grad; + break; + case CssAngle.ANGLEUNIT.turn: + angle = this.turn; + break; + default: + angle = this.deg; + } + + if (this._angleUnitUppercase && + this.angleUnit != CssAngle.ANGLEUNIT.authored) { + angle = angle.toUpperCase(); + } + return angle; + }, + + /** + * This method allows comparison of CssAngle objects using ===. + */ + valueOf: function() { + return this.deg; + }, +}; + +/** + * Given a color, classify its type as one of the possible angle + * units, as known by |CssAngle.angleUnit|. + * + * @param {String} value + * The angle, in any form accepted by CSS. + * @return {String} + * The angle classification, one of "deg", "rad", "grad", or "turn". + */ +function classifyAngle(value) { + value = value.toLowerCase(); + if (value.endsWith("deg")) { + return CssAngle.ANGLEUNIT.deg; + } + + if (value.endsWith("grad")) { + return CssAngle.ANGLEUNIT.grad; + } + + if (value.endsWith("rad")) { + return CssAngle.ANGLEUNIT.rad; + } + if (value.endsWith("turn")) { + return CssAngle.ANGLEUNIT.turn; + } + + return CssAngle.ANGLEUNIT.deg; +} + +loader.lazyGetter(this, "DOMUtils", function() { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +});
--- a/devtools/shared/moz.build +++ b/devtools/shared/moz.build @@ -34,16 +34,17 @@ MOCHITEST_CHROME_MANIFESTS += ['tests/mo XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini'] JAR_MANIFESTS += ['jar.mn'] DevToolsModules( 'async-storage.js', 'async-utils.js', 'content-observer.js', + 'css-angle.js', 'css-color.js', 'deprecated-sync-thenables.js', 'DevToolsUtils.js', 'event-emitter.js', 'event-parsers.js', 'indentation.js', 'Loader.jsm', 'Parser.jsm',
new file mode 100644 --- /dev/null +++ b/devtools/shared/tests/unit/test_cssAngle.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test classifyAngle. + +"use strict"; + +const {angleUtils} = require("devtools/shared/css-angle"); + +const CLASSIFY_TESTS = [ + { input: "180deg", output: "deg" }, + { input: "-180deg", output: "deg" }, + { input: "180DEG", output: "deg" }, + { input: "200rad", output: "rad" }, + { input: "-200rad", output: "rad" }, + { input: "200RAD", output: "rad" }, + { input: "0.5grad", output: "grad" }, + { input: "-0.5grad", output: "grad" }, + { input: "0.5GRAD", output: "grad" }, + { input: "0.33turn", output: "turn" }, + { input: "0.33TURN", output: "turn" }, + { input: "-0.33turn", output: "turn" } +]; + +function run_test() { + for (let test of CLASSIFY_TESTS) { + let result = angleUtils.classifyAngle(test.input); + equal(result, test.output, "test classifyAngle(" + test.input + ")"); + } +}
--- a/devtools/shared/tests/unit/xpcshell.ini +++ b/devtools/shared/tests/unit/xpcshell.ini @@ -14,14 +14,15 @@ support-files = [test_fetch-resource.js] [test_indentation.js] [test_independent_loaders.js] [test_invisible_loader.js] [test_safeErrorString.js] [test_defineLazyPrototypeGetter.js] [test_async-utils.js] [test_consoleID.js] +[test_cssAngle.js] [test_cssColor.js] [test_prettifyCSS.js] [test_require_lazy.js] [test_require.js] [test_stack.js] [test_executeSoon.js]