Your IP : 216.73.216.79


Current Path : /var/www/html/reddsis/node_modules/apexcharts/src/modules/
Upload File :
Current File : /var/www/html/reddsis/node_modules/apexcharts/src/modules/Toolbar.js

// @ts-check
import Graphics from './Graphics'
import Exports from './Exports'
import Utils from './../utils/Utils'
import { BrowserAPIs } from '../ssr/BrowserAPIs.js'
import icoPan from './../assets/ico-pan-hand.svg'
import icoZoom from './../assets/ico-zoom-in.svg'
import icoReset from './../assets/ico-home.svg'
import icoZoomIn from './../assets/ico-plus.svg'
import icoZoomOut from './../assets/ico-minus.svg'
import icoSelect from './../assets/ico-select.svg'
import icoMenu from './../assets/ico-menu.svg'

/**
 * ApexCharts Toolbar Class for creating toolbar in axis based charts.
 *
 * @module Toolbar
 **/

export default class Toolbar {
  /**
   * @param {import('../types/internal').ChartStateW} w
   * @param {import('../types/internal').ChartContext} ctx
   */
  constructor(w, ctx) {
    this.w = w
    this.ctx = ctx // needed: getSyncedCharts, fireEvent, user callbacks, Exports

    this.ev = this.w.config.chart.events
    this.selectedClass = 'apexcharts-selected'

    this.localeValues = this.w.globals.locale.toolbar

    this.minX = w.globals.minX
    this.maxX = w.globals.maxX
    /** @type {HTMLElement | null} */ this.elZoom = null
    /** @type {HTMLElement | null} */ this.elZoomIn = null
    /** @type {HTMLElement | null} */ this.elZoomOut = null
    /** @type {HTMLElement | null} */ this.elPan = null
    /** @type {HTMLElement | null} */ this.elSelection = null
    /** @type {HTMLElement | null} */ this.elZoomReset = null
    /** @type {HTMLElement | null} */ this.elMenuIcon = null
    /** @type {HTMLElement | null} */ this.elMenu = null
    /** @type {HTMLElement[]} */ this.elMenuItems = []
    /** @type {any} */ this.t = null
  }

  createToolbar() {
    const w = this.w

    const createDiv = () => {
      return BrowserAPIs.createElementNS('http://www.w3.org/1999/xhtml', 'div')
    }
    // Toolbar controls are native <button> elements (WCAG 1st rule of ARIA:
    // prefer real buttons over div[role=button]). Type defaults to "submit"
    // inside forms, so we set it explicitly.
    const createBtn = () => {
      const btn = /** @type {HTMLButtonElement} */ (
        BrowserAPIs.createElementNS('http://www.w3.org/1999/xhtml', 'button')
      )
      btn.setAttribute('type', 'button')
      return btn
    }
    const elToolbarWrap = createDiv()
    elToolbarWrap.setAttribute('class', 'apexcharts-toolbar')
    elToolbarWrap.style.top = w.config.chart.toolbar.offsetY + 'px'
    elToolbarWrap.style.right = -w.config.chart.toolbar.offsetX + 3 + 'px'
    w.dom.elWrap.appendChild(elToolbarWrap)

    this.elZoom = createBtn()
    this.elZoomIn = createBtn()
    this.elZoomOut = createBtn()
    this.elPan = createBtn()
    this.elSelection = createBtn()
    this.elZoomReset = createBtn()
    this.elMenuIcon = createBtn()
    // The menu (popup container) stays a <div> — it's not a button.
    this.elMenu = createDiv()
    /** @type {any} */
    this.elCustomIcons = []

    this.t = w.config.chart.toolbar.tools

    if (Array.isArray(this.t.customIcons)) {
      for (let i = 0; i < this.t.customIcons.length; i++) {
        this.elCustomIcons.push(createBtn())
      }
    }

    /** @type {any[]} */
    const toolbarControls = []

    /**
     * @param {string} type
     * @param {Element} el
     * @param {string} ico
     */
    const appendZoomControl = (type, el, ico) => {
      const tool = type.toLowerCase()
      if (this.t[tool] && w.config.chart.zoom.enabled) {
        toolbarControls.push({
          el,
          icon: typeof this.t[tool] === 'string' ? this.t[tool] : ico,
          title: /** @type {any} */ (this.localeValues)[type],
          class: `apexcharts-${tool}-icon`,
        })
      }
    }

    appendZoomControl('zoomIn', this.elZoomIn, icoZoomIn)
    appendZoomControl('zoomOut', this.elZoomOut, icoZoomOut)

    /**
     * @param {string} z
     */
    const zoomSelectionCtrls = (z) => {
      if (this.t[z] && w.config.chart[z].enabled) {
        toolbarControls.push({
          el: z === 'zoom' ? this.elZoom : this.elSelection,
          icon:
            typeof this.t[z] === 'string'
              ? this.t[z]
              : z === 'zoom'
                ? icoZoom
                : icoSelect,
          title: /** @type {any} */ (this.localeValues)[
            z === 'zoom' ? 'selectionZoom' : 'selection'
          ],
          class: `apexcharts-${z}-icon`,
        })
      }
    }
    zoomSelectionCtrls('zoom')
    zoomSelectionCtrls('selection')

    if (this.t.pan && w.config.chart.zoom.enabled) {
      toolbarControls.push({
        el: this.elPan,
        icon: typeof this.t.pan === 'string' ? this.t.pan : icoPan,
        title: this.localeValues.pan,
        class: 'apexcharts-pan-icon',
      })
    }

    appendZoomControl('reset', this.elZoomReset, icoReset)

    if (this.t.download) {
      toolbarControls.push({
        el: this.elMenuIcon,
        icon: typeof this.t.download === 'string' ? this.t.download : icoMenu,
        title: this.localeValues.menu,
        class: 'apexcharts-menu-icon',
      })
    }

    for (let i = 0; i < this.elCustomIcons.length; i++) {
      toolbarControls.push({
        el: this.elCustomIcons[i],
        icon: this.t.customIcons[i].icon,
        title: this.t.customIcons[i].title,
        index: this.t.customIcons[i].index,
        class: 'apexcharts-toolbar-custom-icon ' + this.t.customIcons[i].class,
      })
    }

    toolbarControls.forEach((t, index) => {
      if (t.index) {
        Utils.moveIndexInArray(toolbarControls, index, t.index)
      }
    })

    for (let i = 0; i < toolbarControls.length; i++) {
      // <button> already has implicit role="button" and is keyboard-focusable.
      Graphics.setAttrs(toolbarControls[i].el, {
        class: toolbarControls[i].class,
        title: toolbarControls[i].title,
        'aria-label': toolbarControls[i].title,
      })

      toolbarControls[i].el.innerHTML = toolbarControls[i].icon
      elToolbarWrap.appendChild(toolbarControls[i].el)
    }

    // Toggle-button ARIA pressed state for zoom / selection / pan
    if (this.elZoom.parentNode) {
      this.elZoom.setAttribute('aria-pressed', String(!!w.interact.zoomEnabled))
    }
    if (this.elSelection.parentNode) {
      this.elSelection.setAttribute(
        'aria-pressed',
        String(!!w.interact.selectionEnabled),
      )
    }
    if (this.elPan.parentNode) {
      this.elPan.setAttribute('aria-pressed', String(!!w.interact.panEnabled))
    }

    // Menu icon: popup indicator
    if (this.elMenuIcon.parentNode) {
      this.elMenuIcon.setAttribute('aria-haspopup', 'true')
      this.elMenuIcon.setAttribute('aria-expanded', 'false')
    }

    this._createHamburgerMenu(elToolbarWrap)

    if (w.interact.zoomEnabled) {
      this.elZoom.classList.add(this.selectedClass)
    } else if (w.interact.panEnabled) {
      this.elPan.classList.add(this.selectedClass)
    } else if (w.interact.selectionEnabled) {
      this.elSelection.classList.add(this.selectedClass)
    }

    this.addToolbarEventListeners()
  }

  /**
   * @param {Element} parent
   */
  _createHamburgerMenu(parent) {
    this.elMenuItems = []
    parent.appendChild(/** @type {Node} */ (this.elMenu))

    Graphics.setAttrs(this.elMenu, {
      class: 'apexcharts-menu',
      role: 'menu',
    })

    const menuItems = [
      {
        name: 'exportSVG',
        title: this.localeValues.exportToSVG,
      },
      {
        name: 'exportPNG',
        title: this.localeValues.exportToPNG,
      },
      {
        name: 'exportCSV',
        title: this.localeValues.exportToCSV,
      },
    ]

    for (let i = 0; i < menuItems.length; i++) {
      this.elMenuItems.push(
        BrowserAPIs.createElementNS('http://www.w3.org/1999/xhtml', 'div'),
      )
      this.elMenuItems[i].innerHTML = menuItems[i].title
      Graphics.setAttrs(this.elMenuItems[i], {
        class: `apexcharts-menu-item ${menuItems[i].name}`,
        title: menuItems[i].title,
        role: 'menuitem',
        tabindex: '-1',
      })
      ;/** @type {HTMLElement} */ (this.elMenu).appendChild(this.elMenuItems[i])
    }
  }

  addToolbarEventListeners() {
    this.elZoomReset?.addEventListener('click', this.handleZoomReset.bind(this))
    this.elSelection?.addEventListener(
      'click',
      this.toggleZoomSelection.bind(this, 'selection'),
    )
    this.elZoom?.addEventListener(
      'click',
      this.toggleZoomSelection.bind(this, 'zoom'),
    )
    this.elZoomIn?.addEventListener('click', this.handleZoomIn.bind(this))
    this.elZoomOut?.addEventListener('click', this.handleZoomOut.bind(this))
    this.elPan?.addEventListener('click', this.togglePanning.bind(this))
    this.elMenuIcon?.addEventListener('click', this.toggleMenu.bind(this))
    this.elMenuItems.forEach((m) => {
      if (m.classList.contains('exportSVG')) {
        m.addEventListener('click', this.handleDownload.bind(this, 'svg'))
      } else if (m.classList.contains('exportPNG')) {
        m.addEventListener('click', this.handleDownload.bind(this, 'png'))
      } else if (m.classList.contains('exportCSV')) {
        m.addEventListener('click', this.handleDownload.bind(this, 'csv'))
      }
    })
    for (let i = 0; i < this.t.customIcons.length; i++) {
      this.elCustomIcons[i].addEventListener(
        'click',
        this.t.customIcons[i].click.bind(this, this.ctx, this.ctx.w),
      )
    }

    // ── Keyboard accessibility ──────────────────────────────────────────────

    // Activate any toolbar button (not menu items) with Enter or Space
    const toolbarButtons = [
      this.elZoomReset,
      this.elSelection,
      this.elZoom,
      this.elZoomIn,
      this.elZoomOut,
      this.elPan,
      this.elMenuIcon,
      ...this.elCustomIcons,
    ]
    toolbarButtons.forEach((btn) => {
      /**
       * @param {Event} e
       */
      btn.addEventListener('keydown', (/** @type {any} */ e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault()
          // Capture the button's class before click triggers a potential
          // re-render (zoom-in/out/reset recreate the toolbar DOM, which
          // removes the focused element and loses focus — same issue as legend).
          const btnClass = btn.className
          btn.click()
          // After re-render, restore focus to the equivalent new button so the
          // user can keep operating without having to re-tab to the toolbar.
          requestAnimationFrame(() => {
            const baseEl = this.w.dom.baseEl
            if (!baseEl) return
            // Match on the first apexcharts-specific class (e.g. apexcharts-zoomin-icon)
            const apexClass = btnClass
              .split(' ')
              /**
               * @param {string} c
               */
              .find((/** @type {any} */ c) => c.startsWith('apexcharts-'))
            if (!apexClass) return
            const restored = baseEl.querySelector(`.${apexClass}`)
            if (restored) /** @type {HTMLElement} */ (restored).focus()
          })
        }
      })
    })

    // Menu keyboard navigation: Arrow keys move focus, Escape closes menu
    this.elMenuIcon?.addEventListener(
      'keydown',
      (/** @type {KeyboardEvent} */ e) => {
        if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
          e.preventDefault()
          if (!this.elMenu?.classList.contains('apexcharts-menu-open')) {
            this.toggleMenu()
          }
          // Focus first (ArrowDown) or last (ArrowUp) menu item after menu renders
          window.setTimeout(() => {
            const idx = e.key === 'ArrowDown' ? 0 : this.elMenuItems.length - 1
            if (this.elMenuItems[idx])
              /** @type {HTMLElement} */ (this.elMenuItems[idx]).focus()
          }, 20)
        }
      },
    )

    this.elMenuItems.forEach((m, idx) => {
      m.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => {
        if (e.key === 'ArrowDown') {
          e.preventDefault()
          const next = this.elMenuItems[idx + 1] || this.elMenuItems[0]
          next.focus()
        } else if (e.key === 'ArrowUp') {
          e.preventDefault()
          const prev =
            this.elMenuItems[idx - 1] ||
            this.elMenuItems[this.elMenuItems.length - 1]
          prev.focus()
        } else if (e.key === 'Escape' || e.key === 'Tab') {
          this._closeMenu()
          this.elMenuIcon?.focus()
          if (e.key === 'Tab') {
            // Allow Tab to continue natural flow — do not prevent default
          } else {
            e.preventDefault()
          }
        } else if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault()
          m.click()
        }
      })
    })
  }

  /**
   * @param {string} type
   */
  toggleZoomSelection(type) {
    const charts = this.ctx.getSyncedCharts()

    /**
     * @param {Record<string, any>} ch
     */
    charts.forEach((ch) => {
      ch.ctx.toolbar.toggleOtherControls()

      const el =
        type === 'selection'
          ? ch.ctx.toolbar.elSelection
          : ch.ctx.toolbar.elZoom
      const enabledType =
        type === 'selection' ? 'selectionEnabled' : 'zoomEnabled'

      ch.w.globals[enabledType] = !ch.w.globals[enabledType]

      if (!el.classList.contains(ch.ctx.toolbar.selectedClass)) {
        el.classList.add(ch.ctx.toolbar.selectedClass)
      } else {
        el.classList.remove(ch.ctx.toolbar.selectedClass)
      }

      el.setAttribute('aria-pressed', String(ch.w.globals[enabledType]))
    })
  }

  getToolbarIconsReference() {
    const w = this.w
    if (!this.elZoom) {
      this.elZoom = w.dom.baseEl.querySelector('.apexcharts-zoom-icon')
    }
    if (!this.elPan) {
      this.elPan = w.dom.baseEl.querySelector('.apexcharts-pan-icon')
    }
    if (!this.elSelection) {
      this.elSelection = w.dom.baseEl.querySelector(
        '.apexcharts-selection-icon',
      )
    }
  }

  /**
   * @param {string} type
   */
  enableZoomPanFromToolbar(type) {
    this.toggleOtherControls()

    type === 'pan'
      ? (this.w.interact.panEnabled = true)
      : (this.w.interact.zoomEnabled = true)

    const el = type === 'pan' ? this.elPan : this.elZoom
    const el2 = type === 'pan' ? this.elZoom : this.elPan
    if (el) {
      el.classList.add(this.selectedClass)
    }
    if (el2) {
      el2.classList.remove(this.selectedClass)
    }
  }

  togglePanning() {
    const charts = this.ctx.getSyncedCharts()

    /**
     * @param {Record<string, any>} ch
     */
    charts.forEach((ch) => {
      ch.ctx.toolbar.toggleOtherControls()
      ch.w.interact.panEnabled = !ch.w.interact.panEnabled

      if (
        !ch.ctx.toolbar.elPan.classList.contains(ch.ctx.toolbar.selectedClass)
      ) {
        ch.ctx.toolbar.elPan.classList.add(ch.ctx.toolbar.selectedClass)
      } else {
        ch.ctx.toolbar.elPan.classList.remove(ch.ctx.toolbar.selectedClass)
      }

      ch.ctx.toolbar.elPan.setAttribute(
        'aria-pressed',
        String(ch.w.interact.panEnabled),
      )
    })
  }

  toggleOtherControls() {
    const w = this.w
    w.interact.panEnabled = false
    w.interact.zoomEnabled = false
    w.interact.selectionEnabled = false

    this.getToolbarIconsReference()

    const toggleEls = [this.elPan, this.elSelection, this.elZoom]
    toggleEls.forEach((el) => {
      if (el) {
        el.classList.remove(this.selectedClass)
      }
    })
  }

  handleZoomIn() {
    const w = this.w

    if (w.axisFlags.isRangeBar) {
      this.minX = w.globals.minY
      this.maxX = w.globals.maxY
    }

    const centerX = (this.minX + this.maxX) / 2
    const newMinX = (this.minX + centerX) / 2
    const newMaxX = (this.maxX + centerX) / 2

    const newMinXMaxX = this._getNewMinXMaxX(newMinX, newMaxX)

    if (!w.interact.disableZoomIn) {
      this.zoomUpdateOptions(newMinXMaxX.minX, newMinXMaxX.maxX)
    }
  }

  handleZoomOut() {
    const w = this.w

    if (w.axisFlags.isRangeBar) {
      this.minX = w.globals.minY
      this.maxX = w.globals.maxY
    }

    // avoid zooming out beyond 1000 which may result in NaN values being printed on x-axis
    if (
      w.config.xaxis.type === 'datetime' &&
      new Date(this.minX).getUTCFullYear() < 1000
    ) {
      return
    }

    const centerX = (this.minX + this.maxX) / 2
    const newMinX = this.minX - (centerX - this.minX)
    const newMaxX = this.maxX - (centerX - this.maxX)

    const newMinXMaxX = this._getNewMinXMaxX(newMinX, newMaxX)

    if (!w.interact.disableZoomOut) {
      this.zoomUpdateOptions(newMinXMaxX.minX, newMinXMaxX.maxX)
    }
  }

  /**
   * @param {number} newMinX
   * @param {number} newMaxX
   */
  _getNewMinXMaxX(newMinX, newMaxX) {
    const shouldFloor = this.w.config.xaxis.convertedCatToNumeric
    return {
      minX: shouldFloor ? Math.floor(newMinX) : newMinX,
      maxX: shouldFloor ? Math.floor(newMaxX) : newMaxX,
    }
  }

  /**
   * @param {number} newMinX
   * @param {number} newMaxX
   */
  zoomUpdateOptions(newMinX, newMaxX) {
    const w = this.w

    if (newMinX === undefined && newMaxX === undefined) {
      this.handleZoomReset()
      return
    }

    if (w.config.xaxis.convertedCatToNumeric) {
      // in category charts, avoid zooming out beyond min and max
      if (newMinX < 1) {
        newMinX = 1
        newMaxX = w.globals.dataPoints
      }

      if (newMaxX - newMinX < 2) {
        return
      }
    }

    let xaxis = {
      min: newMinX,
      max: newMaxX,
    }

    const beforeZoomRange = this.getBeforeZoomRange(
      xaxis,
      /** @type {any} */ (undefined),
    )
    if (beforeZoomRange) {
      xaxis = beforeZoomRange.xaxis
    }

    /** @type {{ xaxis: any; yaxis?: any }} */
    const options = {
      xaxis,
    }
    if (!w.globals.initialConfig) return
    const yaxis = Utils.clone(w.globals.initialConfig.yaxis)

    if (!w.config.chart.group) {
      // if chart in a group, prevent yaxis update here
      // fix issue #650
      options.yaxis = yaxis
    }

    this.w.interact.zoomed = true

    this.ctx.updateHelpers._updateOptions(
      options,
      false,
      this.w.config.chart.animations.dynamicAnimation.enabled,
    )

    this.zoomCallback(xaxis, yaxis)
  }

  /**
   * @param {Record<string, any>} xaxis
   * @param {Record<string, any>} yaxis
   */
  zoomCallback(xaxis, yaxis) {
    if (typeof this.ev.zoomed === 'function') {
      this.ev.zoomed(this.ctx, { xaxis, yaxis })
      this.ctx.events.fireEvent('zoomed', { xaxis, yaxis })
    }
  }

  /**
   * @param {Record<string, any>} xaxis
   * @param {Record<string, any>} yaxis
   */
  getBeforeZoomRange(xaxis, yaxis) {
    let newRange = null
    if (typeof this.ev.beforeZoom === 'function') {
      newRange = this.ev.beforeZoom(this, { xaxis, yaxis })
    }

    return newRange
  }

  toggleMenu() {
    window.setTimeout(() => {
      if (this.elMenu?.classList.contains('apexcharts-menu-open')) {
        this._closeMenu()
      } else {
        this.elMenu?.classList.add('apexcharts-menu-open')
        this.elMenuIcon?.setAttribute('aria-expanded', 'true')
      }
    }, 0)
  }

  _closeMenu() {
    this.elMenu?.classList.remove('apexcharts-menu-open')
    this.elMenuIcon?.setAttribute('aria-expanded', 'false')
  }

  /**
   * @param {string} type
   */
  handleDownload(type) {
    const w = this.w
    const exprt = new Exports(this.w, this.ctx)
    switch (type) {
      case 'svg':
        exprt.exportToSVG()
        break
      case 'png':
        exprt.exportToPng()
        break
      case 'csv':
        exprt.exportToCSV({
          series: w.config.series,
          columnDelimiter: w.config.chart.toolbar.export.csv.columnDelimiter,
        })
        break
    }
  }

  handleZoomReset() {
    const charts = this.ctx.getSyncedCharts()

    /**
     * @param {Record<string, any>} ch
     */
    charts.forEach((ch) => {
      const w = ch.w

      // if user is hitting zoom reset button without zooming in first, then we should not fire zoom reset event
      if (!w.interact.zoomed) return

      // forget lastXAxis min/max as reset button isn't resetting the x-axis completely if zoomX is called before
      w.globals.lastXAxis.min = w.globals.initialConfig.xaxis.min
      w.globals.lastXAxis.max = w.globals.initialConfig.xaxis.max

      ch.updateHelpers.revertDefaultAxisMinMax()

      if (typeof w.config.chart.events.beforeResetZoom === 'function') {
        // here, user get an option to control xaxis and yaxis when resetZoom is called
        // at this point, whatever is returned from w.config.chart.events.beforeResetZoom
        // is set as the new xaxis/yaxis min/max
        const resetZoomRange = w.config.chart.events.beforeResetZoom(ch, w)

        if (resetZoomRange) {
          ch.updateHelpers.revertDefaultAxisMinMax(resetZoomRange)
        }
      }

      if (typeof w.config.chart.events.zoomed === 'function') {
        ch.ctx.toolbar.zoomCallback({
          min: w.config.xaxis.min,
          max: w.config.xaxis.max,
        })
      }

      // if user has some series collapsed before hitting zoom reset button,
      // those series should stay collapsed
      const series = ch.ctx.series.emptyCollapsedSeries(
        Utils.clone(w.globals.initialSeries),
      )

      ch.updateHelpers._updateSeries(
        series,
        w.config.chart.animations.dynamicAnimation.enabled,
      )

      w.interact.zoomed = false
    })
  }

  destroy() {
    this.elZoom = null
    this.elZoomIn = null
    this.elZoomOut = null
    this.elPan = null
    this.elSelection = null
    this.elZoomReset = null
    this.elMenuIcon = null
  }
}