// Copyright 2016 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
library over_react.css_value_util;
import 'package:quiver/core.dart';
/// A CSS length value, with a number and unit component, for use in CSS properties such as `width`, `top`, `padding`, etc.
class CssValue implements Comparable<CssValue> {
/// The number component of this CSS value.
///
/// E.g., 1 for '1px'
final num number;
/// The unit component of this CSS value.
///
/// E.g., 'px' for '1px'
final String unit;
/// Creates a new [CssValue]. If no [unit] is specified, `'px'` is used instead.
const CssValue(this.number, [this.unit = 'px']);
/// Parse [source] and return its [CssValue] representation.
///
/// Accepts a number optionally followed by a CSS length unit. If no unit is present, `'px'` is used as the unit instead.
///
/// If `source` is not a valid CSS value, the [onError] callback is called with [source] and an error object, and its return value is used instead. If no `onError` is provided, `null` is returned.
///
/// Examples of accepted values:
///
/// '2px'
/// '10'
/// 20
/// '1.25em'
/// '-15%'
factory CssValue.parse(dynamic source, {CssValue onError(value, error)}) {
num number;
String unit;
var error;
if (source == null) {
error = new ArgumentError.notNull('value');
} else if (source is num) {
number = source;
unit = 'px';
} else {
var unitMatch = new RegExp(r'(?:rem|em|ex|vh|vw|vmin|vmax|%|px|cm|mm|in|pt|pc|ch)?$').firstMatch(source.toString());
unit = unitMatch.group(0);
if (unit == '') {
unit = 'px';
}
number = double.parse(unitMatch.input.substring(0, unitMatch.start), (_) {
error = new ArgumentError.value(source, 'value', 'Invalid number/unit for CSS value');
});
}
if (number != null && !number.isFinite) {
// Rule out -Infinity, Infinity, and NaN.
error = new ArgumentError.value(number, 'value', 'Number portion of CSS value ($source) must be finite');
}
var result;
if (error != null) {
if (onError == null) {
result = null;
} else {
result = onError(source, error);
}
} else {
result = new CssValue(number, unit);
}
return result;
}
/// Throws an error if this value's [unit] does not match that of [other].
///
/// Used internally to prevent calculations between incompatible units.
void _checkMatchingUnits(CssValue other) {
if (unit != other.unit) {
throw new ArgumentError('Cannot compare CSS unit values of units $unit and ${other.unit}');
}
}
/// Returns the remainder of dividing this value's [number] by [other].
CssValue operator %(num other) => new CssValue(number % other, unit);
/// Returns the result of multiplying this value's [number] by [other].
CssValue operator *(num other) => new CssValue(number * other, unit);
/// Returns the result of dividing this value's [number] by [other].
CssValue operator /(num other) => new CssValue(number / other, unit);
/// Returns a new [CssValue] with the sum of the [number]s of this value and [other].
///
/// Throws an error if the [unit] of this value and [other] do not match.
CssValue operator +(CssValue other) {
_checkMatchingUnits(other);
return new CssValue(number + other.number, unit);
}
/// Returns a new [CssValue] with the difference between the [number]s of this value and [other].
///
/// Throws an error if the [unit] of this value and [other] do not match.
CssValue operator -(CssValue other) {
_checkMatchingUnits(other);
return new CssValue(number - other.number, unit);
}
/// Returns whether this value's [number] is less than that of [other].
///
/// Throws an error if the [unit] of this value and [other] do not match.
bool operator <(CssValue other) {
_checkMatchingUnits(other);
return number < other.number;
}
/// Returns whether this value's [number] is less than or equal to that of [other].
///
/// Throws an error if the [unit] of this value and [other] do not match.
bool operator <=(CssValue other) {
_checkMatchingUnits(other);
return number <= other.number;
}
/// Returns whether this value's [number] and [unit] are equal to that of [other].
@override
bool operator ==(dynamic other) {
return identical(this, other) || (other is CssValue && number == other.number && unit == other.unit);
}
@override
int get hashCode => hash2(this.number, this.unit);
/// Returns whether this value's [number] is greater than that of [other].
///
/// Throws an error if the [unit] of this value and [other] do not match.
bool operator >(CssValue other) {
_checkMatchingUnits(other);
return number > other.number;
}
/// Returns whether this value's [number] is greater than or equal to that of [other].
///
/// Throws an error if the [unit] of this value and [other] do not match.
bool operator >=(CssValue other) {
_checkMatchingUnits(other);
return number >= other.number;
}
/// Returns a new [CssValue] with this value's [unit] and the negation of this value's [number].
CssValue operator -() {
return new CssValue(-number, unit);
}
/// Returns the result of comparing this value's [number] to that of [other].
///
/// Throws an error if the [unit] of this value and [other] do not match.
@override
int compareTo(CssValue other) {
_checkMatchingUnits(other);
return number.compareTo(other.number);
}
/// Returns the String representation of this value, which can be used as CSS style values.
///
/// If [number] is 0, then the [unit] is omitted.
@override
String toString() {
if (number == 0) return '0';
if (number == number.truncate()) return '${number.toStringAsFixed(0)}$unit';
return '$number$unit';
}
}