123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import { delimiter } from '../modules/core/regex.js'
- import { globals } from '../utils/window.js'
- import { register } from '../utils/adopter.js'
- import { registerMethods } from '../utils/methods.js'
- import Matrix from './Matrix.js'
- import Point from './Point.js'
- import parser from '../modules/core/parser.js'
-
- export function isNulledBox (box) {
- return !box.width && !box.height && !box.x && !box.y
- }
-
- export function domContains (node) {
- return node === globals.document
- || (globals.document.documentElement.contains || function (node) {
- // This is IE - it does not support contains() for top-level SVGs
- while (node.parentNode) {
- node = node.parentNode
- }
- return node === globals.document
- }).call(globals.document.documentElement, node)
- }
-
- export default class Box {
- constructor (...args) {
- this.init(...args)
- }
-
- addOffset () {
- // offset by window scroll position, because getBoundingClientRect changes when window is scrolled
- this.x += globals.window.pageXOffset
- this.y += globals.window.pageYOffset
- return new Box(this)
- }
-
- init (source) {
- const base = [ 0, 0, 0, 0 ]
- source = typeof source === 'string'
- ? source.split(delimiter).map(parseFloat)
- : Array.isArray(source)
- ? source
- : typeof source === 'object'
- ? [ source.left != null
- ? source.left
- : source.x, source.top != null ? source.top : source.y, source.width, source.height ]
- : arguments.length === 4
- ? [].slice.call(arguments)
- : base
-
- this.x = source[0] || 0
- this.y = source[1] || 0
- this.width = this.w = source[2] || 0
- this.height = this.h = source[3] || 0
-
- // Add more bounding box properties
- this.x2 = this.x + this.w
- this.y2 = this.y + this.h
- this.cx = this.x + this.w / 2
- this.cy = this.y + this.h / 2
-
- return this
- }
-
- isNulled () {
- return isNulledBox(this)
- }
-
- // Merge rect box with another, return a new instance
- merge (box) {
- const x = Math.min(this.x, box.x)
- const y = Math.min(this.y, box.y)
- const width = Math.max(this.x + this.width, box.x + box.width) - x
- const height = Math.max(this.y + this.height, box.y + box.height) - y
-
- return new Box(x, y, width, height)
- }
-
- toArray () {
- return [ this.x, this.y, this.width, this.height ]
- }
-
- toString () {
- return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height
- }
-
- transform (m) {
- if (!(m instanceof Matrix)) {
- m = new Matrix(m)
- }
-
- let xMin = Infinity
- let xMax = -Infinity
- let yMin = Infinity
- let yMax = -Infinity
-
- const pts = [
- new Point(this.x, this.y),
- new Point(this.x2, this.y),
- new Point(this.x, this.y2),
- new Point(this.x2, this.y2)
- ]
-
- pts.forEach(function (p) {
- p = p.transform(m)
- xMin = Math.min(xMin, p.x)
- xMax = Math.max(xMax, p.x)
- yMin = Math.min(yMin, p.y)
- yMax = Math.max(yMax, p.y)
- })
-
- return new Box(
- xMin, yMin,
- xMax - xMin,
- yMax - yMin
- )
- }
-
- }
-
- function getBox (el, getBBoxFn, retry) {
- let box
-
- try {
- // Try to get the box with the provided function
- box = getBBoxFn(el.node)
-
- // If the box is worthless and not even in the dom, retry
- // by throwing an error here...
- if (isNulledBox(box) && !domContains(el.node)) {
- throw new Error('Element not in the dom')
- }
- } catch (e) {
- // ... and calling the retry handler here
- box = retry(el)
- }
-
- return box
- }
-
- export function bbox () {
- // Function to get bbox is getBBox()
- const getBBox = (node) => node.getBBox()
-
- // Take all measures so that a stupid browser renders the element
- // so we can get the bbox from it when we try again
- const retry = (el) => {
- try {
- const clone = el.clone().addTo(parser().svg).show()
- const box = clone.node.getBBox()
- clone.remove()
- return box
- } catch (e) {
- // We give up...
- throw new Error(`Getting bbox of element "${el.node.nodeName}" is not possible: ${e.toString()}`)
- }
- }
-
- const box = getBox(this, getBBox, retry)
- const bbox = new Box(box)
-
- return bbox
- }
-
- export function rbox (el) {
- const getRBox = (node) => node.getBoundingClientRect()
- const retry = (el) => {
- // There is no point in trying tricks here because if we insert the element into the dom ourselves
- // it obviously will be at the wrong position
- throw new Error(`Getting rbox of element "${el.node.nodeName}" is not possible`)
- }
-
- const box = getBox(this, getRBox, retry)
- const rbox = new Box(box)
-
- // If an element was passed, we want the bbox in the coordinate system of that element
- if (el) {
- return rbox.transform(el.screenCTM().inverseO())
- }
-
- // Else we want it in absolute screen coordinates
- // Therefore we need to add the scrollOffset
- return rbox.addOffset()
- }
-
- // Checks whether the given point is inside the bounding box
- export function inside (x, y) {
- const box = this.bbox()
-
- return x > box.x
- && y > box.y
- && x < box.x + box.width
- && y < box.y + box.height
- }
-
- registerMethods({
- viewbox: {
- viewbox (x, y, width, height) {
- // act as getter
- if (x == null) return new Box(this.attr('viewBox'))
-
- // act as setter
- return this.attr('viewBox', new Box(x, y, width, height))
- },
-
- zoom (level, point) {
- // Its best to rely on the attributes here and here is why:
- // clientXYZ: Doesn't work on non-root svgs because they dont have a CSSBox (silly!)
- // getBoundingClientRect: Doesn't work because Chrome just ignores width and height of nested svgs completely
- // that means, their clientRect is always as big as the content.
- // Furthermore this size is incorrect if the element is further transformed by its parents
- // computedStyle: Only returns meaningful values if css was used with px. We dont go this route here!
- // getBBox: returns the bounding box of its content - that doesn't help!
- let { width, height } = this.attr([ 'width', 'height' ])
-
- // Width and height is a string when a number with a unit is present which we can't use
- // So we try clientXYZ
- if ((!width && !height) || (typeof width === 'string' || typeof height === 'string')) {
- width = this.node.clientWidth
- height = this.node.clientHeight
- }
-
- // Giving up...
- if (!width || !height) {
- throw new Error('Impossible to get absolute width and height. Please provide an absolute width and height attribute on the zooming element')
- }
-
- const v = this.viewbox()
-
- const zoomX = width / v.width
- const zoomY = height / v.height
- const zoom = Math.min(zoomX, zoomY)
-
- if (level == null) {
- return zoom
- }
-
- let zoomAmount = zoom / level
-
- // Set the zoomAmount to the highest value which is safe to process and recover from
- // The * 100 is a bit of wiggle room for the matrix transformation
- if (zoomAmount === Infinity) zoomAmount = Number.MAX_SAFE_INTEGER / 100
-
- point = point || new Point(width / 2 / zoomX + v.x, height / 2 / zoomY + v.y)
-
- const box = new Box(v).transform(
- new Matrix({ scale: zoomAmount, origin: point })
- )
-
- return this.viewbox(box)
- }
- }
- })
-
- register(Box, 'Box')
|