rtl-css-js
Version:
Right To Left conversion for CSS in JS objects
509 lines (484 loc) • 22.2 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.RtlCssJs = factory());
}(this, (function () { 'use strict';
/**
* Takes an array of [keyValue1, keyValue2] pairs and creates an object of {keyValue1: keyValue2, keyValue2: keyValue1}
* @param {Array} array the array of pairs
* @return {Object} the {key, value} pair object
*/
function arrayToObject(array) {
return array.reduce(function (obj, _ref) {
var prop1 = _ref[0],
prop2 = _ref[1];
obj[prop1] = prop2;
obj[prop2] = prop1;
return obj;
}, {});
}
function isBoolean(val) {
return typeof val === 'boolean';
}
function isFunction(val) {
return typeof val === 'function';
}
function isNumber(val) {
return typeof val === 'number';
}
function isNullOrUndefined(val) {
return val === null || typeof val === 'undefined';
}
function isObject(val) {
return val && typeof val === 'object';
}
function isString(val) {
return typeof val === 'string';
}
function includes(inclusive, inclusee) {
return inclusive.indexOf(inclusee) !== -1;
}
/**
* Flip the sign of a CSS value, possibly with a unit.
*
* We can't just negate the value with unary minus due to the units.
*
* @private
* @param {String} value - the original value (for example 77%)
* @return {String} the result (for example -77%)
*/
function flipSign(value) {
if (parseFloat(value) === 0) {
// Don't mangle zeroes
return value;
}
if (value[0] === '-') {
return value.slice(1);
}
return "-" + value;
}
function flipTransformSign(match, prefix, offset, suffix) {
return prefix + flipSign(offset) + suffix;
}
/**
* Takes a percentage for background position and inverts it.
* This was copied and modified from CSSJanus:
* https://github.com/cssjanus/cssjanus/blob/4245f834365f6cfb0239191a151432fb85abab23/src/cssjanus.js#L152-L175
* @param {String} value - the original value (for example 77%)
* @return {String} the result (for example 23%)
*/
function calculateNewBackgroundPosition(value) {
var idx = value.indexOf('.');
if (idx === -1) {
value = 100 - parseFloat(value) + "%";
} else {
// Two off, one for the "%" at the end, one for the dot itself
var len = value.length - idx - 2;
value = 100 - parseFloat(value);
value = value.toFixed(len) + "%";
}
return value;
}
/**
* This takes a list of CSS values and converts it to an array
* @param {String} value - something like `1px`, `1px 2em`, or `3pt rgb(150, 230, 550) 40px calc(100% - 5px)`
* @return {Array} the split values (for example: `['3pt', 'rgb(150, 230, 550)', '40px', 'calc(100% - 5px)']`)
*/
function getValuesAsList(value) {
return value.replace(/ +/g, ' ') // remove all extraneous spaces
.split(' ').map(function (i) {
return i.trim();
}) // get rid of extra space before/after each item
.filter(Boolean) // get rid of empty strings
// join items which are within parenthese
// luckily `calc (100% - 5px)` is invalid syntax and it must be `calc(100% - 5px)`, otherwise this would be even more complex
.reduce(function (_ref2, item) {
var list = _ref2.list,
state = _ref2.state;
var openParansCount = (item.match(/\(/g) || []).length;
var closedParansCount = (item.match(/\)/g) || []).length;
if (state.parensDepth > 0) {
list[list.length - 1] = list[list.length - 1] + " " + item;
} else {
list.push(item);
}
state.parensDepth += openParansCount - closedParansCount;
return {
list: list,
state: state
};
}, {
list: [],
state: {
parensDepth: 0
}
}).list;
}
/**
* This is intended for properties that are `top right bottom left` and will switch them to `top left bottom right`
* @param {String} value - `1px 2px 3px 4px` for example, but also handles cases where there are too few/too many and
* simply returns the value in those cases (which is the correct behavior)
* @return {String} the result - `1px 4px 3px 2px` for example.
*/
function handleQuartetValues(value) {
var splitValues = getValuesAsList(value);
if (splitValues.length <= 3 || splitValues.length > 4) {
return value;
}
var top = splitValues[0],
right = splitValues[1],
bottom = splitValues[2],
left = splitValues[3];
return [top, left, bottom, right].join(' ');
}
/**
*
* @param {String|Number|Object} value css property value to test
* @returns If the css property value can(should?) have an RTL equivalent
*/
function canConvertValue(value) {
return !isBoolean(value) && !isNullOrUndefined(value);
}
/**
* Splits a shadow style into its separate shadows using the comma delimiter, but creating an exception
* for comma separated values in parentheses often used for rgba colours.
* @param {String} value
* @returns {Array} array of all box shadow values in the string
*/
function splitShadow(value) {
var shadows = [];
var start = 0;
var end = 0;
var rgba = false;
while (end < value.length) {
if (!rgba && value[end] === ',') {
shadows.push(value.substring(start, end).trim());
end++;
start = end;
} else if (value[end] === "(") {
rgba = true;
end++;
} else if (value[end] === ')') {
rgba = false;
end++;
} else {
end++;
}
}
// push the last shadow value if there is one
// istanbul ignore next
if (start != end) {
shadows.push(value.substring(start, end + 1));
}
return shadows;
}
// some values require a little fudging, that fudging goes here.
var propertyValueConverters = {
padding: function padding(_ref) {
var value = _ref.value;
if (isNumber(value)) {
return value;
}
return handleQuartetValues(value);
},
textShadow: function textShadow(_ref2) {
var value = _ref2.value;
var flippedShadows = splitShadow(value).map(function (shadow) {
// intentionally leaving off the `g` flag here because we only want to change the first number (which is the offset-x)
return shadow.replace(/(^|\s)(-*)([.|\d]+)/, function (match, whiteSpace, negative, number) {
if (number === '0') {
return match;
}
var doubleNegative = negative === '' ? '-' : '';
return "" + whiteSpace + doubleNegative + number;
});
});
return flippedShadows.join(',');
},
borderColor: function borderColor(_ref3) {
var value = _ref3.value;
return handleQuartetValues(value);
},
borderRadius: function borderRadius(_ref4) {
var value = _ref4.value;
if (isNumber(value)) {
return value;
}
if (includes(value, '/')) {
var _value$split = value.split('/'),
radius1 = _value$split[0],
radius2 = _value$split[1];
var convertedRadius1 = propertyValueConverters.borderRadius({
value: radius1.trim()
});
var convertedRadius2 = propertyValueConverters.borderRadius({
value: radius2.trim()
});
return convertedRadius1 + " / " + convertedRadius2;
}
var splitValues = getValuesAsList(value);
switch (splitValues.length) {
case 2:
{
return splitValues.reverse().join(' ');
}
case 4:
{
var topLeft = splitValues[0],
topRight = splitValues[1],
bottomRight = splitValues[2],
bottomLeft = splitValues[3];
return [topRight, topLeft, bottomLeft, bottomRight].join(' ');
}
default:
{
return value;
}
}
},
background: function background(_ref5) {
var value = _ref5.value,
valuesToConvert = _ref5.valuesToConvert,
isRtl = _ref5.isRtl,
bgImgDirectionRegex = _ref5.bgImgDirectionRegex,
bgPosDirectionRegex = _ref5.bgPosDirectionRegex;
if (isNumber(value)) {
return value;
}
// Yeah, this is in need of a refactor 🙃...
// but this property is a tough cookie 🍪
// get the backgroundPosition out of the string by removing everything that couldn't be the backgroundPosition value
var backgroundPositionValue = value.replace(/(url\(.*?\))|(rgba?\(.*?\))|(hsl\(.*?\))|(#[a-fA-F0-9]+)|((^| )(\D)+( |$))/g, '').trim();
// replace that backgroundPosition value with the converted version
value = value.replace(backgroundPositionValue, propertyValueConverters.backgroundPosition({
value: backgroundPositionValue,
valuesToConvert: valuesToConvert,
isRtl: isRtl,
bgPosDirectionRegex: bgPosDirectionRegex
}));
// do the backgroundImage value replacing on the whole value (because why not?)
return propertyValueConverters.backgroundImage({
value: value,
valuesToConvert: valuesToConvert,
bgImgDirectionRegex: bgImgDirectionRegex
});
},
backgroundImage: function backgroundImage(_ref6) {
var value = _ref6.value,
valuesToConvert = _ref6.valuesToConvert,
bgImgDirectionRegex = _ref6.bgImgDirectionRegex;
if (!includes(value, 'url(') && !includes(value, 'linear-gradient(')) {
return value;
}
return value.replace(bgImgDirectionRegex, function (match, g1, group2) {
return match.replace(group2, valuesToConvert[group2]);
});
},
backgroundPosition: function backgroundPosition(_ref7) {
var value = _ref7.value,
valuesToConvert = _ref7.valuesToConvert,
isRtl = _ref7.isRtl,
bgPosDirectionRegex = _ref7.bgPosDirectionRegex;
return value
// intentionally only grabbing the first instance of this because that represents `left`
.replace(isRtl ? /^((-|\d|\.)+%)/ : null, function (match, group) {
return calculateNewBackgroundPosition(group);
}).replace(bgPosDirectionRegex, function (match) {
return valuesToConvert[match];
});
},
backgroundPositionX: function backgroundPositionX(_ref8) {
var value = _ref8.value,
valuesToConvert = _ref8.valuesToConvert,
isRtl = _ref8.isRtl,
bgPosDirectionRegex = _ref8.bgPosDirectionRegex;
if (isNumber(value)) {
return value;
}
return propertyValueConverters.backgroundPosition({
value: value,
valuesToConvert: valuesToConvert,
isRtl: isRtl,
bgPosDirectionRegex: bgPosDirectionRegex
});
},
transition: function transition(_ref9) {
var value = _ref9.value,
propertiesToConvert = _ref9.propertiesToConvert;
return value.split(/,\s*/g).map(function (transition) {
var values = transition.split(' ');
// Property is always defined first
values[0] = propertiesToConvert[values[0]] || values[0];
return values.join(' ');
}).join(', ');
},
transitionProperty: function transitionProperty(_ref10) {
var value = _ref10.value,
propertiesToConvert = _ref10.propertiesToConvert;
return value.split(/,\s*/g).map(function (prop) {
return propertiesToConvert[prop] || prop;
}).join(', ');
},
transform: function transform(_ref11) {
var value = _ref11.value;
// This was copied and modified from CSSJanus:
// https://github.com/cssjanus/cssjanus/blob/4a40f001b1ba35567112d8b8e1d9d95eda4234c3/src/cssjanus.js#L152-L153
var nonAsciiPattern = "[^\\u0020-\\u007e]";
var escapePattern = "(?:" + '(?:(?:\\[0-9a-f]{1,6})(?:\\r\\n|\\s)?)' + "|\\\\[^\\r\\n\\f0-9a-f])";
var signedQuantPattern = "((?:-?" + ('(?:[0-9]*\\.[0-9]+|[0-9]+)' + "(?:\\s*" + '(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)' + "|" + ("-?" + ("(?:[_a-z]|" + nonAsciiPattern + "|" + escapePattern + ")") + ("(?:[_a-z0-9-]|" + nonAsciiPattern + "|" + escapePattern + ")") + "*") + ")?") + ")|(?:inherit|auto))";
var translateXRegExp = new RegExp("(translateX\\s*\\(\\s*)" + signedQuantPattern + "(\\s*\\))", 'gi');
var translateRegExp = new RegExp("(translate\\s*\\(\\s*)" + signedQuantPattern + "((?:\\s*,\\s*" + signedQuantPattern + "){0,1}\\s*\\))", 'gi');
var translate3dRegExp = new RegExp("(translate3d\\s*\\(\\s*)" + signedQuantPattern + "((?:\\s*,\\s*" + signedQuantPattern + "){0,2}\\s*\\))", 'gi');
var rotateRegExp = new RegExp("(rotate[ZY]?\\s*\\(\\s*)" + signedQuantPattern + "(\\s*\\))", 'gi');
return value.replace(translateXRegExp, flipTransformSign).replace(translateRegExp, flipTransformSign).replace(translate3dRegExp, flipTransformSign).replace(rotateRegExp, flipTransformSign);
}
};
propertyValueConverters.objectPosition = propertyValueConverters.backgroundPosition;
propertyValueConverters.margin = propertyValueConverters.padding;
propertyValueConverters.borderWidth = propertyValueConverters.padding;
propertyValueConverters.boxShadow = propertyValueConverters.textShadow;
propertyValueConverters.webkitBoxShadow = propertyValueConverters.boxShadow;
propertyValueConverters.mozBoxShadow = propertyValueConverters.boxShadow;
propertyValueConverters.WebkitBoxShadow = propertyValueConverters.boxShadow;
propertyValueConverters.MozBoxShadow = propertyValueConverters.boxShadow;
propertyValueConverters.borderStyle = propertyValueConverters.borderColor;
propertyValueConverters.webkitTransform = propertyValueConverters.transform;
propertyValueConverters.mozTransform = propertyValueConverters.transform;
propertyValueConverters.WebkitTransform = propertyValueConverters.transform;
propertyValueConverters.MozTransform = propertyValueConverters.transform;
propertyValueConverters.transformOrigin = propertyValueConverters.backgroundPosition;
propertyValueConverters.webkitTransformOrigin = propertyValueConverters.transformOrigin;
propertyValueConverters.mozTransformOrigin = propertyValueConverters.transformOrigin;
propertyValueConverters.WebkitTransformOrigin = propertyValueConverters.transformOrigin;
propertyValueConverters.MozTransformOrigin = propertyValueConverters.transformOrigin;
propertyValueConverters.webkitTransition = propertyValueConverters.transition;
propertyValueConverters.mozTransition = propertyValueConverters.transition;
propertyValueConverters.WebkitTransition = propertyValueConverters.transition;
propertyValueConverters.MozTransition = propertyValueConverters.transition;
propertyValueConverters.webkitTransitionProperty = propertyValueConverters.transitionProperty;
propertyValueConverters.mozTransitionProperty = propertyValueConverters.transitionProperty;
propertyValueConverters.WebkitTransitionProperty = propertyValueConverters.transitionProperty;
propertyValueConverters.MozTransitionProperty = propertyValueConverters.transitionProperty;
// kebab-case versions
propertyValueConverters['text-shadow'] = propertyValueConverters.textShadow;
propertyValueConverters['border-color'] = propertyValueConverters.borderColor;
propertyValueConverters['border-radius'] = propertyValueConverters.borderRadius;
propertyValueConverters['background-image'] = propertyValueConverters.backgroundImage;
propertyValueConverters['background-position'] = propertyValueConverters.backgroundPosition;
propertyValueConverters['background-position-x'] = propertyValueConverters.backgroundPositionX;
propertyValueConverters['object-position'] = propertyValueConverters.objectPosition;
propertyValueConverters['border-width'] = propertyValueConverters.padding;
propertyValueConverters['box-shadow'] = propertyValueConverters.textShadow;
propertyValueConverters['-webkit-box-shadow'] = propertyValueConverters.textShadow;
propertyValueConverters['-moz-box-shadow'] = propertyValueConverters.textShadow;
propertyValueConverters['border-style'] = propertyValueConverters.borderColor;
propertyValueConverters['-webkit-transform'] = propertyValueConverters.transform;
propertyValueConverters['-moz-transform'] = propertyValueConverters.transform;
propertyValueConverters['transform-origin'] = propertyValueConverters.transformOrigin;
propertyValueConverters['-webkit-transform-origin'] = propertyValueConverters.transformOrigin;
propertyValueConverters['-moz-transform-origin'] = propertyValueConverters.transformOrigin;
propertyValueConverters['-webkit-transition'] = propertyValueConverters.transition;
propertyValueConverters['-moz-transition'] = propertyValueConverters.transition;
propertyValueConverters['transition-property'] = propertyValueConverters.transitionProperty;
propertyValueConverters['-webkit-transition-property'] = propertyValueConverters.transitionProperty;
propertyValueConverters['-moz-transition-property'] = propertyValueConverters.transitionProperty;
// this will be an object of properties that map to their corresponding rtl property (their doppelganger)
var propertiesToConvert = arrayToObject([['paddingLeft', 'paddingRight'], ['marginLeft', 'marginRight'], ['left', 'right'], ['borderLeft', 'borderRight'], ['borderLeftColor', 'borderRightColor'], ['borderLeftStyle', 'borderRightStyle'], ['borderLeftWidth', 'borderRightWidth'], ['borderTopLeftRadius', 'borderTopRightRadius'], ['borderBottomLeftRadius', 'borderBottomRightRadius'],
// kebab-case versions
['padding-left', 'padding-right'], ['margin-left', 'margin-right'], ['border-left', 'border-right'], ['border-left-color', 'border-right-color'], ['border-left-style', 'border-right-style'], ['border-left-width', 'border-right-width'], ['border-top-left-radius', 'border-top-right-radius'], ['border-bottom-left-radius', 'border-bottom-right-radius']]);
var propsToIgnore = ['content'];
// this is the same as the propertiesToConvert except for values
var valuesToConvert = arrayToObject([['ltr', 'rtl'], ['left', 'right'], ['w-resize', 'e-resize'], ['sw-resize', 'se-resize'], ['nw-resize', 'ne-resize']]);
// Sorry for the regex 😞, but basically thisis used to replace _every_ instance of
// `ltr`, `rtl`, `right`, and `left` in `backgroundimage` with the corresponding opposite.
// A situation we're accepting here:
// url('/left/right/rtl/ltr.png') will be changed to url('/right/left/ltr/rtl.png')
// Definite trade-offs here, but I think it's a good call.
var bgImgDirectionRegex = new RegExp('(^|\\W|_)((ltr)|(rtl)|(left)|(right))(\\W|_|$)', 'g');
var bgPosDirectionRegex = new RegExp('(left)|(right)');
/**
* converts properties and values in the CSS in JS object to their corresponding RTL values
* @param {Object} object the CSS in JS object
* @return {Object} the RTL converted object
*/
function convert(object) {
return Object.keys(object).reduce(function (newObj, originalKey) {
var originalValue = object[originalKey];
if (isString(originalValue)) {
// you're welcome to later code 😺
originalValue = originalValue.trim();
}
// Some properties should never be transformed
if (includes(propsToIgnore, originalKey)) {
newObj[originalKey] = originalValue;
return newObj;
}
var _convertProperty = convertProperty(originalKey, originalValue),
key = _convertProperty.key,
value = _convertProperty.value;
newObj[key] = value;
return newObj;
}, Array.isArray(object) ? [] : {});
}
/**
* Converts a property and its value to the corresponding RTL key and value
* @param {String} originalKey the original property key
* @param {Number|String|Object} originalValue the original css property value
* @return {Object} the new {key, value} pair
*/
function convertProperty(originalKey, originalValue) {
var isNoFlip = /\/\*\s?@noflip\s?\*\//.test(originalValue);
var key = isNoFlip ? originalKey : getPropertyDoppelganger(originalKey);
var value = isNoFlip ? originalValue : getValueDoppelganger(key, originalValue);
return {
key: key,
value: value
};
}
/**
* This gets the RTL version of the given property if it has a corresponding RTL property
* @param {String} property the name of the property
* @return {String} the name of the RTL property
*/
function getPropertyDoppelganger(property) {
return propertiesToConvert[property] || property;
}
/**
* This converts the given value to the RTL version of that value based on the key
* @param {String} key this is the key (note: this should be the RTL version of the originalKey)
* @param {String|Number|Object} originalValue the original css property value. If it's an object, then we'll convert that as well
* @return {String|Number|Object} the converted value
*/
function getValueDoppelganger(key, originalValue) {
if (!canConvertValue(originalValue)) {
return originalValue;
}
if (isObject(originalValue)) {
return convert(originalValue); // recursion 🌀
}
var isNum = isNumber(originalValue);
var isFunc = isFunction(originalValue);
var importantlessValue = isNum || isFunc ? originalValue : originalValue.replace(/ !important.*?$/, '');
var isImportant = !isNum && importantlessValue.length !== originalValue.length;
var valueConverter = propertyValueConverters[key];
var newValue;
if (valueConverter) {
newValue = valueConverter({
value: importantlessValue,
valuesToConvert: valuesToConvert,
propertiesToConvert: propertiesToConvert,
isRtl: true,
bgImgDirectionRegex: bgImgDirectionRegex,
bgPosDirectionRegex: bgPosDirectionRegex
});
} else {
newValue = valuesToConvert[importantlessValue] || importantlessValue;
}
if (isImportant) {
return newValue + " !important";
}
return newValue;
}
return convert;
})));
//# sourceMappingURL=rtl-css-js.umd.js.map