UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,149 lines (1,111 loc) 33.1 kB
/** * @fileoverview * Utility functions about HTML DOM, Styles and so on. * This file is splitted from kekule.domUtils.js * @author Partridge Jiang */ (function(){ /** * Util methods about CSS and style values. * @object */ Kekule.StyleUtils = { /** * Remove a property from inline style. * @param {Object} style Inline style object if element. * @param {String} propName */ removeStyleProperty: function(style, propName) { if (style.removeProperty) style.removeProperty(propName); else if (style.removeAttribute) style.removeAttribute(propName); else style[propName] = null; }, /** * Split a units value to {value, units} hash. * @param {String} value * @returns {Hash} */ analysisUnitsValue: function(value) { var r = {}; r.total = value; r.value = parseFloat(value); var sunit; if (value && value.length && (r.value !== undefined) && (!isNaN(r.value))) { sunit = ''; for (var i = value.length - 1; i >= 0; --i) { var c = value.charAt(i); var isDigital = (c >= '0') && (c <= '9'); if ((!isDigital) && (c !== '.') && (c !== '-')) sunit = c + sunit; } } r.units = sunit || ''; return r; }, /** * Multiple a units string value. * @param {String} value * @param {Num} times * @returns {String} */ multiplyUnitsValue: function(value, times) { var v = Kekule.StyleUtils.analysisUnitsValue(value); v.value *= times; return '' + v.value + v.units; }, /** * Turn a #RRGGBB or #RGB style string to a integer value. * @param {String} str * @returns {Int} * @deprecated */ colorStrToValue: function(str) { /* var isLongFormat = str.length > 4; var sR = isLongFormat? str.substr(1, 2): str.substr(1, 1); var sG = isLongFormat? str.substr(3, 2): str.substr(2, 1); var sB = isLongFormat? str.substr(5, 2): str.substr(3, 1); if (!isLongFormat) { sR += sR; sG += sG; sB += sB; } var result = (parseInt(sR, 16) << 16) + (parseInt(sG, 16) << 8) + parseInt(sB, 16); return result; */ var v = Kekule.StyleUtils.analysisCssColor(str, true); var result = (Math.round(v.r) << 16) + (Math.round(v.g) << 8) + (Math.round(v.b)); return result; }, /** * Convert CSS #RRGGBB(AA) or #RGB or rgb/rgba() or hsl/hsla() style string into a hash. * @param {String} str * @param {Bool} convertToRgb Whether convert HSL values to RGB. * @returns {Hash} A hash with {r, g, b, a}. */ analysisCssColor: function(str, convertToRgb) { var SU = Kekule.StyleUtils; var s = (str || '').toLowerCase(); if (s.indexOf('#') === 0) // #RGB or #RRGGBB { var sHex = s.substr(1); return SU._analyisCssColorHex(sHex); } else if (s.indexOf('rgb') === 0) // rgb() or rgba() { var i1 = s.indexOf('('); var i2 = s.indexOf(')'); if (i1 < 0 || i2 < 0) // not a legal value return null; // extract the values inside () return SU._analysisCssColorRgbOrHsl(s.substring(i1 + 1, i2), false); } else if (s.indexOf('hsl') === 0) // hsl or hsla { var i1 = s.indexOf('('); var i2 = s.indexOf(')'); if (i1 < 0 || i2 < 0) // not a legal value return null; // extract the values inside () var result = SU._analysisCssColorRgbOrHsl(s.substring(i1 + 1, i2), true); if (convertToRgb) { result = Object.extend(result, SU._convertHslToRgb(result)); // preseve alpha value } return result; } else return null; }, /** @private */ _analyisCssColorHex: function(sHex) { var l = sHex.length; var isCompact = (l === 3); // Check if in #RGB mode var partLength = isCompact? 1: 2; var partCount = Math.min(4, Math.floor(l / partLength)); // max part count is 4 in #RRGGBBAA var splitted = []; for (var i = 0; i < partCount; ++i) { var currIndex = i * partLength; var sPart = sHex.substr(currIndex, partLength); if (isCompact) sPart += sPart; splitted.push(parseInt(sPart, 16)); } var result = {'r': splitted[0] || 0, 'g': splitted[1] || 0, 'b': splitted[2] || 0}; if (Kekule.ObjUtils.notUnset(splitted[3])) result.a = splitted[3] / 255; else result.a = 1; return result; }, /** @private */ _analysisCssColorRgbOrHsl: function(sValues, isHsl) { var _convertCssNumOrPercentageToNum = Kekule.StyleUtils._convertCssNumOrPercentageToNum; var result; var splitted = sValues.split(','); if (isHsl) { var hue = _convertCssNumOrPercentageToNum(splitted[0], 360); var sat = _convertCssNumOrPercentageToNum(splitted[1]); var light = _convertCssNumOrPercentageToNum(splitted[2]); result = {'h': hue, 's': sat, 'l': light}; } else // rgb { result = { 'r': _convertCssNumOrPercentageToNum(splitted[0], 255), 'g': _convertCssNumOrPercentageToNum(splitted[1], 255), 'b': _convertCssNumOrPercentageToNum(splitted[2], 255) }; } if (Kekule.ObjUtils.notUnset(splitted[3])) result.a = _convertCssNumOrPercentageToNum(splitted[3]); else result.a = 1; return result; }, /** @private */ _convertHslToRgb: function(hsl, rgbValueRangeMax) { if (!rgbValueRangeMax) rgbValueRangeMax = 255; var h = hsl.h; var s = hsl.s; var l = hsl.l; var r, g, b; if (s === 0) // no saturation, gray { r = g = b = l; } else { var q = (l < 0.5)? (l * (1 + s)): (l + s - l * s); var p = 2 * l - q; var hk = h / 360; var tr = hk + 1/3; var tg = hk; var tb = hk - 1/3; var t3s = [tr, tg, tb]; for (var i = 0, l = t3s.length; i < l; ++i) { if (t3s[i] < 0) t3s[i] = t3s[i] + 1; else if (t3s[i] > 1) t3s[i] = t3s[i] - 1; if (6 * t3s[i] < 1) t3s[i] = p + (q - p) * 6 * t3s[i]; else if (2 * t3s[i] < 1) t3s[i] = q; else if (3 * t3s[i] < 2) t3s[i] = p + (q - p) * 6 * (2/3 - t3s[i]); else t3s[i] = p; } r = t3s[0], g = t3s[1], b = t3s[2]; } return {'r': r * rgbValueRangeMax, 'g': g * rgbValueRangeMax, 'b': b * rgbValueRangeMax}; }, /** @private */ _convertCssNumOrPercentageToNum: function(sValue, percentageRangeMax) { var n = parseFloat(sValue) || 0; if (sValue.endsWith('%')) // percentage { n = n / 100 * (percentageRangeMax || 1); } return n; }, /** * Returns computed style of element. If propName not set, all computed result will be returned. * @param {Object} elem * @param {String} propName Can either by in JS('backgroundColor') or CSS('background-color') form. * @returns {Variant} */ getComputedStyle: function(elem, propName) { var styles; var doc = elem.ownerDocument; if (!doc) return null; var view = doc.defaultView; if (view && view.getComputedStyle) { styles = view.getComputedStyle(elem, null); } else if (elem.currentStyle) // IE { styles = elem.currentStyle; } if (styles) // some times IE can not fetch currentStyle { if (propName) { var inCssForm = (propName.indexOf('-') >= 0); if (inCssForm && styles.getPropertyValue) return styles.getPropertyValue(propName); else return styles[propName]; } else // return while style object return styles; } else { return null; } }, /** * Check if element is likely to be visible on page (only display and visibility style are checked). * @param {HTMLElement} elem * @returns {Bool} */ isShown: function(elem) { var U = Kekule.StyleUtils; return (U.isDisplayed(elem) && U.isVisible(elem)); }, /** * Check if element's CSS display property is not set to 'none'. * @param {HTMLElement} elem * @return {Bool} */ isDisplayed: function(elem) { return (Kekule.StyleUtils.getComputedStyle(elem, 'display') || '').toLowerCase() !== 'none'; }, /** * Get display style of element. * @param {HTMLElement} elem * @returns (String} */ getDisplayed: function(elem) { return Kekule.StyleUtils.getComputedStyle(elem, 'display'); }, /** * Set display style of element. * @param {HTMLElement} elem * @param (Variant} value If value is a string, the string will be set to display style. * If it is a boolean, display style will be set to 'none'/'' on false/true. */ setDisplay: function(elem, value) { if (elem) { if (typeof(value) === 'string') elem.style.display = value; else elem.style.display = (!!value) ? '' : 'none'; } }, /** * Check if element's CSS visibility property is not set to 'hidden'. * @param {HTMLElement} elem * @return {Bool} */ isVisible: function(elem) { return (Kekule.StyleUtils.getComputedStyle(elem, 'visibility') || '').toLowerCase() !== 'hidden'; }, /** * Get visibility style of element. * @param {HTMLElement} elem * @returns (String} */ getVisibility: function(elem) { return Kekule.StyleUtils.getComputedStyle(elem, 'visibility'); }, /** * Set visibility style of element. * @param {HTMLElement} elem * @param (Variant} value If value is a string, the string will be set to visibility style. * If it is a boolean, visibility style will be set to 'hidden'/'' on false/true. */ setVisibility: function(elem, value) { if (elem) { if (typeof(value) === 'string') elem.style.visibility = value; else elem.style.visibility = (!!value) ? '' : 'hidden'; } }, /** * Returns whether the element generates a block element box. * @param {HTMLElement} elem * @returns {Bool} */ isBlockElem: function(elem) { var display = Kekule.StyleUtils.getComputedStyle(elem, 'display'); return ['block', 'list-item', 'table', 'flex', 'grid'].indexOf(display) >= 0; }, /** * Check if an element is set with absolute or fixed position style. * @param {HTMLElement} elem * @returns {Bool} */ isAbsOrFixPositioned: function(elem) { var position = Kekule.StyleUtils.getComputedStyle(elem, 'position') || ''; position = position.toLowerCase(); return (position === 'absolute') || (position === 'fixed'); }, /** @private */ _fillAbsOrFixedPositionStyleStack: function(elem, stack) { if (Kekule.DomUtils.isElement(elem)) // if elem is not readlly an element (e.g., shadowRoot), bypass the style compute to avoid error { var position = Kekule.StyleUtils.getComputedStyle(elem, 'position') || ''; position = position.toLowerCase(); if ((position === 'absolute') || (position === 'fixed')) { stack.push(position.toLocaleLowerCase()); } } var parent = elem.parentNode; if (parent && parent.ownerDocument) Kekule.StyleUtils._fillAbsOrFixedPositionStyleStack(parent, stack); return stack; }, /** * Check the ancestors of elem, if one is set to absolute or fixed position, * returns its position style. * @param {HTMLElement} elem * @returns {Bool} */ isAncestorPositionFixed: function(elem) { var parent = elem.parentNode; if (parent) { return Kekule.StyleUtils.isSelfOrAncestorPositionFixed(elem); } else return false; }, /** * Check the elem and its ancestor, if one is set to fixed position, result is true. * @param {HTMLElement} elem * @returns {Bool} */ isSelfOrAncestorPositionFixed: function(elem) { var positionStack = []; Kekule.StyleUtils._fillAbsOrFixedPositionStyleStack(elem, positionStack); if (!positionStack.length) return null; for (var i = positionStack.length - 1; i >= 0; --i) { var p = positionStack[i]; if (p === 'fixed') return true; } return false; }, /** * Set cursor style of an element. * @param {HTMLElement} elem * @param {Variant} cursor A string CSS cursor value or an array of cursor keywords. * If this param is an array, the first cursor keywords available in current browser will actually be used. * @returns {String} The actually used cursor value. */ setCursor: function(elem, cursor) { if (DataType.isArrayValue(cursor)) { for (var i = 0, l = cursor.length; i < l; ++i) { var currCursor = cursor[i]; elem.style.cursor = currCursor; if (elem.style.cursor === currCursor) // successfully setted return currCursor; } return elem.style.cursor; } else // normal string { elem.style.cursor = cursor; return elem.style.cursor; } }, /** @private */ _cssTransformValuesToMatrix: function(cssTransformValues) { var matrix = Kekule.MatrixUtils.create(3, 3, 0); matrix[0][0] = cssTransformValues.a; matrix[1][0] = cssTransformValues.b; matrix[0][1] = cssTransformValues.c; matrix[1][1] = cssTransformValues.d; matrix[0][2] = cssTransformValues.tx; matrix[1][2] = cssTransformValues.ty; return matrix; }, /** @private */ _matrixToCssTransformValues: function(matrix) { var result = { 'a': matrix[0][0], 'b': matrix[1][0], 'c': matrix[0][1], 'd': matrix[1][1], 'tx': matrix[0][2], 'ty': matrix[1][2] }; return result; }, /** * Check if an element has a CSS transform. * @param {HTMLElement} elem * @returns {Bool} */ hasTransform: function(elem) { var transform = Kekule.StyleUtils.getComputedStyle(elem, 'transform'); return transform && (transform !== 'none'); }, /** * Returns matrix function values of CSS transform property. * @param {HTMLElement} elem * @returns {Hash} {a, b, c, d, tx, ty} */ getTransformMatrixValues: function(elem) { var matrix = Kekule.StyleUtils.getComputedStyle(elem, 'transform') || ''; var values = matrix.match(/-?[\d\.]+/g); if (values) return {'a': values[0], 'b': values[1], 'c': values[2], 'd': values[3], 'tx': values[4], 'ty': values[5]}; else return null; }, /** * Set the matrix values of CSS transform. * @param {HTMLElement} elem * @param {Array} values */ setTransformMatrixArrayValues: function(elem, values) { if (values) { var sMatrix = 'matrix(' + values.join(',') + ')'; elem.style.transform = sMatrix; } }, /** * Returns a matrix object that represent the 2D transform of element. * @param {HTMLElement} elem * @returns {Array} */ getTransformMatrix: function(elem) { var values = Kekule.StyleUtils.getTransformMatrixValues(elem); if (values) { return Kekule.StyleUtils._cssTransformValuesToMatrix(values); } else return null; }, /** * A transformed element may be nested in another transformed parent. * This function returns all the transform matrixes from parent to child. * @param {HTMLElement} elem * @param {Array} */ getCascadeTranformMatrixes: function(elem) { var result = []; var currElem = elem; while (currElem) { if (currElem.nodeType === 1) // Node.ELEMENT_NODE, if not element (e.g., shadow root), bypass to parent { var m = Kekule.StyleUtils.getTransformMatrix(currElem); if (m) result.unshift(m); } currElem = currElem.parentNode; } return result; }, /** * A transformed element may be nested in another transformed parent. * This function returns product of all those transform matrixes from parent to child. * @param {HTMLElement} elem * @param {Array} */ getTotalTransformMatrix: function(elem) { var matrixes = Kekule.StyleUtils.getCascadeTranformMatrixes(elem); var result = null; // child on left, parent on right for (var i = matrixes.length - 1; i >= 0; --i) { var m = matrixes[i]; if (!result) result = m; else result = Kekule.MatrixUtils.multiply(result, m); } return result; }, calcInvertTransformMatrix: function(matrix) { var transValues = Kekule.StyleUtils._matrixToCssTransformValues(matrix); var v = transValues; // calc inverted values, algorithm from https://blog.csdn.net/qq_17429661/article/details/51985344 var det = v.a * v.d - v.b * v.c; if (Math.abs(det) < 0.000001) // Singular Matrix return false; // can not calculate var v1 = { a: v.d / det, b: -v.b / det, c: -v.c / det, d: v.a / det, tx: (v.c * v.ty - v.d * v.tx) / det, ty: (v.b * v.tx - v.a * v.ty) / det }; var result = Kekule.StyleUtils._cssTransformValuesToMatrix(v1); return result; } }; /** * Utils methods for HTML elements. * @object */ Kekule.HtmlElementUtils = { /** * Get classes used by element. * @param {HTMLElement} elem * @returns {Array} */ getClassNames: function(elem) { //return Kekule.StrUtils.splitTokens(elem.className || ''); // elem.className may be of SVGAnimatedString when dealing SVG element return Kekule.StrUtils.splitTokens(elem.getAttribute('class') || ''); }, /** * Check if a class is associate with element. * @param {HTMLElement} elem * @param {String} className * @return {Bool} */ hasClass: function(elem, className) { var names = Kekule.HtmlElementUtils.getClassNames(elem); return (names.indexOf(className) >= 0); }, /** * Add class name(s) to element. * @param {HTMLElement} elem * @param {Variant} className Can be a simple name, or a series of name separated by space ('name1 name2') * or an array of strings. */ addClass: function(elem, className) { var U = Kekule.HtmlElementUtils; var names = Kekule.ArrayUtils.isArray(className)? className: Kekule.StrUtils.splitTokens(className); for (var i = 0, l = names.length; i < l; ++i) { if (!U.hasClass(elem, names[i])) elem.className += ' ' + names[i]; } return U; }, /** * remove class(es) from element. * @param {HTMLElement} elem * @param {Variant} className Can be a simple name, or a series of name separated by space ('name1 name2') * or an array of strings. */ removeClass: function(elem, className) { var U = Kekule.HtmlElementUtils; var removedNames = Kekule.ArrayUtils.isArray(className)? className: Kekule.StrUtils.splitTokens(className); var names = U.getClassNames(elem); for (var i = names.length; i >= 0; --i) { var index = removedNames.indexOf(names[i]); if (index >= 0) names.splice(i, 1); } elem.className = names.join(' '); return U; }, /** * Toggle class(es) from element. * @param {HTMLElement} elem * @param {Variant} className Can be a simple name, or a series of name separated by space ('name1 name2') * or an array of strings. */ toggleClass: function(elem, className) { var U = Kekule.HtmlElementUtils; var names = Kekule.ArrayUtils.isArray(className)? className: Kekule.StrUtils.splitTokens(className); for (var i = 0, l = names.length; i < l; ++i) { if (U.hasClass(elem, names[i])) U.removeClass(elem, names[i]); else U.addClass(elem, names[i]); } return U; }, /** * Returns text (without tag) inside an element. * @param {HTMLElement} elem * @returns {String} */ getInnerText: function(elem) { /* if (elem.innerText) // IE return elem.innerText; else { var children = elem.children; for (var i = 0, l = children.length; i < l; ++i) { var node = children[i]; if (node.nodeType === Node.textContent) } } */ return elem.innerText || elem.textContent; }, /** * Resize a HTML element. * @param {HTMLElement} elem * @param {Number} width * @param {Number} height * @param {String} unit Default is px. */ resizeElem: function(elem, width, height, unit) { elem.style.width = width + (unit || 'px'); elem.style.height = height + (unit || 'px'); }, /** * Get element client width and height. * @param {HTMLElement} elem * @returns {Hash} A combination of {width, height}, in px. */ getElemClientDimension: function(elem) { return {'width': elem.clientWidth, 'height': elem.clientHeight}; }, /** * Get element scroll width and height. * @param {HTMLElement} elem * @returns {Hash} A combination of {width, height}, in px. */ getElemScrollDimension: function(elem) { return {'width': elem.scrollWidth, 'height': elem.scrollHeight}; }, /** * Returns computed width/height in px. * @param {HTMLElement} elem * @returns {Hash} A combination of {width, height}, in px. */ getElemComputedDimension: function(elem) { var SU = Kekule.StyleUtils; var dim = {'width': SU.getComputedStyle(elem, 'width'), 'height': SU.getComputedStyle(elem, 'height')}; var wInfo = SU.analysisUnitsValue(dim.width); var hInfo = SU.analysisUnitsValue(dim.height); var result = {}; if (wInfo.units === 'px') result.width = wInfo.value; if (hInfo.units === 'px') result.height = wInfo.value; return result; }, /** * Get element offset width and height. * @param {HTMLElement} elem * @returns {Hash} A combination of {width, height}, in px. */ getElemOffsetDimension: function(elem) { return {'width': elem.offsetWidth, 'height': elem.offsetHeight}; }, /** * Get size of view port. * @param {Variant} elemOrDocOrViewport * @returns {Hash} */ getViewportDimension: function(elemOrDocOrViewport) { // Use the specified window or the current window if no argument var w; if (elemOrDocOrViewport) { if (elemOrDocOrViewport.ownerDocument) w = Kekule.DocumentUtils.getDefaultView(elemOrDocOrViewport.ownerDocument); else if (elemOrDocOrViewport.defaultView) w = elemOrDocOrViewport.defaultView; else if (elemOrDocOrViewport.parentWindow) w = elemOrDocOrViewport.parentWindow; else w = elemOrDocOrViewport; } w = w || window; // This works for all browsers except IE8 and before if (w.innerWidth != null) return { 'width': w.innerWidth, 'height': w.innerHeight }; // For IE (or any browser) in Standards mode var d = w.document; if (d.compatMode == "CSS1Compat") return { 'width': d.documentElement.clientWidth, 'height': d.documentElement.clientHeight }; // For browsers in Quirks mode return { 'width': d.body.clientWidth, 'height': d.body.clientHeight }; }, /** * Returns coord of top-left and bottom-right of visible viewport part in browser window. * @param {Variant} elemOrDocOrViewport * @returns {Hash} */ getViewportVisibleBox: function(elemOrDocOrViewport) { // Use the specified window or the current window if no argument var w; if (elemOrDocOrViewport) { if (elemOrDocOrViewport.ownerDocument) w = Kekule.DocumentUtils.getDefaultView(elemOrDocOrViewport.ownerDocument); else if (elemOrDocOrViewport.defaultView) w = elemOrDocOrViewport.defaultView; else if (elemOrDocOrViewport.parentWindow) w = elemOrDocOrViewport.parentWindow; else w = elemOrDocOrViewport; } w = w || window; var doc = w.document; var dim = Kekule.HtmlElementUtils.getViewportDimension(w); var offset = Kekule.DocumentUtils.getScrollPosition(doc); var result = {'x1': offset.left, 'y1': offset.top, 'x2': dim.width, 'y2': dim.height}; result.left = result.x1; result.top = result.y1; result.right = result.x2; result.bottom = result.y2; return result; }, /** * Returns bounding client rectangle for element. * @param {HTMLElement} elem * @param {Bool} includeScroll If this value is true, scrollTop/Left of documentElement will be added to result. * @returns {Hash} {top, left, bottom, right, width, height} */ getElemBoundingClientRect: function(elem, includeScroll) { var r = Object.extend({}, elem.getBoundingClientRect()); if (Kekule.ObjUtils.isUnset(r.width)) r.width = r.right - r.left; if (Kekule.ObjUtils.isUnset(r.height)) r.height = r.bottom - r.top; if (includeScroll) { var doc = elem.ownerDocument; var scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop || 0; var scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft || 0; r.left += scrollLeft; r.right += scrollLeft; r.top += scrollTop; r.bottom += scrollTop; } //var result = {'left': r.left, 'top': r.top, 'width': r.width, 'height': r.height}; var result = r; result.x = result.left; result.y = result.top; return result; }, /** * Get position relative to top-left corner of HTML page or current viewport (if includeDocScroll is true). * @param {HTMLElement} elem * @param {Bool} relToViewport * @returns {Hash} */ getElemPagePos: function(elem, relToViewport) { var xPosition = 0; var yPosition = 0; /* if (elem.getBoundingClientRect) { var box = elem.getBoundingClientRect(); var doc = elem.ownerDocument; var body = doc.body; var docElem = doc.documentElement; var clientTop = docElem.clientTop || body.clientTop || 0; var clientLeft = docElem.clientLeft || body.clientLeft || 0; yPosition = box.top + (window && window.pageYOffset || docElem && docElem.scrollTop || body.scrollTop ) - clientTop, xPosition = box.left + (window && window.pageXOffset || docElem && docElem.scrollLeft || body.scrollLeft) - clientLeft; } else */ { var currElem = elem; while(currElem) { // TODO: Here Chrome report body.scrollLeft unavailable in strict mode warning xPosition += (currElem.offsetLeft - currElem.scrollLeft + currElem.clientLeft); yPosition += (currElem.offsetTop - currElem.scrollTop + currElem.clientTop); currElem = currElem.offsetParent; } if (relToViewport) { var doc = elem.ownerDocument; var scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop || 0; var scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft || 0; xPosition -= scrollLeft; yPosition -= scrollTop; } } return { x: xPosition, y: yPosition }; }, /** * Get position relative to top-left corner of HTML page togather with width/height of elem. * @param {HTMLElement} elem * @param {Bool} relToViewport * @returns {Hash} {x, y, top, left, bottom, right, width, height} */ getElemPageRect: function(elem, relToViewport) { var pos = Kekule.HtmlElementUtils.getElemPagePos(elem, relToViewport); var dim = Kekule.HtmlElementUtils.getElemClientDimension(elem); return { 'x': pos.x, 'y': pos.y, 'left': pos.x, 'top': pos.y, 'right': pos.x + dim.width, 'bottom': pos.y + dim.height, 'width': dim.width, 'height': dim.height }; }, /** * Get position relative to top-left corner of viewport. * @param {HTMLElement} elem * @returns {Hash} */ getElemViewportPos: function(elem) { //var rect = Kekule.HtmlElementUtils.getElemBoundingClientRect(elem); var rect = Kekule.HtmlElementUtils.getElemPagePos(elem, true); return {'x': rect.left, 'y': rect.top}; }, /** * Set element's position style to make it to a absolute, relative or fixed one (but retain element's position). * @param {HTMLElement} elem */ makePositioned: function(elem) { var p = Kekule.StyleUtils.getComputedStyle(elem, 'position'); if (!p || p.toLowerCase() === 'static') elem.style.position = 'relative'; }, /** * Check if element is a form control (input, button, select and textarea). * @param elem */ isFormCtrlElement: function(elem) { var formCtrlTags = ['input', 'button', 'textarea', 'select']; var tagName = elem && elem.tagName.toLowerCase(); return !!tagName && formCtrlTags.indexOf(tagName) >= 0; }, /** * Issues an asynchronous request to make the element be displayed full-screen. * @param {HTMLElement} elem */ requestFullScreen: function(elem) { var func = elem.requestFullScreen || elem.mozRequestFullScreen || elem.webkitRequestFullScreen || elem.msRequestFullScreen; return func? func.apply(elem): null; }, /** * Issues an asynchronous request to make the element exit full-screen mode. * @param {HTMLElement} elem */ exitFullScreen: function(elem) { var func = elem.exitFullScreen || elem.mozExitFullScreen || elem.webkitExitFullScreen || elem.msExitFullScreen; return func? func.apply(elem): null; } }; /** * Utils on document. * @object */ Kekule.DocumentUtils = { /** * Returns the default view (window) of current document. * @param {HTMLDocument} document * @returns {Object} */ getDefaultView: function(document) { return document.defaultView || document.parentWindow; }, /** * Returns scroll top/left of document element. * @param {HTMLDocument} document * @returns {Hash} {left, top} */ getScrollPosition: function(document) { var win = Kekule.DocumentUtils.getDefaultView(document); var result = { 'left': ((win.pageXOffset !== undefined)? win.pageXOffset: (document.documentElement || document.body.parentNode || document.body).scrollLeft) || 0, 'top': ((win.pageYOffset !== undefined)? win.pageYOffset: (document.documentElement || document.body.parentNode || document.body).scrollTop) || 0 }; result.x = result.left; result.y = result.top; return result; }, /** * Returns dimension of viewport visible client. * @param {HTMLDocument} document * @returns {Hash} {width, height} */ getClientDimension: function(document) { if (document.compatMode == "BackCompat") { return { 'width': document.body.clientWidth, 'height': document.body.clientHeight } } else { return { 'width': document.documentElement.clientWidth, 'height': document.documentElement.clientHeight } } }, /** * Returns top-left and bottom-right coords and width/height of viewport visible client. * @param {HTMLDocument} document * @returns {Hash} {left, top, right, bottom, x1, x2, y1, y2, width, height} */ getClientVisibleBox: function(document) { var offset = Kekule.DocumentUtils.getScrollPosition(document); var dim = Kekule.DocumentUtils.getInnerClientDimension(document); var result = {}; result.left = result.x1 = offset.left; result.top = result.y1 = offset.top; result.right = result.x2 = dim.width + offset.left; result.bottom = result.y2 = dim.height + offset.top; result.width = dim.width; result.height = dim.height; return result; }, /** * Returns innerWidth and innerHeight property of a window. * @param {HTMLDocument} document * @returns {Hash} {width, height} */ getWindowInnerDimension: function(document) { var win = Kekule.DocumentUtils.getDefaultView(document); return {'width': Math.min(win.innerWidth, win.outerWidth), 'height': Math.min(win.innerHeight, win.outerHeight)}; }, /** * Returns the visible viewport dimension (on mobile device) or the dimension of whole viewport client (ondesktop). * @param {HTMLDocument} document * @returns {Hash} {width, height} */ getInnerClientDimension: function(document) { var clientDim = Kekule.DocumentUtils.getClientDimension(document); var innerDim = Kekule.DocumentUtils.getWindowInnerDimension(document); return { 'width': innerDim.width? Math.min(innerDim.width, clientDim.width): clientDim.width, 'height': innerDim.height? Math.min(innerDim.height, clientDim.height): clientDim.height }; }, /** * Returns the scale level of current page in mobile browser. * @param {HTMLDocument} document * @returns {Float} */ getClientScaleLevel: function(document) { return (document.compatMode == "BackCompat")? document.body.clientWidth / window.innerWidth: document.documentElement.clientWidth / window.innerWidth; }, /** * Returns the ratio of actual device pixel to CSS 1px. * @param {HTMLDocument} document * @returns {Number} */ getPixelZoomLevel: function(document) { var scale = Kekule.DocumentUtils.getClientScaleLevel(document); return scale * (Kekule.DocumentUtils.getDefaultView(document).devicePixelRatio || 1); }, /** * Returns PPI of current device. * The algorithm is from https://jsfiddle.net/pgLo6273/2/. * @param {HTMLDocument} document * @returns {Number} */ getDevicePPI: function(document) { var win = Kekule.DocumentUtils.getDefaultView(document); if (win.matchMedia) { var minRes = 0; var maxRes = 0; var curRes = 200; var trc = []; while (!maxRes || ((maxRes - minRes) > 1)) { if (win.matchMedia('(min-resolution: ' + curRes + 'dpi)').matches) { // ppi >= curRes if (maxRes) { minRes = curRes; curRes = Math.round((minRes + maxRes) / 2); } else { minRes = curRes; curRes = 2 * curRes; } } else { // ppi < curRes maxRes = curRes; curRes = Math.round((minRes + maxRes) / 2); } } return curRes; } else return 96; // old device, assume to be a fixed one } }; })();