{"mappings":"AACA,SAA2B,cAAc;AACzC,cAAc,sBAAiC;AAK/C,SAAS,kBAAkB;KAMtB;;;;;;;;;;;AAYL,cA0FM,MAAM,UAAU,aAAa,oBAAoB,WAAW,YAAY;;;;CAI5E,AACA;;;;CAKA,AACA;;;;CAKA,AACA;;;;CAKA,AACA;;;;;;CAOA,AACA,OAAO;;;;CAKP,AACA,SAAS;CAET,UACU,OAAO;CAEjB,UACU,UAAU,WAAW;CAE/B,AACA;CAEA,UAAU,SAAS;CACnB,QAAQ;CAER,IAAI,SAAS,mBAAmB,QAAQ;;;;CAOxC,IAAI;;;;CAOJ,SAAS,aAAa;CAStB,yBAAyB,cAAc,qBAAqB;CAO5D,IAAI,kBAAkB;CAItB,UAAU,UAAU,eAAe;CA2BnC,UAAU,cAAc,gBAAgB,eAAe;CAkBvD,QAAQ;;;;;CAMR,YAAY;;;;CAiBZ;;;;;;CAUA,UAAU,mBAAmB,iBACnB,GAAG;;;;;;CAqBb,UAAU,gBAAgB,iBAChB,GAAG;;;;;;CAWb,UAAU,eAAe,iBACf;;;;;CAgBV,UAAU,cAAc,EAAE,SAAS,WAAW;;;;;;CAU9C,UAAU,qBAAqB,GAAG;;;;;;CAqBlC,UAAU,wBAAwB,oBAAoB,yBAC5C,GAAG;;;;;;CAkBb,UAAU,wBAAwB,WAAW,6BACnC,GAAG;CASb,UAAU;CAcV;CAIA,QAAQ;CAIR,YAAY;;AAQd,eAAe;AACf,SAAS","names":[],"sources":["../../../../src/web-components/range/component.ts"],"sourcesContent":["import { attr, tokenList, godown, loop, styles } from \"@godown/element\";\nimport { isNullable, omit, Ranger } from \"sharekit\";\nimport { type TemplateResult, css, html } from \"lit\";\nimport { property, query, queryAll, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\n\nimport { cssGlobalVars, scopePrefix } from \"../../internal/global-style.js\";\nimport { SuperInput } from \"../../internal/super-input.js\";\nimport { ringTypeAttribute } from \"../../internal/ring.js\";\n\nconst protoName = \"range\";\nconst cssScope = scopePrefix(protoName);\n\ntype RangeValue = number | number[];\n\n/**\n * {@linkcode Range} is similar to `<input type=\"range\">`.\n *\n * Value accepts number, or array.\n *\n * Number has 1 handle, the array has the number of its elements.\n *\n * @fires range - Fires when the value changes.\n * @category input\n */\n@godown(protoName)\n@styles(\n  css`\n    :host {\n      ${cssScope}--track-width: .5em;\n      ${cssScope}--handle-scale: 1;\n      ${cssScope}--track-background: var(${cssGlobalVars.active});\n      background: var(${cssGlobalVars.passive});\n      width: 100%;\n      display: block;\n      height: var(${cssScope}--track-width);\n    }\n\n    :host([contents]) [part=\"root\"] {\n      width: inherit;\n    }\n\n    :host([vertical]) {\n      height: 100%;\n      width: fit-content;\n    }\n\n    [part=\"root\"] {\n      min-height: inherit;\n      position: relative;\n      border-radius: inherit;\n      --from: 0%;\n      --to: 50%;\n    }\n\n    [part=\"track\"] {\n      height: 100%;\n      min-height: inherit;\n      display: flex;\n      position: absolute;\n      pointer-events: none;\n      border-radius: inherit;\n      justify-content: space-between;\n      left: min(var(--from), var(--to));\n      background: var(${cssScope}--track-background);\n      width: max(calc(var(--to) - var(--from)), calc(var(--from) - var(--to)));\n    }\n\n    [part~=\"handle\"] {\n      width: 1em;\n      height: 1em;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      user-select: none;\n      position: absolute;\n      border-radius: 50%;\n      transform-origin: 0% 25%;\n      outline: 0;\n      border-style: solid;\n      border-width: 0.1em;\n      transform: scale(var(${cssScope}--handle-scale)) translate(-50%, -25%);\n      background: var(${cssGlobalVars.background});\n      border-color: currentColor;\n    }\n  `,\n  css`\n    [vertical] {\n      height: inherit;\n      width: var(${cssScope}--track-width);\n    }\n\n    [vertical] i {\n      transform: translate(-25%, -50%);\n    }\n\n    [vertical] [part=\"track\"] {\n      width: 100%;\n      height: max(calc(var(--to) - var(--from)), calc(var(--from) - var(--to)));\n      top: min(var(--from), var(--to));\n      left: 0;\n    }\n  `,\n  css`\n    [part~=\"handle\"] {\n      left: var(--handle);\n      top: 0;\n    }\n\n    [vertical] [part~=\"handle\"] {\n      top: var(--handle);\n      left: 0;\n    }\n  `,\n)\nclass Range<V extends RangeValue = RangeValue> extends SuperInput<RangeValue> {\n  /**\n   * Minimum value.\n   */\n  @property({ type: Number })\n  min = 0;\n\n  /**\n   * Maximum value.\n   */\n  @property({ type: Number })\n  max = 100;\n\n  /**\n   * Sliding step length.\n   */\n  @property({ type: Number })\n  step: number;\n\n  /**\n   * Whether to display the range vertically.\n   */\n  @property({ type: Boolean, reflect: true })\n  vertical = false;\n\n  /**\n   * Value, or each of values, will render a handle.\n   *\n   * Accepts number or array of numbers.\n   */\n  @property({ type: Array })\n  value: V;\n\n  /**\n   * The default of `{@linkcode this.value}`.\n   */\n  @property({ type: Array })\n  default: V;\n\n  @query(\"[part=root]\")\n  protected _root: HTMLElement;\n\n  @queryAll(\"[part=handle]\")\n  protected _handles: NodeListOf<HTMLElement>;\n\n  @state()\n  lastFocus?: number;\n\n  protected _ranger: Ranger;\n  private __focusStack: number[] = [];\n\n  get range(): V extends number ? false : true {\n    return Array.isArray(this.value) as any;\n  }\n\n  /**\n   * Return values in the form of an array.\n   */\n  get rangeValue(): number[] {\n    return (this.range ? this.value : [this.value]) as number[];\n  }\n\n  /**\n   * Pad the value to the specified length.\n   */\n  padValue(len: number, value = 0): number[] {\n    const { rangeValue } = this;\n    const miss = len - rangeValue.length;\n    if (miss > 0) {\n      return new Array(miss).fill(value).concat(rangeValue);\n    }\n    return rangeValue;\n  }\n\n  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {\n    super.attributeChangedCallback(name, _old, value);\n    if (name === \"max\" || name === \"min\" || name === \"step\") {\n      this._ranger = new Ranger(this.min, this.max, this.step);\n    }\n  }\n\n  get observedRecord(): Record<string, any> {\n    return omit(super.observedRecord, ringTypeAttribute);\n  }\n\n  protected render(): TemplateResult<1> {\n    const rangeValue = this.padValue(2);\n    const from = Math.min(...rangeValue);\n    const to = Math.max(...rangeValue);\n    const gap = this._ranger.diff;\n\n    return html`\n      <div\n        part=\"root\"\n        ${attr(this.observedRecord)}\n        @mousedown=\"${this.disabled ? null : this._handleMousedownRoot}\"\n        style=\"${styleMap(\n          Object.fromEntries([\n            [\"--from\", `${((from - this.min) / gap) * 100}%`],\n            [\"--to\", `${((to - this.min) / gap) * 100}%`],\n            ...rangeValue.map(\n              (value, index) => [`--handle-${index}`, `${((value - this.min) / gap) * 100}%`] as [string, string],\n            ),\n          ]),\n        )}\"\n      >\n        <div part=\"track\"></div>\n        ${loop(this.rangeValue.length, (index) => this._renderHandle(index))}\n      </div>\n    `;\n  }\n\n  protected _renderHandle(index: number): TemplateResult<1> {\n    const { disabled, range, rangeValue } = this;\n\n    // in single-handle mod (value is a number or an array with length 1),\n    const end = !range || (range && index === rangeValue.length - 1 && rangeValue.length === 1);\n    return html`\n      <i\n        tabindex=\"0\"\n        part=\"${tokenList(\"handle\", `handle-${index}`)}\"\n        @mousedown=\"${disabled ? null : this.createMouseDown(index)}\"\n        style=\"${styleMap({\n          \"z-index\": this.__focusStack.indexOf(index) + 1,\n          \"--handle\": `var(--${end ? \"to\" : `handle-${index}`})`,\n        })}\"\n      ></i>\n    `;\n  }\n\n  private __keydownEvent: EventListenerOrEventListenerObject;\n\n  /**\n   * Focuses the handle at the given index, updates the focus stack.\n   * @param index - The index of the handle to focus.\n   */\n  focusHandle(index: number): void {\n    this.lastFocus = index;\n    const indexOfFocusStack = this.__focusStack.indexOf(index);\n    if (indexOfFocusStack !== -1) {\n      this.__focusStack.splice(indexOfFocusStack, 1);\n    }\n    this.__focusStack.push(index);\n    const handleItem = this._handles.item(index);\n    handleItem?.focus();\n    if (!this.__keydownEvent) {\n      this.__keydownEvent = this.events.add(document, \"keydown\", this.createKeydownEvent(index));\n    }\n  }\n\n  /**\n   * Removes the focus from the currently focused handle.\n   */\n  blurHandle(): void {\n    this.lastFocus = undefined;\n    this.__keydownEvent = this.events.remove(document, \"keydown\", this.__keydownEvent);\n  }\n\n  /**\n   * Creates a keydown event handler that updates the value of the range based on arrow key presses.\n   * @param index - The index of the handle to update.\n   * @returns A function that handles the keydown event and updates the range value.\n   */\n  protected createKeydownEvent(index: number) {\n    return (e: KeyboardEvent): void => {\n      const { rangeValue, step } = this;\n      if (rangeValue.length < 2) {\n        index = 0;\n      }\n      const old = rangeValue[index];\n      if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n        e.preventDefault();\n        this.createSetValue(index)(old - step);\n      } else if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n        e.preventDefault();\n        this.createSetValue(index)(old + step);\n      }\n    };\n  }\n\n  /**\n   * Creates a mouse down event handler that focuses the handle at the given index and sets the value of the range.\n   * @param index - The index of the handle to focus.\n   * @returns A function that handles the mouse down event and updates the range value.\n   */\n  protected createMouseDown(index: number) {\n    return (e: MouseEvent): void => {\n      this.focusHandle(index);\n      this.createMousedownListener(this.createSetValue(index))(e);\n    };\n  }\n\n  /**\n   * Creates a function that sets the value of the range at the given index.\n   * @param index - The index of the value to set.\n   * @returns A function that sets the value of the range.\n   */\n  protected createSetValue(index: number) {\n    return (value: number): void => {\n      const normalizeValue = this._ranger.normalize(value);\n      let newValue: RangeValue = normalizeValue;\n      if (this.range) {\n        newValue = [...(this.value as number[])];\n        newValue[index] = normalizeValue;\n      }\n      this.value = newValue as V;\n      this.dispatchCustomEvent(\"change\", this.value);\n    };\n  }\n\n  /**\n   * Compute value from event.\n   * @returns The value closest to the event client position.\n   */\n  protected _computeValue({ clientX, clientY }: MouseEvent): number {\n    const { top, left, height, width } = this._root.getBoundingClientRect();\n    return this._ranger.present(this.vertical ? (clientY - top) / height : (clientX - left) / width);\n  }\n\n  /**\n   * Handles the mouse down event on the root element of the range component.\n   * Computes the closest value to the mouse position, sets the value, and focuses the corresponding handle.\n   * @param e - The mouse down event object.\n   */\n  protected _handleMousedownRoot(e: MouseEvent): void {\n    const value = this._computeValue(e);\n    const index = this.range\n      ? this.rangeValue.reduce((acc, item, index) => {\n          const diff = Math.abs(value - item);\n          const prevDiff = Math.abs(value - this.rangeValue[acc]);\n          return diff < prevDiff ? index : acc;\n        }, 0)\n      : 0;\n\n    const set = this.createSetValue(index);\n    set(value);\n    this.createMousedownListener(set)(e);\n    this.focusHandle(index);\n  }\n\n  /**\n   * Creates a mouse down event handler that focuses the handle at the given index and sets the value of the range.\n   * @param index - The index of the handle to focus.\n   * @returns A function that handles the mouse down event and updates the range value.\n   */\n  protected createMousedownListener(mouseMoveCallback: (arg0: number) => void) {\n    return (e: MouseEvent): void => {\n      e.preventDefault();\n      e.stopPropagation();\n      const move = this.createMousemoveListener(mouseMoveCallback);\n      this.events.add(document, \"mousemove\", move);\n      const stop = () => {\n        this.events.remove(document, \"mousemove\", move);\n        this.events.remove(document, \"mouseup\", stop);\n      };\n      this.events.add(document, \"mouseup\", stop);\n    };\n  }\n\n  /**\n   * Creates a mouse move event handler that updates the range value based on the mouse position.\n   * @param callback - A function to call with the new value when the mouse is moved.\n   * @returns A function that handles the mouse move event and updates the range value.\n   */\n  protected createMousemoveListener(callback: (newValue: number) => void) {\n    return (e: MouseEvent): void => {\n      const value = this._computeValue(e);\n      if (value !== this._ranger.restrict(value)) {\n        return;\n      }\n      callback?.call(this, value);\n    };\n  }\n\n  protected _connectedInit(): void {\n    this._ranger = new Ranger(this.min, this.max, this.step);\n    const gap = this._ranger.diff;\n    this.step ||= gap / 100;\n    if (isNullable(this.value)) {\n      if (!isNullable(this.default)) {\n        this.value = this.default;\n      } else {\n        (this.value as number) = Math.round(gap / 2 / this.step) * this.step;\n      }\n    }\n    this.default ??= this.value;\n  }\n\n  reset(): void {\n    this.value = this.default;\n  }\n\n  sort(): V {\n    return (this.value = this.toSorted());\n  }\n\n  toSorted(): V {\n    if (this.range) {\n      return [...(this.value as number[])].sort((a, b) => a - b) as V;\n    }\n    return this.value;\n  }\n}\n\nexport default Range;\nexport { Range };\n"],"version":3,"file":"component.d.ts"}