interface/Slider.js

/*
 * Created by yvesb on 08/10/2016.
 */
/*
 * MathGraph32 Javascript : Software for animating online dynamic mathematics figures
 * https://www.mathgraph32.org/
 * @Author Yves Biton (yves.biton@sesamath.net)
 * @License: GNU AGPLv3 https://www.gnu.org/licenses/agpl-3.0.html
 */
import { cens } from 'src/kernel/dom'
import { getStr, preventDefault } from '../kernel/kernel'
import constantes from '../kernel/constantes'
import SliderDlg from '../dialogs/SliderDlg'

export default Slider

function getMousePosition (app, evt) {
  const { left, top } = app.rightPanel.getBoundingClientRect()
  return {
    x: evt.clientX - left,
    y: evt.clientY - top
  }
}

function getTouchPosition (app, evt) {
  const { x, y } = app.rightPanel.getBoundingClientRect()
  return {
    x: evt.targetTouches[0].clientX - x,
    y: evt.targetTouches[0].clientY - y
  }
}

// pour qu'un Slider puisse être passé à app.setTip il doit avoir une propriété y
/**
 *
 * @param {MtgApp} app
 * @param {number} width
 * @param {number} height
 * @param {number} min
 * @param {number} max
 * @param {number} startval
 * @param {number} digits
 * @param {number} step
 * @param {string} tip
 * @param {string} transform
 * @param {string} append
 * @param {number} y
 * @param {Function} onUpdate
 * @constructor
 * @implements TippedElt
 */
function Slider (app, width, height, min, max, startval, digits, step, tip, transform, append, y, onUpdate) {
  this.target = 'right' // Pour les tips
  const zf = app.zoomFactor
  width *= zf
  height *= zf
  /** @type {MtgApp} */
  this.app = app
  /** @type {number} */
  this.width = width
  /** @type {number} */
  this.height = height
  /** @type {number} */
  this.min = min
  /** @type {number} */
  this.max = max
  /**
   * La valeur courante du slider
   * @type {number}
   */
  this.val = startval
  /** @type {Function} */
  this.onUpdate = onUpdate
  /** @type {number} */
  this.step = step
  /**
   * Le textCode du tip
   * @type {string}
   */
  this.textCode = tip
  /**
   * Le tip (dans la langue cible)
   * @type {string}
   */
  this.tip = getStr(tip)
  /**
   * L'éventuelle chaîne à rajouter au bout de l'affichage
   * @type {string}
   */
  this.append = append
  /** @type {number} */
  this.y = y // Pour la ligne d'affichage du tip
  /** @type {boolean} */
  this.captured = false
  /** @type {number} */
  this.clicks = 0
  const gap = constantes.sliderGap * zf
  const g = cens('g', {
    transform
  })
  /** @type {SVGElement} */
  this.container = g
  // Un rectangle à gauche qui contiendra le curseur et réagira à la souris
  const widthc = width - digits * constantes.sliderDigitsFontSize * 0.6 * zf // Largeur du curseur
  // this.widthc = widthc;
  const rectg = cens('rect', {
    x: 0,
    y: 0,
    width: widthc,
    height,
    stroke: 'none',
    fill: constantes.buttonBackGroundColor
  })
  g.appendChild(rectg)
  const rectd = cens('rect', {
    x: widthc,
    y: 0,
    width: width - widthc,
    height,
    fill: constantes.buttonBackGroundColor
  })
  g.appendChild(rectd)
  const yl = (height - constantes.sliderThickness * zf) / 2
  // On crée le trait horizontal
  this.segmentWidth = widthc - 2 * constantes.sliderRadius * zf - 2 * gap
  this.xc = gap + constantes.sliderRadius * zf // Abscisse de début du segment
  const seg = cens('line', {
    stroke: constantes.sliderColor,
    'stroke-linecap': 'round',
    x1: this.xc,
    y1: yl,
    x2: gap + this.segmentWidth,
    y2: yl,
    'pointer-events': 'none'
  })
  g.appendChild(seg)

  // On crée le rond qui sera le curseur
  /** @type {number} */
  this.xCurseur = this.xc + this.abscisse(startval)
  /** @type {SVGElement} */
  this.circ = cens('ellipse', {
    cx: this.xCurseur,
    cy: yl,
    rx: constantes.sliderRadius * zf,
    ry: (constantes.sliderHeight / 2 - 2) * zf,
    stroke: 'black',
    fill: 'url(#sliderGrad)',
    'pointer-events': 'none'
  })
  g.appendChild(this.circ)

  // Il faut toujours préciser passive explicitement (pour que chrome arrête de râler en console)
  const activeOpts = { capture: false, passive: false }
  const upListener = this.onDeviceUp.bind(this)
  rectg.addEventListener('mouseup', upListener, activeOpts)
  rectg.addEventListener('touchend', upListener, activeOpts)
  rectg.addEventListener('mousedown', this.onDeviceDown.bind(this, getMousePosition), activeOpts)
  rectg.addEventListener('touchstart', this.onDeviceDown.bind(this, getTouchPosition), activeOpts)
  rectg.addEventListener('mousemove', this.onDeviceMove.bind(this, getMousePosition), activeOpts)
  rectg.addEventListener('touchmove', this.onDeviceMove.bind(this, getTouchPosition), activeOpts)

  // On crée à droite un affichage de la valeur
  this.txt = cens('text', {
    x: String(width - 2),
    y: height - 4 * zf,
    'font-size': String(constantes.sliderDigitsFontSize * zf) + 'px',
    style: 'stroke:blue;font-size:' + String(constantes.sliderDigitsFontSize * zf) + 'px;' + 'font-family:serif;text-anchor:end;'
  })
  this.txt.appendChild(document.createTextNode(this.val + this.append))
  g.appendChild(this.txt)
}
Slider.prototype.abscisse = function (val) {
  return (val - this.min) / (this.max - this.min) * this.segmentWidth
}

Slider.prototype.onDeviceUp = function (evt) {
  this.captured = false
  preventDefault(evt)
  // evt.stopPropagation(evt);
}

// fonc est imposé par le bind, ev filé par l'EventListener
Slider.prototype.onDeviceDown = function (fonc, ev) {
  this.clicks++
  if (this.clicks >= 2) {
    this.doubleClickAction()
    this.clicks = 0 // Ajout version 6.1.0
  } else {
    const self = this
    setTimeout(function () {
      switch (self.clicks) {
        case 1 :
          self.singleClickAction(fonc, ev)
          self.clicks = 0 // Ajout version 6.1.0
          break
        case 2 :
          self.doubleClickAction()
          self.clicks = 0 // Ajout version 6.1.0
      }
    }, 300)
  }
}

Slider.prototype.singleClickAction = function (fonc, ev) {
  let newval
  const zf = this.app.zoomFactor
  const point = fonc(this.app, ev) // Renvoie les coordonnées de la souris par rapport au svgFigure
  const x = point.x
  if (Math.abs(x - this.xCurseur) <= constantes.sliderRadius * zf) this.captured = true
  else {
    if (x > this.xCurseur) {
      newval = this.val + this.step
      if (newval > this.max) newval = this.max
      this.update(newval)
    } else {
      if (x < this.xCurseur) {
        newval = this.val - this.step
        if (newval < this.min) newval = this.min
        this.update(newval)
      }
    }
  }
  preventDefault(ev)
}

Slider.prototype.doubleClickAction = function () {
  new SliderDlg(this.app, this)
}
/**
 * Listener du move, actif (il faut préciser passive: false au addEventListener)
 * @param {function} fonc la fonction imposée par le .bind()
 * @param {MouseEvent|TouchEvent} ev l'événement transmis par l'EventListener
 */
Slider.prototype.onDeviceMove = function (fonc, ev) {
  if (this.captured) {
    const point = fonc(this.app, ev) // Renvoie les coordonnées de la souris par rapport au svgFigure
    let x = point.x
    const x1 = this.xc + this.segmentWidth
    if (x < this.xc) x = this.xc
    else if (x > x1) x = x1
    const abs = (x - this.xc) / this.segmentWidth
    const newval = Math.round(this.min + abs * (this.max - this.min))
    this.update(newval)
  } else {
    if (!this.tipDisplayed) {
      this.app.cacheTip() // Sans argument pour effacer l'ancien tip quel qu'il soit
      // Cet appel va modifier this.tipDisplayed
      this.app.setTip(this)
      setTimeout(() => this.app.cacheTip(this), 2500)
    }
  }
  preventDefault(ev)
}
Slider.prototype.updatePosition = function () {
  this.xCurseur = this.xc + this.segmentWidth * (this.val - this.min) / (this.max - this.min)
  this.circ.setAttribute('cx', this.xCurseur)
  this.updateDisplay()
}

Slider.prototype.updateDisplay = function () {
  this.txt.removeChild(this.txt.firstChild)
  this.txt.appendChild(document.createTextNode(this.val + this.append))
}

Slider.prototype.update = function (newVal) {
  if ((typeof newVal === 'number') && Number.isInteger(newVal) && (newVal >= this.min) && (newVal <= this.max)) {
    this.val = newVal
    this.onUpdate(newVal) // Pour appeler la fonction affectant cette valeur
    this.updatePosition()
  }
}