| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| |
| // @flow |
| |
| import { getViewportInfo } from '../env/viewport' |
| import addPrefix from 'inline-style-prefixer/static/index' |
| |
| const noUnitsNumberKeys = [ |
| 'flex', |
| 'opacity', |
| 'zIndex', |
| 'fontWeight', |
| 'lines' |
| ] |
| |
| // whether to support using 0.5px to paint 1px width border. |
| let _supportHairlines: ?boolean |
| export function supportHairlines () { |
| if (typeof _supportHairlines === 'undefined') { |
| const dpr = window.devicePixelRatio |
| if (dpr && dpr >= 2 && document.documentElement) { |
| const docElm = document.documentElement |
| const testElm = document.createElement('div') |
| const fakeBody = document.createElement('body') |
| const beforeNode = docElm.firstElementChild || docElm.firstChild |
| testElm.style.border = '0.5px solid transparent' |
| fakeBody.appendChild(testElm) |
| docElm.insertBefore(fakeBody, beforeNode) |
| _supportHairlines = testElm.offsetHeight === 1 |
| docElm.removeChild(fakeBody) |
| } |
| else { |
| _supportHairlines = false |
| } |
| } |
| return _supportHairlines |
| } |
| |
| /** |
| * remove comments from a cssText. |
| */ |
| export function trimComment (cssText: string): string { |
| return cssText.replace(/(?:\/\*)[\s\S]*?\*\//g, '') |
| } |
| |
| let support: boolean | null = null |
| |
| export function supportSticky (): boolean { |
| if (support !== null) { |
| return support |
| } |
| const element = window.document.createElement('div') |
| const elementStyle = element.style |
| elementStyle.cssText = 'position:-webkit-sticky;position:sticky;' |
| support = elementStyle.position.indexOf('sticky') !== -1 |
| return support |
| } |
| |
| const regPercentage = /^[+-]?\d+(\.\d+)?%$/ |
| export function isPercentage (val: string) { |
| return regPercentage.test(val) |
| } |
| |
| const regUnitsNum = /^([+-]?\d+(?:\.\d+)?)([p,w]x)?$/ // support units: px, wx. |
| export function normalizeUnitsNum (val: string): string { |
| const match = val.match(regUnitsNum) |
| if (!match) { return '' } |
| let unit = 'px' // px by default. |
| if (match[2]) { |
| unit = match[2] |
| } |
| return parseScale(parseFloat(match[1]), unit) |
| } |
| |
| function getUnitScaleMap () { |
| const { scale, dpr } = getViewportInfo() |
| return { |
| px: scale, |
| wx: scale * dpr |
| } |
| } |
| |
| function limitScale (val, limit) { |
| limit = limit || 1 |
| const sign = val === 0 ? 0 : val > 0 ? 1 : -1 |
| let newVal = Math.abs(val) > limit ? val : sign * limit |
| // support 1px device width. |
| if (newVal === 1 && val < 1 && supportHairlines()) { |
| newVal = 0.5 |
| } |
| return newVal |
| } |
| |
| function parseScale (val: number, unit: string): string { |
| const unitScaleMap = getUnitScaleMap() |
| return limitScale(val * unitScaleMap[unit]) + 'px' |
| } |
| |
| export function normalizeString (styleKey: string, styleVal: string): string { |
| if (isPercentage(styleVal)) { |
| return styleVal |
| } |
| |
| /** |
| * 1. test if is a regular scale css. e.g. `width: 100px;` |
| * this should be a standalone number value with or without unit, otherwise |
| * it shouldn't be changed. |
| */ |
| const unitsNum = normalizeUnitsNum(styleVal) |
| if (unitsNum) { return unitsNum } |
| |
| /** |
| * 2. if a string contains multiple px values, than they should be all normalized. |
| * values should have wx or px units, otherwise they should be left unchanged. |
| * e.g. |
| * transform: translate(10px, 6px, 0) |
| * border: 2px solid red |
| */ |
| const numReg = /([+-]?[\d.]+)([p,w]x)/ig |
| if (numReg.test(styleVal)) { |
| const unitScaleMap = getUnitScaleMap() |
| const val = styleVal.replace(numReg, function (m, $0, $1) { |
| const res = parseFloat($0) * unitScaleMap[$1] |
| return limitScale(res) + 'px' |
| }) |
| return val |
| } |
| |
| // otherwise |
| return styleVal |
| } |
| |
| export function autoPrefix (style: {}): {} { |
| const prefixed = addPrefix(style) |
| // flex only added WebkitFlex. Should add WebkitBoxFlex also. |
| const flex = prefixed.flex |
| if (flex) { |
| prefixed.WebkitBoxFlex = flex |
| prefixed.MozBoxFlex = flex |
| prefixed.MsFlex = flex |
| } |
| return prefixed |
| } |
| |
| export function normalizeNumber (styleKey: string, styleVal: number): string { |
| const { scale } = getViewportInfo() |
| return styleVal * scale + 'px' |
| } |
| |
| /** |
| * normalize style to adapte to current viewport by multiply current scale. |
| * @param {object} style: should be camelCase. |
| */ |
| export function normalizeStyle (style: {}) { |
| const res = {} |
| for (const key in style) { |
| const val = style[key] |
| if (noUnitsNumberKeys.indexOf(key) > -1) { |
| res[key] = val |
| continue |
| } |
| switch (typeof val) { |
| case 'string': |
| res[key] = normalizeString(key, val) |
| break |
| case 'number': |
| res[key] = normalizeNumber(key, val) |
| break |
| default: |
| res[key] = val |
| break |
| } |
| } |
| return res |
| } |
| |
| /** |
| * get transformObj |
| */ |
| export function getTransformObj (elm: HTMLElement): any { |
| let styleObj = {} |
| if (!elm) { return styleObj } |
| const transformStr = elm.style.webkitTransform |
| || elm.style.mozTransform |
| || elm.style.transform |
| if (transformStr && transformStr.match(/(?: *(?:translate|rotate|scale)[^(]*\([^(]+\))+/i)) { |
| styleObj = transformStr.trim().replace(/, +/g, ',').split(' ').reduce(function (pre, str) { |
| ['translate', 'scale', 'rotate'].forEach(function (name) { |
| if (new RegExp(name, 'i').test(str)) { |
| pre[name] = str |
| } |
| }) |
| return pre |
| }, {}) |
| } |
| return styleObj |
| } |
| |
| /** |
| * translate a transform string from a transformObj. |
| */ |
| export function getTransformStr (obj: {}): string { |
| return Object.keys(obj).reduce(function (pre, key) { |
| return pre + obj[key] + ' ' |
| }, '') |
| } |
| |
| /** |
| * add transform style to element. |
| * @param {HTMLElement} elm |
| * @param {object} style: transform object, format is like this: |
| * { |
| * translate: 'translate3d(2px, 2px, 2px)', |
| * scale: 'scale(0.2)', |
| * rotate: 'rotate(30deg)' |
| * } |
| * @param {boolean} replace: whether to replace all transform properties. |
| */ |
| export function addTransform (elm: HTMLElement, style: {}, replace: boolean): void { |
| if (!style) { return } |
| let styleObj = {} |
| if (!replace) { |
| styleObj = getTransformObj(elm) |
| } |
| for (const key in style) { |
| const val = style[key] |
| if (val) { |
| styleObj[key] = val |
| } |
| } |
| const resStr = getTransformStr(styleObj) |
| elm.style.webkitTransform = resStr |
| elm.style.mozTransform = resStr |
| elm.style.transform = resStr |
| } |
| |
| /** |
| * add translate X to the element. |
| */ |
| export function addTranslateX (elm: HTMLElement, toAdd: number): void { |
| if (!toAdd) { return } |
| const styleObj = getTransformObj(elm) |
| if (!styleObj.translate) { |
| styleObj.translate = 'translate3d(0px, 0px, 0px)' |
| } |
| styleObj.translate = styleObj.translate.replace(/[+-\d.]+[pw]x/, function ($0) { |
| return (parseFloat($0) + toAdd) + 'px' |
| }) |
| const resStr = getTransformStr(styleObj) |
| elm.style.webkitTransform = resStr |
| elm.style.mozTransform = resStr |
| elm.style.transform = resStr |
| } |
| |
| /** |
| * copy a transform behaviour from one element to another. |
| * key could be: 'translate' | 'scale' | 'rotate' |
| */ |
| export function copyTransform (from: HTMLElement, to: HTMLElement, key: string | void): void { |
| let str |
| if (!key) { |
| str = from.style.webkitTransform |
| || from.style.mozTransform |
| || from.style.transform |
| } |
| else { |
| const fromObj = getTransformObj(from) |
| if (!fromObj[key]) { return } |
| const toObj = getTransformObj(to) |
| toObj[key] = fromObj[key] |
| str = getTransformStr(toObj) |
| } |
| to.style.webkitTransform = str |
| to.style.mozTransform = str |
| to.style.transform = str |
| } |
| |
| /** |
| * get color's r, g, b value. |
| * @param {string} color support all kinds of value of color. |
| */ |
| export function getRgb (color: string) { |
| const haxReg = /#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/ |
| const rgbReg = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/ |
| const span = document.createElement('span') |
| const body = document.body |
| span.style.cssText = `color: ${color}; width: 0px; height: 0px;` |
| body && body.appendChild(span) |
| color = window.getComputedStyle(span).color + '' |
| body && body.removeChild(span) |
| |
| let match |
| match = color.match(haxReg) |
| if (match) { |
| return { |
| r: parseInt(match[1], 16), |
| g: parseInt(match[2], 16), |
| b: parseInt(match[3], 16) |
| } |
| } |
| match = color.match(rgbReg) |
| if (match) { |
| return { |
| r: parseInt(match[1]), |
| g: parseInt(match[2]), |
| b: parseInt(match[3]) |
| } |
| } |
| } |
| |
| /** |
| * get style sheet with owner node's id |
| * @param {string} id owner node id. |
| */ |
| export function getStyleSheetById (id?: string) { |
| if (!id) { return } |
| const styleSheets = document.styleSheets |
| const len = styleSheets.length |
| for (let i = 0; i < len; i++) { |
| const styleSheet = styleSheets[i] |
| if (styleSheet.ownerNode.id === id) { |
| return styleSheet |
| } |
| } |
| } |
| |
| function getChildrenTotalWidth (children) { |
| const len = children.length |
| let total = 0 |
| for (let i = 0; i < len; i++) { |
| total += children[i].getBoundingClientRect().width |
| } |
| return total |
| } |
| /** |
| * get total content width of the element. |
| * @param {HTMLElement} elm |
| */ |
| export function getRangeWidth (elm: HTMLElement) { |
| const children = elm.children |
| if (!children) { |
| return elm.getBoundingClientRect().width |
| } |
| if (!Range) { |
| return getChildrenTotalWidth(children) |
| } |
| const range = document.createRange() |
| if (!range.selectNodeContents) { |
| return getChildrenTotalWidth(children) |
| } |
| range.selectNodeContents(elm) |
| return range.getBoundingClientRect().width |
| } |