<script setup>
import { computed, ref, useSlots, nextTick } from "vue";
import { useNamespace, useFormItem, useId, useLocale } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
import { useConfig } from "@yh-ui/hooks";
defineOptions({
  name: "YhInputNumber"
});
const props = defineProps({
  modelValue: { type: Number, required: false },
  min: { type: Number, required: false, default: -Infinity },
  max: { type: Number, required: false, default: Infinity },
  step: { type: Number, required: false, default: 1 },
  stepStrictly: { type: Boolean, required: false, default: false },
  precision: { type: Number, required: false },
  size: { type: null, required: false, default: void 0 },
  readonly: { type: Boolean, required: false, default: false },
  disabled: { type: Boolean, required: false, default: false },
  controlsPosition: { type: null, required: false, default: "" },
  controls: { type: Boolean, required: false, default: true },
  name: { type: String, required: false },
  placeholder: { type: String, required: false },
  id: { type: String, required: false },
  tabindex: { type: [String, Number], required: false },
  valueOnClear: { type: [Number, null, String], required: false, default: null },
  prefix: { type: String, required: false },
  suffix: { type: String, required: false },
  prefixIcon: { type: null, required: false },
  suffixIcon: { type: null, required: false },
  clearable: { type: Boolean, required: false, default: false },
  validateEvent: { type: Boolean, required: false, default: true },
  validator: { type: Function, required: false },
  themeOverrides: { type: null, required: false }
});
const emit = defineEmits(["update:modelValue", "change", "input", "focus", "blur", "clear"]);
const slots = useSlots();
const ns = useNamespace("input-number");
const { t } = useLocale();
const { themeStyle } = useComponentTheme(
  "input-number",
  computed(() => props.themeOverrides)
);
const inputRef = ref();
const inputId = useId();
const focused = ref(false);
const hovering = ref(false);
const userInput = ref(null);
const validationError = ref("");
const { form, formItem, validate: triggerValidate } = useFormItem();
const { globalSize } = useConfig();
const mergedDisabled = computed(() => props.disabled || form?.disabled || false);
const mergedSize = computed(
  () => props.size || formItem?.size || form?.size || globalSize.value || "default"
);
const numPrecision = computed(() => {
  if (props.precision !== void 0) return props.precision;
  const stepPrecision = getPrecision(props.step);
  if (props.modelValue !== void 0) {
    return Math.max(getPrecision(props.modelValue), stepPrecision);
  }
  return stepPrecision;
});
const getPrecision = (value) => {
  if (value === void 0) return 0;
  const valueString = value.toString();
  const dotPosition = valueString.indexOf(".");
  return dotPosition !== -1 ? valueString.length - dotPosition - 1 : 0;
};
const toPrecision = (num, pre) => {
  const precision = pre ?? numPrecision.value;
  return Number(Number(num).toFixed(precision));
};
const displayValue = computed(() => {
  if (userInput.value !== null) return userInput.value;
  let currentValue = props.modelValue;
  if (currentValue === void 0 || currentValue === null || Number.isNaN(currentValue)) return "";
  if (props.precision !== void 0) return Number(currentValue).toFixed(props.precision);
  return toPrecision(currentValue).toString();
});
const minDisabled = computed(() => props.modelValue !== void 0 && props.modelValue <= props.min);
const maxDisabled = computed(() => props.modelValue !== void 0 && props.modelValue >= props.max);
const showClear = computed(
  () => props.clearable && !mergedDisabled.value && !props.readonly && props.modelValue !== void 0 && props.modelValue !== null && (focused.value || hovering.value)
);
const hasPrefix = computed(() => !!props.prefix || !!props.prefixIcon || !!slots.prefix);
const hasSuffix = computed(
  () => !!props.suffix || !!props.suffixIcon || !!slots.suffix || showClear.value
);
const inputNumberClasses = computed(() => [
  ns.b(),
  ns.m(mergedSize.value),
  ns.is("disabled", mergedDisabled.value),
  ns.is("focused", focused.value),
  ns.is("controls-right", props.controlsPosition === "right"),
  ns.is("without-controls", !props.controls),
  ns.is("has-prefix", hasPrefix.value),
  ns.is("has-suffix", hasSuffix.value),
  ns.is("error", formItem?.validateStatus === "error")
]);
const validate = (value) => {
  if (props.validator) {
    const result = props.validator(value);
    if (typeof result === "string") {
      validationError.value = result;
      return false;
    }
    if (!result) {
      validationError.value = t("form.validationFailed");
      return false;
    }
  }
  validationError.value = "";
  return true;
};
const setCurrentValue = (newValue, emitChange = true) => {
  const oldValue = props.modelValue;
  if (newValue !== void 0 && newValue !== null) {
    if (Number.isNaN(newValue)) return;
    if (props.stepStrictly) {
      newValue = toPrecision(Math.round(newValue / props.step) * props.step, numPrecision.value);
    }
    if (props.precision !== void 0) newValue = toPrecision(newValue, props.precision);
    if (newValue > props.max) newValue = props.max;
    if (newValue < props.min) newValue = props.min;
  }
  if (oldValue === newValue) return;
  if (props.validateEvent) validate(newValue);
  userInput.value = null;
  emit("update:modelValue", newValue);
  if (props.validateEvent) {
    nextTick(() => {
      triggerValidate("change");
    });
  }
  if (emitChange) emit("change", newValue, oldValue);
};
const increase = () => {
  if (mergedDisabled.value || props.readonly || maxDisabled.value) return;
  setCurrentValue(toPrecision((props.modelValue ?? 0) + props.step));
};
const decrease = () => {
  if (mergedDisabled.value || props.readonly || minDisabled.value) return;
  setCurrentValue(toPrecision((props.modelValue ?? 0) - props.step));
};
const handleClear = () => {
  if (mergedDisabled.value || props.readonly) return;
  const clearVal = props.valueOnClear === "min" ? props.min : props.valueOnClear === "max" ? props.max : props.valueOnClear;
  setCurrentValue(clearVal === null ? void 0 : clearVal);
  emit("clear");
};
const handleInput = (event) => {
  const target = event.target;
  userInput.value = target.value;
  emit("input", target.value === "" ? void 0 : Number(target.value));
};
const handleChange = (event) => {
  const target = event.target;
  const value = target.value;
  userInput.value = null;
  if (value === "") {
    const clearVal = props.valueOnClear === "min" ? props.min : props.valueOnClear === "max" ? props.max : props.valueOnClear;
    setCurrentValue(clearVal === null ? void 0 : clearVal);
    return;
  }
  const newValue = Number(value);
  if (!Number.isNaN(newValue)) setCurrentValue(newValue);
};
const handleFocus = (event) => {
  focused.value = true;
  emit("focus", event);
};
const handleBlur = (event) => {
  focused.value = false;
  userInput.value = null;
  emit("blur", event);
  if (props.validateEvent) {
    nextTick(() => {
      triggerValidate("blur");
    });
  }
};
const handleKeydown = (event) => {
  if (event.key === "ArrowUp") {
    event.preventDefault();
    increase();
  } else if (event.key === "ArrowDown") {
    event.preventDefault();
    decrease();
  }
};
defineExpose({
  focus: () => inputRef.value?.focus(),
  blur: () => inputRef.value?.blur(),
  clear: handleClear
});
</script>

<template>
  <div
    :class="inputNumberClasses"
    :style="themeStyle"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <span
      v-if="controls && controlsPosition !== 'right'"
      :class="[ns.e('decrease'), ns.is('disabled', minDisabled || mergedDisabled)]"
      @click="decrease"
    >
      <slot name="decreaseIcon">
        <svg viewBox="0 0 1024 1024" width="1em" height="1em">
          <path fill="currentColor" d="M128 544h768a32 32 0 0 0 0-64H128a32 32 0 0 0 0 64z" />
        </svg>
      </slot>
    </span>

    <div :class="ns.e('wrapper')">
      <span v-if="hasPrefix" :class="ns.e('prefix')">
        <slot name="prefix">
          <span v-if="prefix" :class="ns.e('prefix-text')">{{ prefix }}</span>
          <component v-if="prefixIcon && typeof prefixIcon !== 'string'" :is="prefixIcon" />
        </slot>
      </span>
      <input
        ref="inputRef"
        type="text"
        :class="ns.e('inner')"
        :value="displayValue"
        :name="name"
        :id="id || inputId"
        :placeholder="placeholder ?? t('inputnumber.placeholder')"
        :disabled="mergedDisabled"
        :readonly="readonly"
        autocomplete="off"
        @input="handleInput"
        @change="handleChange"
        @focus="handleFocus"
        @blur="handleBlur"
        @keydown="handleKeydown"
      />
      <span v-if="hasSuffix" :class="ns.e('suffix')">
        <span v-if="showClear" :class="ns.e('clear')" @click.stop="handleClear">
          <svg viewBox="0 0 1024 1024" width="1em" height="1em">
            <path
              fill="currentColor"
              d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 832a384 384 0 1 0 0-768 384 384 0 0 0 0 768zm-160-448l128 128-128 128 45.248 45.248L525.248 621.248l128 128L698.496 704l-128-128 128-128L653.248 402.752 525.248 530.752l-128-128z"
            />
          </svg>
        </span>
        <slot name="suffix">
          <span v-if="suffix" :class="ns.e('suffix-text')">{{ suffix }}</span>
          <component v-if="suffixIcon && typeof suffixIcon !== 'string'" :is="suffixIcon" />
        </slot>
      </span>
    </div>

    <span
      v-if="controls && controlsPosition !== 'right'"
      :class="[ns.e('increase'), ns.is('disabled', maxDisabled || mergedDisabled)]"
      @click="increase"
    >
      <slot name="increaseIcon">
        <svg viewBox="0 0 1024 1024" width="1em" height="1em">
          <path
            fill="currentColor"
            d="M544 128v352h352a32 32 0 0 1 0 64H544v352a32 32 0 0 1-64 0V544H128a32 32 0 0 1 0-64h352V128a32 32 0 0 1 64 0z"
          />
        </svg>
      </slot>
    </span>

    <span v-if="controls && controlsPosition === 'right'" :class="ns.e('controls')">
      <span
        :class="[ns.e('increase'), ns.is('disabled', maxDisabled || mergedDisabled)]"
        @click="increase"
      >
        <svg viewBox="0 0 1024 1024" width="1em" height="1em">
          <path
            fill="currentColor"
            d="M488.832 344.32l-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"
          />
        </svg>
      </span>
      <span
        :class="[ns.e('decrease'), ns.is('disabled', minDisabled || mergedDisabled)]"
        @click="decrease"
      >
        <svg viewBox="0 0 1024 1024" width="1em" height="1em">
          <path
            fill="currentColor"
            d="M488.832 679.68l-339.84-356.672a32 32 0 0 1 0-44.16l.384-.384a29.44 29.44 0 0 1 42.688 0l320 335.872 319.872-335.872a29.44 29.44 0 0 1 42.688 0l.384.384a32 32 0 0 1 0 44.16L535.168 679.68a32 32 0 0 1-46.336 0z"
          />
        </svg>
      </span>
    </span>
  </div>
</template>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-input-number {
  --yh-input-number-width: 150px;
  --yh-input-number-height: var(--yh-component-size-default);
  --yh-input-number-font-size: var(--yh-font-size-base);
  --yh-input-number-bg-color: var(--yh-fill-color-blank);
  --yh-input-number-border-color: var(--yh-border-color);
  --yh-input-number-text-color: var(--yh-text-color-regular);
  --yh-input-number-placeholder-color: var(--yh-text-color-placeholder);
  --yh-input-number-hover-border-color: var(--yh-input-number-focus-border-color);
  --yh-input-number-focus-border-color: var(--yh-color-primary);
  --yh-input-number-btn-bg-color: transparent;
  --yh-input-number-btn-hover-bg-color: var(--yh-fill-color-light);
  --yh-input-number-btn-color: var(--yh-text-color-secondary);
  --yh-input-number-btn-hover-color: var(--yh-color-primary);
  --yh-input-number-border-radius: var(--yh-radius-base);
  position: relative;
  display: inline-flex;
  align-items: stretch;
  width: var(--yh-input-number-width);
  height: var(--yh-input-number-height);
  background-color: var(--yh-input-number-bg-color);
  border: var(--yh-border-width) var(--yh-border-style) var(--yh-input-number-border-color);
  border-radius: var(--yh-input-number-border-radius);
  transition: var(--yh-transition-fast);
  box-sizing: border-box;
}
.yh-input-number:hover:not(.is-disabled) {
  border-color: var(--yh-input-number-hover-border-color);
}
.yh-input-number.is-focused:not(.is-disabled) {
  border-color: var(--yh-input-number-focus-border-color);
  box-shadow: 0 0 0 2px var(--yh-color-primary-light-8);
}

.yh-input-number__wrapper {
  flex: 1;
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
  height: 100%;
}

.yh-input-number__inner {
  flex: 1;
  width: 100%;
  height: 100%;
  padding: 0 8px;
  font-size: var(--yh-input-number-font-size);
  font-family: inherit;
  color: var(--yh-input-number-text-color);
  text-align: center;
  background: transparent;
  border: none;
  outline: none;
  box-sizing: border-box;
  appearance: none;
  -moz-appearance: textfield;
}
.yh-input-number__inner::-webkit-inner-spin-button, .yh-input-number__inner::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.yh-input-number__inner::placeholder {
  color: var(--yh-input-number-placeholder-color);
}

.yh-input-number__prefix {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  height: 100%;
  color: var(--yh-text-color-secondary);
  font-size: var(--yh-input-number-font-size);
  padding-left: 8px;
}

.yh-input-number__suffix {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  height: 100%;
  color: var(--yh-text-color-secondary);
  font-size: var(--yh-input-number-font-size);
  padding-right: 8px;
}

.yh-input-number__prefix-text {
  font-size: inherit;
  white-space: nowrap;
  color: var(--yh-text-color-secondary);
}

.yh-input-number__suffix-text {
  font-size: inherit;
  white-space: nowrap;
  color: var(--yh-text-color-secondary);
}

.yh-input-number__clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-right: 4px;
  color: var(--yh-text-color-placeholder);
  cursor: pointer;
  transition: var(--yh-transition-fast);
}
.yh-input-number__clear svg {
  width: 14px;
  height: 14px;
}
.yh-input-number__clear:hover {
  color: var(--yh-text-color-secondary);
}

.yh-input-number__decrease {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 100%;
  color: var(--yh-input-number-btn-color);
  background-color: var(--yh-input-number-btn-bg-color);
  cursor: pointer;
  user-select: none;
  transition: var(--yh-transition-fast);
  border-right: var(--yh-border-width) var(--yh-border-style) var(--yh-input-number-border-color);
  border-radius: calc(var(--yh-input-number-border-radius) - 1px) 0 0 calc(var(--yh-input-number-border-radius) - 1px);
}
.yh-input-number__decrease svg {
  width: 14px;
  height: 14px;
  transition: var(--yh-transition-fast);
}
.yh-input-number__decrease:hover:not(.is-disabled) {
  color: var(--yh-input-number-btn-hover-color);
  background-color: var(--yh-input-number-btn-hover-bg-color);
}
.yh-input-number__decrease:hover:not(.is-disabled) svg {
  transform: scale(1.1);
}
.yh-input-number__decrease:active:not(.is-disabled) svg {
  transform: scale(0.95);
}
.yh-input-number__decrease.is-disabled {
  color: var(--yh-text-color-disabled);
  cursor: not-allowed;
  opacity: 0.5;
}

.yh-input-number__increase {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 100%;
  color: var(--yh-input-number-btn-color);
  background-color: var(--yh-input-number-btn-bg-color);
  cursor: pointer;
  user-select: none;
  transition: var(--yh-transition-fast);
  border-left: var(--yh-border-width) var(--yh-border-style) var(--yh-input-number-border-color);
  border-radius: 0 calc(var(--yh-input-number-border-radius) - 1px) calc(var(--yh-input-number-border-radius) - 1px) 0;
}
.yh-input-number__increase svg {
  width: 14px;
  height: 14px;
  transition: var(--yh-transition-fast);
}
.yh-input-number__increase:hover:not(.is-disabled) {
  color: var(--yh-input-number-btn-hover-color);
  background-color: var(--yh-input-number-btn-hover-bg-color);
}
.yh-input-number__increase:hover:not(.is-disabled) svg {
  transform: scale(1.1);
}
.yh-input-number__increase:active:not(.is-disabled) svg {
  transform: scale(0.95);
}
.yh-input-number__increase.is-disabled {
  color: var(--yh-text-color-disabled);
  cursor: not-allowed;
  opacity: 0.5;
}

.yh-input-number.is-controls-right .yh-input-number__wrapper .yh-input-number__inner {
  text-align: left;
  padding-left: 12px;
}

.yh-input-number.is-controls-right .yh-input-number__controls {
  display: flex;
  flex-direction: column;
  height: 100%;
  border-left: var(--yh-border-width) var(--yh-border-style) var(--yh-input-number-border-color);
}

.yh-input-number.is-controls-right .yh-input-number__decrease {
  width: 32px;
  height: 50%;
  border: 0;
  border-radius: 0 0 calc(var(--yh-input-number-border-radius) - 1px) 0;
  flex-shrink: 0;
}
.yh-input-number.is-controls-right .yh-input-number__decrease svg {
  width: 12px;
  height: 12px;
}

.yh-input-number.is-controls-right .yh-input-number__increase {
  width: 32px;
  height: 50%;
  border: 0;
  border-bottom: var(--yh-border-width) var(--yh-border-style) var(--yh-input-number-border-color);
  border-radius: 0 calc(var(--yh-input-number-border-radius) - 1px) 0 0;
  flex-shrink: 0;
}
.yh-input-number.is-controls-right .yh-input-number__increase svg {
  width: 12px;
  height: 12px;
}

.yh-input-number.is-without-controls .yh-input-number__inner {
  border-radius: var(--yh-input-number-border-radius);
}

.yh-input-number.is-has-prefix .yh-input-number__inner {
  text-align: left;
  padding-left: 4px;
}

.yh-input-number.is-has-suffix .yh-input-number__inner {
  padding-right: 4px;
}

.yh-input-number.is-error {
  border-color: var(--yh-color-danger);
}
.yh-input-number.is-error.is-focused {
  box-shadow: 0 0 0 2px var(--yh-color-danger-light-8);
}

.yh-input-number.is-disabled {
  background-color: var(--yh-fill-color-light);
  cursor: not-allowed;
}
.yh-input-number.is-disabled .yh-input-number__inner {
  color: var(--yh-text-color-placeholder);
  cursor: not-allowed;
}

.yh-input-number.is-disabled .yh-input-number__decrease {
  color: var(--yh-text-color-disabled);
  cursor: not-allowed;
}

.yh-input-number.is-disabled .yh-input-number__increase {
  color: var(--yh-text-color-disabled);
  cursor: not-allowed;
}

.yh-input-number--large {
  --yh-input-number-width: 180px;
  --yh-input-number-height: var(--yh-component-size-large);
  --yh-input-number-font-size: var(--yh-font-size-md);
}
.yh-input-number--large .yh-input-number__decrease {
  width: 40px;
}
.yh-input-number--large .yh-input-number__decrease svg {
  width: 16px;
  height: 16px;
}

.yh-input-number--large .yh-input-number__increase {
  width: 40px;
}
.yh-input-number--large .yh-input-number__increase svg {
  width: 16px;
  height: 16px;
}

.yh-input-number--large.is-controls-right .yh-input-number__decrease {
  width: 36px;
}
.yh-input-number--large.is-controls-right .yh-input-number__decrease svg {
  width: 14px;
  height: 14px;
}

.yh-input-number--large.is-controls-right .yh-input-number__increase {
  width: 36px;
}
.yh-input-number--large.is-controls-right .yh-input-number__increase svg {
  width: 14px;
  height: 14px;
}

.yh-input-number--small {
  --yh-input-number-width: 130px;
  --yh-input-number-height: var(--yh-component-size-small);
  --yh-input-number-font-size: var(--yh-font-size-xs);
}
.yh-input-number--small .yh-input-number__decrease {
  width: 24px;
}
.yh-input-number--small .yh-input-number__decrease svg {
  width: 12px;
  height: 12px;
}

.yh-input-number--small .yh-input-number__increase {
  width: 24px;
}
.yh-input-number--small .yh-input-number__increase svg {
  width: 12px;
  height: 12px;
}

.yh-input-number--small.is-controls-right .yh-input-number__decrease {
  width: 24px;
}
.yh-input-number--small.is-controls-right .yh-input-number__decrease svg {
  width: 10px;
  height: 10px;
}

.yh-input-number--small.is-controls-right .yh-input-number__increase {
  width: 24px;
}
.yh-input-number--small.is-controls-right .yh-input-number__increase svg {
  width: 10px;
  height: 10px;
}
</style>
