@enact/ui
Version:
A collection of simplified unstyled cross-platform UI components for Enact
692 lines (660 loc) • 25.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.calculateFontSize = calculateFontSize;
exports.config = void 0;
exports.defineScreenTypes = defineScreenTypes;
exports.getAspectRatio = getAspectRatio;
exports.getAspectRatioName = getAspectRatioName;
exports.getResolutionClasses = getResolutionClasses;
exports.getScreenType = getScreenType;
exports.getScreenTypeObject = getScreenTypeObject;
exports.init = init;
exports.linearScalingType = void 0;
exports.scale = scale;
exports.scaleToRem = void 0;
exports.selectSrc = selectSrc;
exports.unit = unit;
exports.unitToPixelFactors = void 0;
var baseScreen,
orientation,
riRatio,
screenType,
workspaceBounds = {
width: typeof window === 'object' ? window.innerWidth : 1920,
height: typeof window === 'object' ? window.innerHeight : 1080
},
screenTypes = [{
name: 'standard',
pxPerRem: 16,
width: workspaceBounds.width,
height: workspaceBounds.height,
aspectRatioName: 'standard',
base: true
}],
// Assign one sane type in case defineScreenTypes is never run.
screenTypeObject,
config;
/**
* Object that stores the pixel conversion factors to each keyed unit.
*
* @type {Object}
* @memberof ui/resolution
* @public
*/
var unitToPixelFactors = exports.unitToPixelFactors = {
'rem': 12,
'in': 96
};
/**
* Enumerates the available reporting types for linear scaling.
*
* @enum {String}
* @memberof ui/resolution
* @public
*/
var linearScalingType = exports.linearScalingType = {
baseScreen: 'baseScreen',
currentScreen: 'currentScreen'
};
/**
* Default configuration values.
*
* See {@link ui/resolution.config} for full documentation.
*
* @type {Object}
* @memberof ui/resolution
* @private
*/
var configDefaults = {
fontSizeHandling: 'scale',
orientationHandling: 'normal',
linearScaling: {
active: false,
type: linearScalingType.currentScreen
}
};
/**
* Calculates a linearly scaled size for 1rem based on the current workspace dimensions.
*
* The calculation determines a scaling factor using the geometric mean of horizontal
* and vertical scale factors relative to a reference screen (either the `baseScreen`
* or the currently matched `screenTypeObject`).
*
* The resulting size is rounded to one decimal place and constrained to a minimum
* of 1px and a maximum of the `pxPerRem` value defined for the largest screen type
* (the last element in `screenTypes`).
*
* @function
* @memberof ui/resolution
* @returns {Number} The calculated pixel size for 1rem, clamped between 1 and the
* maximum defined pxPerRem.
* @private
*/
function getLinearSize() {
var isCurrentScreen = config.linearScaling.type === linearScalingType.currentScreen;
var reportScreen = isCurrentScreen ? screenTypeObject : baseScreen;
var maxSize = screenTypes.at(-1).pxPerRem;
var scaleX = workspaceBounds.width / reportScreen.width;
var scaleY = workspaceBounds.height / reportScreen.height;
var ratio = Math.sqrt(scaleX * scaleY);
var size = Math.round(reportScreen.pxPerRem * ratio * 10) / 10;
return Math.max(1, Math.min(size, maxSize));
}
/**
* Calculates the scale factor and determines if scaling is necessary
* for the landscape workspace based on the screen type.
*
* @function
* @memberof ui/resolution
* @param {String} [type=screenType] The screen type identifier used to retrieve dimensions. Defaults to the global
* or scoped `screenType`.
* @returns {Object} Returns an object containing the scaling data.
* @private
*/
var getLandscapeScaleFactor = function getLandscapeScaleFactor() {
var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : screenType;
if (!type) return {
shouldScale: false,
factor: 1
};
var scrObj = getScreenTypeObject(type);
var shouldScale = workspaceBounds.width < scrObj.width || workspaceBounds.height < scrObj.height;
return {
shouldScale: shouldScale,
factor: workspaceBounds.height / scrObj.height
};
};
/**
* Get the closest resolution type based on the `resolution`.
*
* @function
* @memberOf ui/resolution
* @param {Object} resolution The resolution object (must include `height` and `width` properties).
* @param {Object[]} types The resolution types object (must include `name`, `width`, `height`, and `aspectRatioName` properties).
* @returns {String} Screen type (e.g., `'fhd'`, `'uhd'`, etc.).
* @private
*/
var getClosestResolutionType = function getClosestResolutionType(resolution, types) {
// Calculates the distance between two resolutions using their width and height as coordinates (x, y)
var getDistance = function getDistance(p1, p2) {
return Math.sqrt(Math.pow(p2.width - p1.width, 2) + Math.pow(p2.height - p1.height, 2));
};
// Compares the calculated distances and returns the closest resolution type name that matches current resolution
var _types$reduce = types.reduce(function (prev, curr) {
var distCurr = getDistance(curr, resolution);
var distPrev = getDistance(prev, resolution);
return distCurr < distPrev ? curr : prev;
}),
name = _types$reduce.name;
return name;
};
/**
* Update the common measured boundary object.
*
* This object is used as "what size screen are we looking at". Providing no arguments has no
* effect and updates nothing.
*
* @function
* @memberOf ui/resolution
* @param {Node} measurementNode A standard DOM node or the `window` node.
*
* @returns {undefined}
* @private
*/
var updateWorkspaceBounds = function updateWorkspaceBounds(measurementNode) {
if (measurementNode) {
if (measurementNode.clientHeight || measurementNode.clientWidth) {
workspaceBounds = {
height: measurementNode.clientHeight,
width: measurementNode.clientWidth
};
} else if (measurementNode.innerHeight || measurementNode.innerWidth) {
// A backup for if measurementNode is actually `window` and not a normal node
workspaceBounds = {
height: measurementNode.innerHeight,
width: measurementNode.innerWidth
};
}
}
};
/**
* Fetch the screenType object
*
* @function
* @memberof ui/resolution
* @param {String} type The key string for the screen type object. If falsy, the current
* screenType is used
*
* @returns {Object} screenTypeObject
* @private
*/
function getScreenTypeObject(type) {
type = type || screenType;
if (screenTypeObject && screenTypeObject.name === type) {
return screenTypeObject;
}
return screenTypes.filter(function (elem) {
return type === elem.name;
})[0];
}
/**
* Sets up screen resolution scaling capabilities by defining an array of all the screens
* being used.
*
* These should be listed in order from smallest to largest, according to
* width.
*
* The `name`, `pxPerRem`, `width`, and `aspectRatioName` properties are required for
* each screen type in the array. Setting `base: true` on a screen type marks it as the
* default resolution, upon which everything else will be based.
*
* Executing this method also initializes the rest of the resolution-independence code.
*
* Example:
* ```
* import ri from 'enact/ui/resolution';
*
* ri.defineScreenTypes([
* {name: 'vga', pxPerRem: 8, width: 640, height: 480, aspectRatioName: 'standard'},
* {name: 'xga', pxPerRem: 16, width: 1024, height: 768, aspectRatioName: 'standard'},
* {name: 'hd', pxPerRem: 16, width: 1280, height: 720, aspectRatioName: 'hdtv'},
* {name: 'uw-hd', pxPerRem: 16, width: 1920, height: 804, aspectRatioName: 'cinema'},
* {name: 'fhd', pxPerRem: 24, width: 1920, height: 1080, aspectRatioName: 'hdtv', base: true},
* {name: 'uw-uxga', pxPerRem: 24, width: 2560, height: 1080, aspectRatioName: 'cinema'},
* {name: 'qhd', pxPerRem: 32, width: 2560, height: 1440, aspectRatioName: 'hdtv'},
* {name: 'wqhd', pxPerRem: 32, width: 3440, height: 1440, aspectRatioName: 'cinema'},
* {name: 'uhd', pxPerRem: 48, width: 3840, height: 2160, aspectRatioName: 'hdtv'},
* {name: 'uhd2', pxPerRem: 96, width: 7680, height: 4320, aspectRatioName: 'hdtv'}
* ]);
* ```
*
* @function
* @memberof ui/resolution
* @param {Object[]} types An array of objects containing screen configuration data, as in the
* preceding example.
* @returns {undefined}
* @public
*/
function defineScreenTypes(types) {
if (types) screenTypes = types;
for (var i = 0; i < screenTypes.length; i++) {
if (screenTypes[i]['base']) baseScreen = screenTypes[i];
}
init();
}
/**
* Fetches the name of the screen type that best matches the current screen size.
*
* The best match is defined as the screen type that is the closest to the screen resolution without
* going over. ("The Price is Right" style.)
*
* @function
* @memberof ui/resolution
* @param {Object} rez Optional measurement scheme. Must include `height` and `width` properties.
* @returns {String} Screen type (e.g., `'fhd'`, `'uhd'`, etc.)
* @public
*/
function getScreenType(rez) {
rez = rez || workspaceBounds;
var types = screenTypes;
orientation = 'landscape';
if (rez.height > rez.width) {
orientation = 'portrait';
var swap = rez.width;
rez.width = rez.height;
rez.height = swap;
}
// Loop through resolutions, last->first, largest->smallest and return in case of exact match.
for (var i = types.length - 1; i >= 0; i--) {
if (rez.height === types[i].height && rez.width === types[i].width) {
return types[i].name;
}
}
// Return the name of the closest fitting set of dimensions.
return getClosestResolutionType(rez, types);
}
/**
* Calculate the base rem font size.
*
* This is how the magic happens. This accepts an optional `screenType` name. If one isn't provided,
* the currently detected screen type is used. This uses the config option `orientationHandling`,
* which when set to "scale" and the screen is in portrait orientation (such as after screen rotation), will dynamically calculate
* what the base font size should be, if the width were proportionally scaled down to fit in the portrait space.
*
* To use, put the following in your application code:
* ```
* import ri from '@enact/ui/resolution';
*
* ri.config.orientationHandling = 'scale';
* ri.init();
* ```
*
* This configuration is particularly useful for applications that support screen rotation and need
* to maintain consistent scaling when the device orientation changes from landscape to portrait or vice versa.
* This has no effect if the screen is in landscape, or if `orientationHandling` is unset.
*
* @function
* @memberof ui/resolution
* @param {String} type Screen type to base size the calculation on. If no
* screen type is provided, the current screen type will be used.
* @returns {String} The calculated pixel size (with unit suffix. Ex: "24px").
* @public
*/
function calculateFontSize(type) {
// If linear scaling is enabled, bypass standard screen-type-based calculation and use the dynamic linear size
if (config.linearScaling.active && !type) {
return getLinearSize() + 'px';
}
var scrObj = getScreenTypeObject(type);
var shouldScaleFontSize = config.fontSizeHandling === 'scale' && (workspaceBounds.width < scrObj.width || workspaceBounds.height < scrObj.height);
var size;
if (orientation === 'portrait' && config.orientationHandling === 'scale') {
size = scrObj.height / scrObj.width * scrObj.pxPerRem;
} else {
size = scrObj.pxPerRem;
if (orientation === 'landscape' && shouldScaleFontSize) {
size = workspaceBounds.height * scrObj.pxPerRem / scrObj.height;
}
}
return size + 'px';
}
/**
* @function
* @memberof ui/resolution
* @param {String} size A valid CSS measurement to be applied as the base document font size.
* @returns {undefined}
* @private
*/
function updateBaseFontSize(size) {
if (typeof window === 'object') {
document.documentElement.style.fontSize = size;
}
}
/**
* Returns the CSS classes for the given `type`.
*
* This function generates CSS class names that can be used to style components based on
* the current screen resolution, orientation, and aspect ratio. The returned classes are
* particularly useful for responsive layouts and handling screen rotation scenarios.
*
* The following CSS classes are returned:
* - **Orientation class**: `enact-orientation-landscape` or `enact-orientation-portrait`
* - Applied based on the current screen orientation
* - Updates automatically when device rotation occurs (if dynamic mode is enabled)
* - **Resolution class**: `enact-res-{screentype}`
* - Examples: `enact-res-fhd`, `enact-res-uhd`, `enact-res-hd`, `enact-res-qhd`
* - Applied based on the matched screen type
* - **Aspect ratio class**: `enact-aspect-ratio-{aspectRatioName}`
* - Examples: `enact-aspect-ratio-hdtv`, `enact-aspect-ratio-cinema`, `enact-aspect-ratio-standard`
* - Applied based on the screen type's aspect ratio name
*
* Example return value: `'enact-orientation-landscape enact-res-fhd enact-aspect-ratio-hdtv'`
*
* @function
* @memberof ui/resolution
* @param {String} type Screen type
* @returns {String} CSS class names
* @public
*/
function getResolutionClasses() {
var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : screenType;
var classes = [];
if (orientation) {
classes.push('enact-orientation-' + orientation);
}
if (type) {
classes.push('enact-res-' + type.toLowerCase());
var scrObj = getScreenTypeObject(type);
if (scrObj.aspectRatioName) {
classes.push('enact-aspect-ratio-' + scrObj.aspectRatioName.toLowerCase());
}
}
return classes.join(' ');
}
/**
* Returns the ratio of pixels per rem for the given `type` to the pixels per rem for the base type.
*
* @function
* @memberof ui/resolution
* @param {String} type Screen type
*
* @returns {Number} ratio
*/
function getRiRatio() {
var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : screenType;
if (type && baseScreen) {
var ratio = getUnitToPixelFactors(type) / getUnitToPixelFactors(baseScreen.name);
if (orientation === 'landscape' && config.fontSizeHandling === 'scale') {
var _getLandscapeScaleFac = getLandscapeScaleFactor(type),
shouldScale = _getLandscapeScaleFac.shouldScale,
factor = _getLandscapeScaleFac.factor;
if (shouldScale) {
ratio *= factor;
}
}
if (type === screenType) {
// cache this if it's for our current screen type.
riRatio = ratio;
}
return ratio;
}
return 1;
}
/**
* Returns the pixels per rem for the given `type`.
*
* @memberof ui/resolution
* @param {String} type Screen type
*
* @returns {Number} pixels per rem
*/
function getUnitToPixelFactors() {
var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : screenType;
// Use linear scaling for the current screen type if active.
if (config.linearScaling.active && (!type || type === screenType)) {
return getLinearSize();
}
if (type) {
return getScreenTypeObject(type).pxPerRem;
}
return 1;
}
/**
* Calculates the aspect ratio of the specified screen type.
*
* If no screen type is provided, the current screen type is used.
*
* @function
* @memberof ui/resolution
* @param {String} type Screen type whose aspect ratio will be calculated. If no screen
* type is provided, the current screen type is used.
* @returns {Number} The calculated screen ratio (e.g., `1.333`, `1.777`, `2.333`, etc.)
* @public
*/
function getAspectRatio(type) {
var scrObj = getScreenTypeObject(type);
if (scrObj.width && scrObj.height) {
return scrObj.width / scrObj.height;
}
return 1;
}
/**
* Returns the name of the aspect ratio for a specified screen type, or for the default
* screen type if none is provided.
*
* @function
* @memberof ui/resolution
* @param {String} type Screen type whose aspect ratio name will be returned. If no
* screen type is provided, the current screen type will be used.
* @returns {String} The name of the screen type's aspect ratio
* @public
*/
function getAspectRatioName(type) {
var scrObj = getScreenTypeObject(type);
return scrObj.aspectRatioName || 'standard';
}
/**
* Takes a provided pixel value and performs a scaling operation based on the current
* screen type.
*
* @function
* @memberof ui/resolution
* @param {Number} px The quantity of standard-resolution pixels to scale to the
* current screen resolution.
* @returns {Number} The scaled value based on the current screen scaling factor
* @public
*/
function scale(px) {
return (riRatio || getRiRatio()) * px;
}
/**
* Convert to various unit formats.
*
* Useful for converting pixels to a resolution-independent
* measurement method, like "rem". Other units are available if defined in the
* {@link ui/resolution.unitToPixelFactors} object.
*
* Example:
* ```
* import ri from '@enact/ui/resolution';
*
* // Do calculations and get back the desired CSS unit.
* var frameWidth = 250,
* frameWithMarginInches = ri.unit( 10 + frameWidth + 10, 'in' ), // '2.8125in' == frameWithMarginInches
* frameWithMarginRems = ri.unit( 10 + frameWidth + 10, 'rem' ); // '22.5rem' == frameWithMarginRems
* ```
*
* @function
* @memberof ui/resolution
* @param {String|Number} pixels The pixels or math to convert to the unit ("px" suffix in String
* format is permitted. ex: `'20px'`)
* @param {String} toUnit The name of the unit to convert to.
*
* @returns {String|undefined} Resulting conversion in CSS safe format, in case of malformed input, `undefined`
* @public
*/
function unit(pixels, toUnit) {
if (!toUnit || !unitToPixelFactors[toUnit]) return;
if (typeof pixels === 'string' && pixels.substring(-2) === 'px') pixels = parseInt(pixels.substring(0, pixels.length - 2));
if (typeof pixels !== 'number') return;
if (screenType && orientation === 'landscape' && config.fontSizeHandling === 'scale') {
var _getLandscapeScaleFac2 = getLandscapeScaleFactor(),
shouldScale = _getLandscapeScaleFac2.shouldScale,
factor = _getLandscapeScaleFac2.factor;
if (shouldScale) {
return pixels / factor / unitToPixelFactors[toUnit] + '' + toUnit;
}
}
return pixels / unitToPixelFactors[toUnit] + '' + toUnit;
}
/**
* Shorthand for when you know you need to scale some pixel value and have it converted to "rem" for
* proper scaling.
*
* This runs {@link ui/resolution.scale} and {@link ui/resolution.unit} together.
*
* @function
* @memberof ui/resolution
* @param {Number} pixels The quantity of standard-resolution pixels to scale to rems
*
* @returns {String|undefined} Resulting conversion or, in case of malformed input, `undefined`
* @public
*/
var scaleToRem = exports.scaleToRem = function scaleToRem(pixels) {
return unit(scale(pixels), 'rem');
};
/**
* The default configurable options for {@link ui/resolution.selectSrc}. Additional resolutions
* may be added.
*
* @typedef {Object} selectSrcOptions
* @memberof ui/resolution
* @property {String} [hd] HD / 720p Resolution image asset source URI/URL
* @property {String} [fhd] FHD / 1080p Resolution image asset source URI/URL
* @property {String} [uhd] UHD / 4K Resolution image asset source URI/URL
*/
/**
* Selects the ideal image asset from a set of assets, based on various screen
* resolutions: HD (720p), FHD (1080p), UHD (4k).
*
* When a `src` argument is provided, `selectSrc()` will choose the best image with
* respect to the current screen resolution. `src` may be either the traditional
* string, which will pass straight through, or a hash/object of screen types and
* their asset sources (keys:screen and values:src). The image sources will be used
* when the screen resolution is less than or equal to the provided screen types.
*
* Example:
* ```
* // Take advantage of the multi-res mode
* import {Image} from '@enact/ui/Image';
*
* const src = {
* 'hd': 'http://lorempixel.com/64/64/city/1/',
* 'fhd': 'http://lorempixel.com/128/128/city/1/',
* 'uhd': 'http://lorempixel.com/256/256/city/1/'
* };
* ...
* <Image src={src} ... />
* ...
* ```
*
* @function
* @memberof ui/resolution
* @param {String|ui/resolution.selectSrcSrcOptions} src A string containing a single image
* source or a key/value hash/object
* containing keys representing screen
* types (`'hd'`, `'fhd'`, `'uhd'`,
* etc.) and values containing the asset
* source for that target screen
* resolution.
*
* @returns {String} The chosen source, given the string
* or hash provided
* @public
*/
function selectSrc(src) {
if (typeof src != 'string' && src) {
var newSrc = src.fhd || src.uhd || src.hd;
var types = screenTypes;
// loop through resolutions
for (var i = types.length - 1; i >= 0; i--) {
var t = types[i].name;
// return exact match
if (screenType === t && src[t]) return src[t];
}
var srcResolutions = types.filter(function (type) {
return type.name in src;
});
var currentScreenTypeResolution = getScreenTypeObject();
if (srcResolutions.length && currentScreenTypeResolution) {
var closestType = getClosestResolutionType(currentScreenTypeResolution, srcResolutions);
newSrc = src[closestType];
}
src = newSrc;
}
return src;
}
/**
* This will need to be re-run any time the screen size changes, so all the values can be re-cached.
*
* @function
* @memberof ui/resolution
* @param {Object} args A hash of options. The key `measurementNode` is used to as the node,
* typically the root element, to measure and use as the dimensions for
* the `screenType`.
*
* @returns {undefined}
* @public
*/
function init() {
var args = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var measurementNode = args.measurementNode;
updateWorkspaceBounds(measurementNode);
screenType = getScreenType();
screenTypeObject = getScreenTypeObject();
unitToPixelFactors.rem = getUnitToPixelFactors();
riRatio = getRiRatio();
updateBaseFontSize(calculateFontSize());
}
/**
* @typedef {Object} LinearScalingConfig
* @memberof ui/resolution
* @property {Boolean} active - Enables linear scaling calculation for
* font sizes and unit conversions. Default: `true`.
* @property {ui/resolution.linearScalingType} type - Determines the
* reference screen used for linear scaling calculation.
* Default: `linearScalingType.currentScreen`.
*/
/**
* Configuration options for resolution independence behavior.
*
* @typedef {Object} ResolutionConfig
* @memberof ui/resolution
* @property {('normal'|'scale')} fontSizeHandling - Determines how to calculate
* font-size. When set to `'scale'` and the screen is in landscape orientation,
* calculates font-size linearly based on screen resolution. When set to `'normal'`,
* the font-size will be the pxPerRem value of the best match screen type.
* Default: `'scale'`
* @property {('normal'|'scale')} orientationHandling - Determines how to handle screen
* orientation and rotation. When set to `'scale'` and the screen is in portrait
* orientation (due to screen rotation), dynamically calculates the base font size
* based on proportional scaling. When set to `'normal'`, uses the standard pxPerRem
* value. This is particularly useful for supporting device rotation scenarios.
* Default: `'normal'`
* @property {LinearScalingConfig} linearScaling - Configuration for linear scaling mode.
* @public
*/
/**
* Configuration object for resolution independence behavior.
*
* This object controls how the resolution independence system handles different screen sizes,
* orientations, and screen rotation scenarios.
* See the details in {@link ui/resolution.ResolutionConfig|ResolutionConfig}
*
* @type {ui/resolution.ResolutionConfig}
* @memberof ui/resolution
* @public
*/
exports.config = config = Object.assign({}, configDefaults);