diff options
Diffstat (limited to 'devtools/client/shared/css-angle.js')
-rw-r--r-- | devtools/client/shared/css-angle.js | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/devtools/client/shared/css-angle.js b/devtools/client/shared/css-angle.js new file mode 100644 index 0000000000..9ecead6b44 --- /dev/null +++ b/devtools/client/shared/css-angle.js @@ -0,0 +1,349 @@ +/* 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 SPECIALVALUES = new Set(["initial", "inherit", "unset"]); + +const { getCSSLexer } = require("devtools/shared/css/lexer"); + +loader.lazyRequireGetter( + this, + "CSS_ANGLEUNIT", + "devtools/shared/css/constants", + true +); + +/** + * This module is used to convert between various angle units. + * + * Usage: + * let {angleUtils} = require("devtools/client/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.prototype = { + // Still keep trying to lazy load properties-db by lazily getting ANGLEUNIT + get ANGLEUNIT() { + return CSS_ANGLEUNIT; + }, + + _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() { + const token = getCSSLexer(this.authored).nextToken(); + if (!token) { + return false; + } + return ( + token.tokenType === "dimension" && + token.text.toLowerCase() in this.ANGLEUNIT + ); + }, + + get specialValue() { + return SPECIALVALUES.has(this.lowerCased) ? this.authored : null; + }, + + get deg() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + const angleUnit = classifyAngle(this.authored); + if (angleUnit === this.ANGLEUNIT.deg) { + // The angle is valid and is in degree. + return this.authored; + } + + let degValue; + if (angleUnit === this.ANGLEUNIT.rad) { + // The angle is valid and is in radian. + degValue = this.authoredAngleValue / (Math.PI / 180); + } + + if (angleUnit === this.ANGLEUNIT.grad) { + // The angle is valid and is in gradian. + degValue = this.authoredAngleValue * 0.9; + } + + if (angleUnit === this.ANGLEUNIT.turn) { + // The angle is valid and is in turn. + degValue = this.authoredAngleValue * 360; + } + + let unitStr = this.ANGLEUNIT.deg; + if (this._angleUnitUppercase === true) { + unitStr = unitStr.toUpperCase(); + } + return `${Math.round(degValue * 100) / 100}${unitStr}`; + }, + + get rad() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + const unit = classifyAngle(this.authored); + if (unit === this.ANGLEUNIT.rad) { + // The angle is valid and is in radian. + return this.authored; + } + + let radValue; + if (unit === this.ANGLEUNIT.deg) { + // The angle is valid and is in degree. + radValue = this.authoredAngleValue * (Math.PI / 180); + } + + if (unit === this.ANGLEUNIT.grad) { + // The angle is valid and is in gradian. + radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180); + } + + if (unit === this.ANGLEUNIT.turn) { + // The angle is valid and is in turn. + radValue = this.authoredAngleValue * 360 * (Math.PI / 180); + } + + let unitStr = this.ANGLEUNIT.rad; + if (this._angleUnitUppercase === true) { + unitStr = unitStr.toUpperCase(); + } + return `${Math.round(radValue * 10000) / 10000}${unitStr}`; + }, + + get grad() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + const unit = classifyAngle(this.authored); + if (unit === this.ANGLEUNIT.grad) { + // The angle is valid and is in gradian + return this.authored; + } + + let gradValue; + if (unit === this.ANGLEUNIT.deg) { + // The angle is valid and is in degree + gradValue = this.authoredAngleValue / 0.9; + } + + if (unit === this.ANGLEUNIT.rad) { + // The angle is valid and is in radian + gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180); + } + + if (unit === this.ANGLEUNIT.turn) { + // The angle is valid and is in turn + gradValue = this.authoredAngleValue * 400; + } + + let unitStr = this.ANGLEUNIT.grad; + if (this._angleUnitUppercase === true) { + unitStr = unitStr.toUpperCase(); + } + return `${Math.round(gradValue * 100) / 100}${unitStr}`; + }, + + get turn() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + const unit = classifyAngle(this.authored); + if (unit === this.ANGLEUNIT.turn) { + // The angle is valid and is in turn + return this.authored; + } + + let turnValue; + if (unit === this.ANGLEUNIT.deg) { + // The angle is valid and is in degree + turnValue = this.authoredAngleValue / 360; + } + + if (unit === this.ANGLEUNIT.rad) { + // The angle is valid and is in radian + turnValue = this.authoredAngleValue / (Math.PI / 180) / 360; + } + + if (unit === this.ANGLEUNIT.grad) { + // The angle is valid and is in gradian + turnValue = this.authoredAngleValue / 400; + } + + let unitStr = this.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; + + const reg = new RegExp(`(${Object.keys(this.ANGLEUNIT).join("|")})$`, "i"); + const 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(this.ANGLEUNIT); + const putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit)); + formats = formats.concat(putOnEnd); + const currentDisplayedValue = this[formats[0]]; + + for (const format of formats) { + if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) { + this.angleUnit = this.ANGLEUNIT[format]; + break; + } + } + return this.toString(); + }, + + /** + * Return a string representing a angle + */ + toString: function() { + let angle; + + switch (this.angleUnit) { + case this.ANGLEUNIT.deg: + angle = this.deg; + break; + case this.ANGLEUNIT.rad: + angle = this.rad; + break; + case this.ANGLEUNIT.grad: + angle = this.grad; + break; + case this.ANGLEUNIT.turn: + angle = this.turn; + break; + default: + angle = this.deg; + } + + if (this._angleUnitUppercase && this.angleUnit != this.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 CSS_ANGLEUNIT.deg; + } + + if (value.endsWith("grad")) { + return CSS_ANGLEUNIT.grad; + } + + if (value.endsWith("rad")) { + return CSS_ANGLEUNIT.rad; + } + if (value.endsWith("turn")) { + return CSS_ANGLEUNIT.turn; + } + + return CSS_ANGLEUNIT.deg; +} |