563a7c7c134f75c18c14dd6f1678e55fbc9fb1db1cdaa49bbcd11a58b883b1ff366602b4efebbab077aeca00a04b3e392dd7a6750af59669eb39d1bd28cd64 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import defineConfigurable from './defineConfigurable.js';
  2. import getWindowOf from './getWindowOf.js';
  3. import isBrowser from './isBrowser.js';
  4. // Placeholder of an empty content rectangle.
  5. const emptyRect = createRectInit(0, 0, 0, 0);
  6. /**
  7. * Converts provided string to a number.
  8. *
  9. * @param {number|string} value
  10. * @returns {number}
  11. */
  12. function toFloat(value) {
  13. return parseFloat(value) || 0;
  14. }
  15. /**
  16. * Extracts borders size from provided styles.
  17. *
  18. * @param {CSSStyleDeclaration} styles
  19. * @param {...string} positions - Borders positions (top, right, ...)
  20. * @returns {number}
  21. */
  22. function getBordersSize(styles, ...positions) {
  23. return positions.reduce((size, position) => {
  24. const value = styles['border-' + position + '-width'];
  25. return size + toFloat(value);
  26. }, 0);
  27. }
  28. /**
  29. * Extracts paddings sizes from provided styles.
  30. *
  31. * @param {CSSStyleDeclaration} styles
  32. * @returns {Object} Paddings box.
  33. */
  34. function getPaddings(styles) {
  35. const positions = ['top', 'right', 'bottom', 'left'];
  36. const paddings = {};
  37. for (const position of positions) {
  38. const value = styles['padding-' + position];
  39. paddings[position] = toFloat(value);
  40. }
  41. return paddings;
  42. }
  43. /**
  44. * Calculates content rectangle of provided SVG element.
  45. *
  46. * @param {SVGGraphicsElement} target - Element content rectangle of which needs
  47. * to be calculated.
  48. * @returns {DOMRectInit}
  49. */
  50. function getSVGContentRect(target) {
  51. const bbox = target.getBBox();
  52. return createRectInit(0, 0, bbox.width, bbox.height);
  53. }
  54. /**
  55. * Calculates content rectangle of provided HTMLElement.
  56. *
  57. * @param {HTMLElement} target - Element for which to calculate the content rectangle.
  58. * @returns {DOMRectInit}
  59. */
  60. function getHTMLElementContentRect(target) {
  61. // Client width & height properties can't be
  62. // used exclusively as they provide rounded values.
  63. const {clientWidth, clientHeight} = target;
  64. // By this condition we can catch all non-replaced inline, hidden and
  65. // detached elements. Though elements with width & height properties less
  66. // than 0.5 will be discarded as well.
  67. //
  68. // Without it we would need to implement separate methods for each of
  69. // those cases and it's not possible to perform a precise and performance
  70. // effective test for hidden elements. E.g. even jQuery's ':visible' filter
  71. // gives wrong results for elements with width & height less than 0.5.
  72. if (!clientWidth && !clientHeight) {
  73. return emptyRect;
  74. }
  75. const styles = getWindowOf(target).getComputedStyle(target);
  76. const paddings = getPaddings(styles);
  77. const horizPad = paddings.left + paddings.right;
  78. const vertPad = paddings.top + paddings.bottom;
  79. // Computed styles of width & height are being used because they are the
  80. // only dimensions available to JS that contain non-rounded values. It could
  81. // be possible to utilize the getBoundingClientRect if only it's data wasn't
  82. // affected by CSS transformations let alone paddings, borders and scroll bars.
  83. let width = toFloat(styles.width),
  84. height = toFloat(styles.height);
  85. // Width & height include paddings and borders when the 'border-box' box
  86. // model is applied (except for IE).
  87. if (styles.boxSizing === 'border-box') {
  88. // Following conditions are required to handle Internet Explorer which
  89. // doesn't include paddings and borders to computed CSS dimensions.
  90. //
  91. // We can say that if CSS dimensions + paddings are equal to the "client"
  92. // properties then it's either IE, and thus we don't need to subtract
  93. // anything, or an element merely doesn't have paddings/borders styles.
  94. if (Math.round(width + horizPad) !== clientWidth) {
  95. width -= getBordersSize(styles, 'left', 'right') + horizPad;
  96. }
  97. if (Math.round(height + vertPad) !== clientHeight) {
  98. height -= getBordersSize(styles, 'top', 'bottom') + vertPad;
  99. }
  100. }
  101. // Following steps can't be applied to the document's root element as its
  102. // client[Width/Height] properties represent viewport area of the window.
  103. // Besides, it's as well not necessary as the <html> itself neither has
  104. // rendered scroll bars nor it can be clipped.
  105. if (!isDocumentElement(target)) {
  106. // In some browsers (only in Firefox, actually) CSS width & height
  107. // include scroll bars size which can be removed at this step as scroll
  108. // bars are the only difference between rounded dimensions + paddings
  109. // and "client" properties, though that is not always true in Chrome.
  110. const vertScrollbar = Math.round(width + horizPad) - clientWidth;
  111. const horizScrollbar = Math.round(height + vertPad) - clientHeight;
  112. // Chrome has a rather weird rounding of "client" properties.
  113. // E.g. for an element with content width of 314.2px it sometimes gives
  114. // the client width of 315px and for the width of 314.7px it may give
  115. // 314px. And it doesn't happen all the time. So just ignore this delta
  116. // as a non-relevant.
  117. if (Math.abs(vertScrollbar) !== 1) {
  118. width -= vertScrollbar;
  119. }
  120. if (Math.abs(horizScrollbar) !== 1) {
  121. height -= horizScrollbar;
  122. }
  123. }
  124. return createRectInit(paddings.left, paddings.top, width, height);
  125. }
  126. /**
  127. * Checks whether provided element is an instance of the SVGGraphicsElement.
  128. *
  129. * @param {Element} target - Element to be checked.
  130. * @returns {boolean}
  131. */
  132. const isSVGGraphicsElement = (() => {
  133. // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement
  134. // interface.
  135. if (typeof SVGGraphicsElement !== 'undefined') {
  136. return target => target instanceof getWindowOf(target).SVGGraphicsElement;
  137. }
  138. // If it's so, then check that element is at least an instance of the
  139. // SVGElement and that it has the "getBBox" method.
  140. // eslint-disable-next-line no-extra-parens
  141. return target => (
  142. target instanceof getWindowOf(target).SVGElement &&
  143. typeof target.getBBox === 'function'
  144. );
  145. })();
  146. /**
  147. * Checks whether provided element is a document element (<html>).
  148. *
  149. * @param {Element} target - Element to be checked.
  150. * @returns {boolean}
  151. */
  152. function isDocumentElement(target) {
  153. return target === getWindowOf(target).document.documentElement;
  154. }
  155. /**
  156. * Calculates an appropriate content rectangle for provided html or svg element.
  157. *
  158. * @param {Element} target - Element content rectangle of which needs to be calculated.
  159. * @returns {DOMRectInit}
  160. */
  161. export function getContentRect(target) {
  162. if (!isBrowser) {
  163. return emptyRect;
  164. }
  165. if (isSVGGraphicsElement(target)) {
  166. return getSVGContentRect(target);
  167. }
  168. return getHTMLElementContentRect(target);
  169. }
  170. /**
  171. * Creates rectangle with an interface of the DOMRectReadOnly.
  172. * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly
  173. *
  174. * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions.
  175. * @returns {DOMRectReadOnly}
  176. */
  177. export function createReadOnlyRect({x, y, width, height}) {
  178. // If DOMRectReadOnly is available use it as a prototype for the rectangle.
  179. const Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;
  180. const rect = Object.create(Constr.prototype);
  181. // Rectangle's properties are not writable and non-enumerable.
  182. defineConfigurable(rect, {
  183. x, y, width, height,
  184. top: y,
  185. right: x + width,
  186. bottom: height + y,
  187. left: x
  188. });
  189. return rect;
  190. }
  191. /**
  192. * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.
  193. * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit
  194. *
  195. * @param {number} x - X coordinate.
  196. * @param {number} y - Y coordinate.
  197. * @param {number} width - Rectangle's width.
  198. * @param {number} height - Rectangle's height.
  199. * @returns {DOMRectInit}
  200. */
  201. export function createRectInit(x, y, width, height) {
  202. return {x, y, width, height};
  203. }