HEX
Server: Apache
System: Linux p3plzcpnl506847.prod.phx3.secureserver.net 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: slfopp7cb1df (5698090)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/slfopp7cb1df/public_html/sitepacket.com/src/client/src/controllers/tooltipController.js
import { getElementRect, hexToRgb, isMobile, isPointInsidePolygon } from 'Editor/scripts/utilities'
import * as consts from 'Client/consts'
import * as editorConsts from 'Editor/scripts/consts'
import Tooltip from 'Client/UI/tooltip/tooltip'
import TooltipFullscreen from 'Client/UI/tooltip/tooltipFullscreen'

/*

Controls everything related to tooltips, like creating, showing, hiding, calculating properties, etc

*/

export default class TooltipController {
  store = undefined
  tooltipsAreFullscreen = false

  // The tooltip container
  container = document.createElement('div')

  // Current mouse coords
  // used to update tooltip position
  mouseCoords = { x: 0, y: 0 }

  // Array containing all Tooltip objects
  tooltips = []

  // Array containing indices of opened tooltips
  openedTooltips = new Set()

  // Allows to temporarily disable sticky tooltips
  stickyTooltips = undefined

  // Cache for quick calculations
  tooltipElements = {}

  // Timeouts for the tooltip in/out animation
  tooltipAnimationTimeouts = {}

  bodyOverflow = undefined

  constructor(store) {
    this.store = store

    if (!this.store.state.tooltips.enable_tooltips) return

    this.store.subscribe(this.handleAction.bind(this))

    // This is needed to enable and reset sticky tooltips by the API and the event controller
    this.stickyTooltips = this.store.state.tooltips.sticky_tooltips
    if (isMobile()) {
      this.stickyTooltips = false
    }

    // No sticky tooltips on mobile!
    if (isMobile()) {
      this.disableStickyTooltips()
    }

    // Fullscreen tooltips flag
    if (
      this.store.state.tooltips.fullscreen_tooltips === 'always' ||
      (this.store.state.tooltips.fullscreen_tooltips === 'mobile-only' && isMobile())
    ) {
      this.tooltipsAreFullscreen = true
    }

    this.setupContainer()
    this.createTooltips()
  }
  async handleAction(action) {
    if (action.type === 'updateTooltipPositions') {
      this.updateAllTooltipPositions()
    }
    if (action.type === 'closeFullscreenTooltip') {
      this.hideAllTooltips()
    }
    if (action.type === 'zoomUpdate') {
      this.updateAllTooltipPositions()
    }
  }
  disableStickyTooltips() {
    this.stickyTooltips = false
  }
  resetStickyTooltips() {
    if (isMobile()) return
    this.stickyTooltips = this.store.state.tooltips.sticky_tooltips
  }

  // Create various elements
  setupContainer() {
    // Remove tooltip containers from previous init, if it exists
    document
      .querySelectorAll(`.imp-tooltips-container[data-image-map-id="${this.store.getID()}"]`)
      ?.forEach((el) => el.remove())

    // Create new container
    this.container.classList.add('imp-tooltips-container')
    this.container.dataset.imageMapId = this.store.getID()

    // If tooltips are fullscreen, add class to the container
    if (this.tooltipsAreFullscreen) {
      this.container.classList.add('is-fullscreen')
      let color_bg = hexToRgb(this.store.state.tooltips.fullscreen_background) || { r: 0, b: 0, g: 0 }
      this.container.style.background = `rgba(${color_bg.r}, ${color_bg.g}, ${color_bg.b}, ${this.store.state.tooltips.fullscreen_background_opacity})`
    }
  }
  createTooltips() {
    let html = ''

    for (let obj of this.store.getObjects()) {
      if (obj.parent) obj = this.store.getObject({ id: obj.parent })

      if (!obj.tooltip.enable_tooltip) continue

      // Text objects don't have tooltips
      if (obj.type === editorConsts.OBJECT_TEXT) continue

      // Create a Tooltip object
      let tooltip = undefined
      if (this.tooltipsAreFullscreen) {
        tooltip = new TooltipFullscreen({
          style: obj.tooltip_style,
          content: obj.tooltip_content,
          animation: this.store.state.tooltips.tooltip_animation,
          id: obj.id,
        })
      } else {
        tooltip = new Tooltip({
          style: obj.tooltip_style,
          content: obj.tooltip_content,
          animation: this.store.state.tooltips.tooltip_animation,
          id: obj.id,
          title: obj.title,
        })
      }

      // Store the tooltip object for future reference
      this.tooltips[obj.id] = tooltip

      // Get html
      html += tooltip.html()
    }

    // Add the tooltips to the container
    this.container.innerHTML = html

    // Sticky tooltips class
    if (this.stickyTooltips) {
      this.container.classList.add('imp-sticky-tooltips')
    }

    // Clear cache
    this.tooltipElements = {}
  }
  getTooltipElement(id) {
    id = this.store.getObject({ id }).parent || id

    // Cache
    if (!this.tooltipElements[id]) {
      this.tooltipElements[id] = this.container.querySelector(`[data-tooltip-id="${id}"]`)
    }

    return this.tooltipElements[id]
  }

  // Show/hide tooltips
  showTooltip(id) {
    if (window.startchanging) debugger
    id = this.store.getObject({ id }).parent || id

    // Check if tooltip element exists
    if (!this.getTooltipElement(id)) return

    // Check if object exists
    if (!this.store.getObject({ id })) return

    return new Promise(async (resolve) => {
      let object = this.store.getObject({ id })

      // If the object doesn't have tooltip enabled, then return
      if (!object.tooltip.enable_tooltip) {
        resolve()
        return
      }

      // i = the index of the tooltip
      if (object.type === editorConsts.OBJECT_TEXT) {
        resolve()
        return
      }

      // If the tooltip is already visible, then return
      if (this.openedTooltips.has(id)) {
        resolve()
        return
      }

      // Add tooltip to the list of opened tooltips
      this.openedTooltips.add(id)

      // Show fullscreen or normal tooltips
      if (
        (this.store.state.tooltips.fullscreen_tooltips === 'mobile-only' && isMobile()) ||
        this.store.state.tooltips.fullscreen_tooltips === 'always'
      ) {
        // Fullscreen tooltips
        this.animateFullscreenTooltipIn(id)
        requestAnimationFrame(resolve)
      } else {
        // Normal tooltips
        let el = this.getTooltipElement(id)
        clearTimeout(this.tooltipAnimationTimeouts[id])
        el.style.display = 'inline-block'
        el.style.transitionProperty = 'none'
        el.style.transform = 'none'
        this.updateTooltipPosition(id)
        this.animateTooltipIn(id)
        requestAnimationFrame(resolve)
      }

      // Send event
      ImageMapPro.trigger({
        type: consts.HOOK_TOOLTIP_SHOW,
        payload: {
          map: this.store.state.general.name,
          object: this.store.getObject({ id }).title,
        },
      })
    })
  }
  hideTooltip(id) {
    id = this.store.getObject({ id }).parent || id

    return new Promise((resolve) => {
      let object = this.store.getObject({ id })
      if (object.type === editorConsts.OBJECT_TEXT) {
        resolve()
        return
      }

      // Remove from the list of opened tooltips
      if (!this.openedTooltips.has(id)) {
        resolve()
        return
      } else {
        this.openedTooltips.delete(id)
      }

      // Hide mobile tooltip
      if (
        (this.store.state.tooltips.fullscreen_tooltips === 'mobile-only' && isMobile()) ||
        this.store.state.tooltips.fullscreen_tooltips === 'always'
      ) {
        this.animateFullscreenTooltipOut(id)
        requestAnimationFrame(resolve)
      } else {
        // Hide normal tooltip
        this.animateTooltipOut(id)
        requestAnimationFrame(resolve)
      }

      // Send event
      ImageMapPro.trigger({
        type: consts.HOOK_TOOLTIP_HIDE,
        payload: {
          map: this.store.state.general.name,
          object: this.store.getObject({ id }).title,
        },
      })
    })
  }
  hideAllTooltips() {
    return new Promise((resolve) => {
      this.openedTooltips.forEach((id) => this.hideTooltip(id))
      requestAnimationFrame(resolve)
    })
  }
  updateTooltipPosition(id) {
    if (this.tooltipsAreFullscreen) return
    id = this.store.getObject({ id }).parent || id

    let tooltipEl = this.getTooltipElement(id)
    let objectRect = this.getObjectVisibleRect(id)

    let distance = 20
    let tooltipRect = tooltipEl.getBoundingClientRect()

    // Calculate and set the position
    let pos = { x: 0, y: 0 }
    if (this.tooltips[id].style.position === 'left') {
      pos.x = objectRect.x - distance - tooltipRect.width
      pos.y = objectRect.y + objectRect.height / 2 - tooltipRect.height / 2
    }
    if (this.tooltips[id].style.position === 'right') {
      pos.x = objectRect.x + objectRect.width + distance
      pos.y = objectRect.y + objectRect.height / 2 - tooltipRect.height / 2
    }
    if (this.tooltips[id].style.position === 'top') {
      pos.x = objectRect.x + objectRect.width / 2 - tooltipRect.width / 2
      pos.y = objectRect.y - distance - tooltipRect.height
    }
    if (this.tooltips[id].style.position === 'bottom') {
      pos.x = objectRect.x + objectRect.width / 2 - tooltipRect.width / 2
      pos.y = objectRect.y + objectRect.height + distance
    }

    // Apply offset
    pos.x += (this.tooltips[id].style.offset_x / 100) * this.store.getCanvasWrapRect().width
    pos.y += (this.tooltips[id].style.offset_y / 100) * this.store.getCanvasWrapRect().height

    // Set
    tooltipEl.parentNode.style.left =
      pos.x + this.store.getCanvasWrapRect().offset.left - this.store.getTooltipsContainerRect().offset.left + 'px'
    tooltipEl.parentNode.style.top =
      pos.y + this.store.getCanvasWrapRect().offset.top - this.store.getTooltipsContainerRect().offset.top + 'px'

    // Constrain
    if (this.store.state.tooltips.constrain_tooltips) {
      tooltipRect = tooltipEl.getBoundingClientRect()

      if (tooltipRect.x < 0) pos.x -= tooltipRect.x
      if (tooltipRect.y < 0) pos.y -= tooltipRect.y
      if (tooltipRect.x + tooltipRect.width > window.innerWidth)
        pos.x += window.innerWidth - (tooltipRect.x + tooltipRect.width)
      if (tooltipRect.y + tooltipRect.height > window.innerHeight)
        pos.y += window.innerHeight - (tooltipRect.y + tooltipRect.height)

      tooltipEl.parentNode.style.left =
        pos.x + this.store.getCanvasWrapRect().offset.left - this.store.getTooltipsContainerRect().offset.left + 'px'
      tooltipEl.parentNode.style.top =
        pos.y + this.store.getCanvasWrapRect().offset.top - this.store.getTooltipsContainerRect().offset.top + 'px'
    }
  }
  updateAllTooltipPositions() {
    for (let tooltipIndex of this.openedTooltips) {
      this.updateTooltipPosition(tooltipIndex)
    }
  }
  animateTooltipIn(id) {
    let el = this.getTooltipElement(id)

    if (this.store.state.tooltips.sticky_tooltips) {
      el.style.opacity = 1
      return
    }

    if (this.store.state.tooltips.tooltip_animation === 'grow') {
      el.style.transitionProperty = 'none'
      el.style.transform = 'scale(0, 0)'
      el.style.opacity = 1
      clearTimeout(this.tooltipAnimationTimeouts[id])
      this.tooltipAnimationTimeouts[id] = requestAnimationFrame(() => {
        el.style.transitionProperty = 'transform'
        el.style.transform = 'scale(1, 1)'
      })

      return
    }

    if (this.store.state.tooltips.tooltip_animation === 'fade') {
      el.style.transitionProperty = 'none'
      el.style.opacity = 0
      clearTimeout(this.tooltipAnimationTimeouts[id])
      this.tooltipAnimationTimeouts[id] = requestAnimationFrame(() => {
        el.style.transitionProperty = 'opacity'
        el.style.opacity = 1
      })

      return
    }

    el.style.opacity = 1
  }
  animateTooltipOut(id) {
    let tooltip = this.getTooltipElement(id)

    // Stop all videos
    tooltip.querySelectorAll('video').forEach(function (element) {
      element.dispatchEvent(new Event('pause'))
    })
    tooltip.querySelectorAll('iframe').forEach(function (element) {
      element.setAttribute('src', element.getAttribute('src'))
    })

    if (this.store.state.tooltips.sticky_tooltips) {
      tooltip.style.opacity = 0
      tooltip.style.display = 'none'
      return
    }

    if (this.store.state.tooltips.tooltip_animation === 'grow') {
      tooltip.style.transform = 'scale(0, 0)'
      clearTimeout(this.tooltipAnimationTimeouts[id])
      this.tooltipAnimationTimeouts[id] = setTimeout(() => {
        tooltip.style.opacity = 0
        tooltip.style.display = 'none'
      }, 200)

      return
    }

    if (this.store.state.tooltips.tooltip_animation === 'fade') {
      tooltip.style.opacity = 0
      tooltip.style.display = 'none'
      return
    }

    tooltip.style.opacity = 0
    tooltip.style.display = 'none'
  }
  animateFullscreenTooltipIn(id) {
    // Container
    this.container.style.display = 'block'
    this.container.style.opacity = 1

    // Tooltip
    let tooltip = this.getTooltipElement(id)
    tooltip.style.display = 'flex'
    tooltip.style.opacity = 0
    tooltip.style.transform = 'scale(0.33)'
    requestAnimationFrame(() => {
      tooltip.style.opacity = 1
      tooltip.style.transform = 'scale(1)'
    })

    // Prevent scrolling of the body and store the original overflow attribute value
    this.bodyOverflow = document.body.style.overflow
    document.body.style.overflow = 'hidden'
  }
  animateFullscreenTooltipOut(id) {
    // Container
    this.container.style.opacity = 0
    clearTimeout(this.tooltipAnimationTimeouts[id + '-container'])
    this.tooltipAnimationTimeouts[id + '-container'] = setTimeout(() => {
      this.container.style.display = 'none'
    }, 350)

    // Tooltip
    let tooltip = this.getTooltipElement(id)
    tooltip.style.opacity = 0
    tooltip.style.transform = 'scale(0.33)'
    clearTimeout(this.tooltipAnimationTimeouts[id])
    this.tooltipAnimationTimeouts[id] = setTimeout(() => {
      tooltip.style.display = 'none'
    }, 350)

    // Stop all videos
    tooltip.querySelectorAll('video').forEach(function (element) {
      element.dispatchEvent(new Event('pause'))
    })
    tooltip.querySelectorAll('iframe').forEach(function (element) {
      element.setAttribute('src', element.getAttribute('src'))
    })

    // Restore original body overflow
    document.body.style.overflow = this.bodyOverflow
  }

  // Calcs
  isPointInsideVisibleTooltip({ x, y }) {
    // If a fullscreen tooltip is open, return true
    if (this.tooltipsAreFullscreen && this.openedTooltips.size > 0) {
      return true
    }

    for (let id of this.openedTooltips) {
      let polys = this.createPolysForTooltip(id)
      if (this.stickyTooltips) {
        if (isPointInsidePolygon(x, y, polys.polyTooltipOnly)) {
          return true
        }
      } else {
        if (isPointInsidePolygon(x, y, polys.poly) || isPointInsidePolygon(x, y, polys.polyTooltipOnly)) {
          return true
        }
      }
    }

    return false
  }
  createPolysForTooltip(id) {
    id = this.store.getObject({ id }).parent || id

    // Used when calculating the poly size
    // need to use getBoundingClientRect() because element.offsetWidth is undefined?
    // window scroll must be added, because the mouse coordiates and the tooltip's rect includes scroll
    let objectRect = this.store.getObjectController().objects[id].getBoundingClientRect()
    let objectCenterX = objectRect.x + window.scrollX + objectRect.width / 2
    let objectCenterY = objectRect.y + window.scrollY + objectRect.height / 2

    // Cache tooltip dimensions and coords
    let tooltipRect = getElementRect(this.getTooltipElement(id))
    let tx = tooltipRect.offset.left
    let ty = tooltipRect.offset.top
    let tw = tooltipRect.offsetWidth
    let th = tooltipRect.offsetHeight

    // Create a polygon, representing the tooltip area + buffer space
    // Poly without buffer is needed, in case the tooltip gets moved due to window constrain
    // and the object center ends up inside the tooltip
    let poly = []
    let polyTooltipOnly = []

    if (this.store.getObject({ id }).tooltip_style.position === 'top') {
      poly = [
        [tx, ty],
        [tx + tw, ty],
        [tx + tw, ty + th],
        [tx + tw / 2, objectCenterY],
        [tx, ty + th],
      ]
      polyTooltipOnly = [
        [tx, ty],
        [tx + tw, ty],
        [tx + tw, ty + th],
        [tx, ty + th],
      ]
    }
    if (this.store.getObject({ id }).tooltip_style.position === 'bottom') {
      poly = [
        [tx, ty],
        [tx + tw / 2, objectCenterY],
        [tx + tw, ty],
        [tx + tw, ty + th],
        [tx, ty + th],
      ]
      polyTooltipOnly = [
        [tx, ty],
        [tx + tw, ty],
        [tx + tw, ty + th],
        [tx, ty + th],
      ]
    }
    if (this.store.getObject({ id }).tooltip_style.position === 'left') {
      poly = [
        [tx, ty],
        [tx + tw, ty],
        [objectCenterX, ty + th / 2],
        [tx + tw, ty + th],
        [tx, ty + th],
      ]
      polyTooltipOnly = [
        [tx, ty],
        [tx + tw, ty],
        [tx + tw, ty + th],
        [tx, ty + th],
      ]
    }
    if (this.store.getObject({ id }).tooltip_style.position === 'right') {
      poly = [
        [tx, ty],
        [tx + tw, ty],
        [tx + tw, ty + th],
        [tx, ty + th],
        [objectCenterX, ty + th / 2],
      ]
      polyTooltipOnly = [
        [tx, ty],
        [tx + tw, ty],
        [tx + tw, ty + th],
        [tx, ty + th],
      ]
    }

    return { poly, polyTooltipOnly }
  }
  getObjectVisibleRect(id) {
    if (!this.store.getObjectController().objects[id]) return

    let sx
    let sy
    let sw
    let sh

    let object = this.store.getObject({ id })
    let objectRect = this.store.getObjectController().objects[id].getRect()

    let windowWidth = window.innerWidth
    let windowHeight = window.innerHeight

    if (this.stickyTooltips) {
      // Sticky tooltips
      // Set width/height of the shape to 0
      // and X and Y to the mouse coordinates
      sx = this.store.getEventCoordinates().x - this.store.getCanvasWrapRect().offset.left
      sy = this.store.getEventCoordinates().y - this.store.getCanvasWrapRect().offset.top

      sw = 0
      sh = 0
    } else {
      sw = (objectRect.width / 100) * this.store.getCanvasWrapRect().width
      sh = (objectRect.height / 100) * this.store.getCanvasWrapRect().height

      sw = sw * this.store.getZoom()
      sh = sh * this.store.getZoom()

      sx = (objectRect.x / 100) * this.store.getCanvasWrapRect().width
      sy = (objectRect.y / 100) * this.store.getCanvasWrapRect().height

      sx = sx * this.store.getZoom() + this.store.getPan().x
      sy = sy * this.store.getZoom() + this.store.getPan().y
    }

    if (object.type === editorConsts.OBJECT_SPOT && this.store.state.objectConfig.scale_spots) {
      let scaledWidth = sw / this.store.getZoom()
      let scaledHeight = sh / this.store.getZoom()
      sx += sw / 2 - scaledWidth / 2

      if (object.default_style.use_icon && object.default_style.icon_is_pin) {
        sy += sh - scaledHeight
      } else {
        sy += sh / 2 - scaledHeight / 2
      }

      sw = scaledWidth
      sh = scaledHeight
    }

    // Limit the rect of the object to the bounds of the wrap
    if (this.store.getIsFullscreen() && this.store.state.tooltips.constrain_tooltips) {
      // In fullscreen mode compensate for the scroll
      let canvasOffsetLeft = this.store.getCanvasWrapRect().offset.left - window.scrollX
      let canvasOffsetTop = this.store.getCanvasWrapRect().offset.top - window.scrollY

      if (sx + canvasOffsetLeft < 0) {
        sw = sw + sx + canvasOffsetLeft
        sx = -canvasOffsetLeft
      }
      if (sx + canvasOffsetLeft + sw > windowWidth) {
        sw += windowWidth - (sx + canvasOffsetLeft + sw)
      }
      if (sy + canvasOffsetTop < 0) {
        sh = sh + sy + canvasOffsetTop
        sy = -canvasOffsetTop
      }
      if (sy + canvasOffsetTop + sh > windowHeight) {
        sh += windowHeight - (sy + canvasOffsetTop + sh)
      }
    } else {
      if (sx < 0) {
        sw = sw + sx
        sx = 0
      }
      if (sx + sw > this.store.getCanvasWrapRect().width) {
        sw = this.store.getCanvasWrapRect().width - sx
      }
      if (sy < 0) {
        sh = sh + sy
        sy = 0
      }
      if (sy + sh > this.store.getCanvasWrapRect().height) {
        sh = this.store.getCanvasWrapRect().height - sy
      }
    }

    return {
      x: sx,
      y: sy,
      width: sw,
      height: sh,
    }
  }
}