// 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.
/// Utilities for working with CSS `rem` units and detecting changes to the root font size.
library over_react.rem_util;
import 'dart:async';
import 'dart:html';
import 'package:over_react/over_react.dart';
import 'package:over_react/src/util/css_value_util.dart';
import 'package:react/react_dom.dart' as react_dom;
double _computeRootFontSize() {
return new CssValue.parse(document.documentElement.getComputedStyle().fontSize).number.toDouble();
}
double _rootFontSize = _computeRootFontSize();
var _changeSensor;
void _initRemChangeSensor() {
if (_changeSensor != null) return;
// Force lazy-initialization of this variable if it hasn't happened already.
_rootFontSize;
var mountNode = new DivElement()
..id = 'rem_change_sensor';
// Ensure the sensor doesn't interfere with the rest of the page.
mountNode.style
..width = '0'
..height = '0'
..position = 'absolute'
..zIndex = '-1';
document.body.append(mountNode);
_changeSensor = react_dom.render((Dom.div()
..style = const {
'position': 'absolute',
'visibility': 'hidden',
// ResizeSensor doesn't pick up sub-pixel changes due to its use of offsetWidth/Height,
// so use 100rem for greater precision.
'width': '100rem',
'height': '100rem',
}
)(
(ResizeSensor()..onResize = (ResizeSensorEvent e) {
recomputeRootFontSize();
})()
), mountNode);
}
final StreamController<double> _remChange = new StreamController.broadcast(onListen: () {
_initRemChangeSensor();
});
/// The latest component root font size (rem) value, in pixels.
double get rootFontSize => _rootFontSize;
/// A Stream of changes to the root font size (rem) value.
///
/// Stream data is the latest value, in pixels.
final Stream<double> onRemChange = _remChange.stream;
/// Forces re-computation of the root font size. Not necessary when using [onRemChange].
void recomputeRootFontSize() {
var latestRootFontSize = _computeRootFontSize();
if (latestRootFontSize != _rootFontSize) {
_rootFontSize = latestRootFontSize;
_remChange.add(_rootFontSize);
}
}
/// Converts a pixel (`px`) [value] to its `rem` equivalent using the current root font size.
///
/// * If [value] is a [String] or [CssValue]:
/// * And [value] already has the correct unit, it will not be converted.
/// * And [CssValue.unit] is not `'px'` or `'rem'`, an error will be thrown unless
/// [passThroughUnsupportedUnits] is `true`, in which case no conversion will take place.
/// * If [value] is a [num], it will be treated as a `px` and converted, unless [treatNumAsRem] is `true`.
/// * If [value] is `null`, `null` will be returned.
///
/// Example input:
///
/// * `'15px'`
/// * `new CssValue(15, 'px')`
/// * `15`
/// * `1.5, treatNumAsRem: true`
/// * `'1.5rem'`
/// * `new CssValue(1.5, 'rem')`
///
/// Example output (assuming 1rem = 10px):
///
/// * `1.5rem`
CssValue toRem(dynamic value, {bool treatNumAsRem: false, bool passThroughUnsupportedUnits: false}) {
if (value == null) return null;
num remValueNum;
if (value is num) {
remValueNum = treatNumAsRem ? value : value / rootFontSize;
} else {
var parsedValue = value is CssValue ? value : new CssValue.parse(value);
if (parsedValue?.unit == 'rem') {
remValueNum = parsedValue.number;
} else if (parsedValue?.unit == 'px') {
remValueNum = parsedValue.number / rootFontSize;
} else {
if (passThroughUnsupportedUnits) return parsedValue;
throw new ArgumentError.value(value, 'value', 'must be a px num or a String px/rem value');
}
}
return new CssValue(remValueNum, 'rem');
}
/// Converts a rem [value] to its pixel (`px`) equivalent using the current root font size.
///
/// * If [value] is a [String] or [CssValue]:
/// * And [value] already has the correct unit, it will not be converted.
/// * And [CssValue.unit] is not `'px'` or `'rem'`, an error will be thrown unless
/// [passThroughUnsupportedUnits] is `true`, in which case no conversion will take place.
/// * If [value] is a [num], it will be treated as a `rem` and converted, unless [treatNumAsPx] is `true`.
/// * If [value] is `null`, `null` will be returned.
///
/// Example input:
///
/// * `'1.5rem'`
/// * `new CssValue(1.5, 'rem')`
/// * `1.5`
/// * `15, treatNumAsPx: true`
/// * `15px`
/// * `new CssValue(15, 'px')`
///
/// Example output (assuming 1rem = 10px):
///
/// * `15px`
CssValue toPx(dynamic value, {bool treatNumAsPx: false, bool passThroughUnsupportedUnits: false}) {
if (value == null) return null;
num pxValueNum;
if (value is num) {
pxValueNum = treatNumAsPx ? value : value * rootFontSize;
} else {
var parsedValue = value is CssValue ? value : new CssValue.parse(value);
if (parsedValue?.unit == 'px') {
pxValueNum = parsedValue.number;
} else if (parsedValue?.unit == 'rem') {
pxValueNum = parsedValue.number * rootFontSize;
} else {
if (passThroughUnsupportedUnits) return parsedValue;
throw new ArgumentError.value(value, 'value', 'must be a rem num or a String px/rem value');
}
}
return new CssValue(pxValueNum, 'px');
}