Rename single-word Vue files to multi-word names and update references
Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
481
src/components/DurationInput.vue
Normal file
481
src/components/DurationInput.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="w-32 px-1 border border-neutral-500 cursor-pointer"
|
||||
@click="openTimeSelect = !openTimeSelect"
|
||||
:id="size"
|
||||
>
|
||||
<div v-if="totalSeconds === 0" class="text-center">
|
||||
<p>0</p>
|
||||
</div>
|
||||
<!-- This section shows the fixed time display, not the popup -->
|
||||
<div
|
||||
id="cyp-timerange-show"
|
||||
v-else
|
||||
class="flex justify-center items-center gap-1"
|
||||
>
|
||||
<p v-show="days != 0">{{ days }}d</p>
|
||||
<p v-show="hours != 0">{{ hours }}h</p>
|
||||
<p v-show="minutes != 0">{{ minutes }}m</p>
|
||||
<p v-show="seconds != 0">{{ seconds }}s</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The following section is the popup that appears when the user clicks to open -->
|
||||
<div
|
||||
class="duration-container absolute left-0 top-full translate-y-2 dhms-input-popup-container"
|
||||
v-show="openTimeSelect"
|
||||
v-closable="{ id: size, handler: onClose }"
|
||||
>
|
||||
<div class="duration-box" v-for="(unit, index) in inputTypes" :key="unit">
|
||||
<input
|
||||
id="input_duration_dhms"
|
||||
type="text"
|
||||
class="duration duration-val input-dhms-field"
|
||||
:data-index="index"
|
||||
:data-tunit="unit"
|
||||
:data-max="tUnits[unit].max"
|
||||
:data-min="tUnits[unit].min"
|
||||
:maxlength="tUnits[unit].dsp === 'd' ? 3 : 2"
|
||||
@focus="onFocus"
|
||||
@change="onChange"
|
||||
@keyup="onKeyUp"
|
||||
v-model="inputTimeFields[index]"
|
||||
/>
|
||||
<label class="duration" for="input_duration_dhms">{{
|
||||
tUnits[unit].dsp
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// The Lucia project.
|
||||
// Copyright 2023-2026 DSP, inc. All rights reserved.
|
||||
// Authors:
|
||||
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
|
||||
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
|
||||
// imacat.yang@dsp.im (imacat), 2023/9/23
|
||||
/**
|
||||
* @module components/durationjs
|
||||
* Duration input component with day/hour/minute/second
|
||||
* fields and bounded min/max validation.
|
||||
*/
|
||||
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import emitter from "@/utils/emitter";
|
||||
|
||||
const props = defineProps({
|
||||
max: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
updateMax: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
updateMin: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "md",
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["total-seconds"]);
|
||||
|
||||
const display = ref("dhms");
|
||||
const seconds = ref(0);
|
||||
const minutes = ref(0);
|
||||
const hours = ref(0);
|
||||
const days = ref(0);
|
||||
const maxDays = ref(0);
|
||||
const minDays = ref(0);
|
||||
const totalSeconds = ref(0);
|
||||
const maxTotal = ref(null);
|
||||
const minTotal = ref(null);
|
||||
const inputTypes = ref([]);
|
||||
const lastInput = ref(null);
|
||||
const openTimeSelect = ref(false);
|
||||
|
||||
const tUnits = computed({
|
||||
get() {
|
||||
return {
|
||||
s: { dsp: "s", inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
|
||||
m: { dsp: "m", inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
|
||||
h: { dsp: "h", inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
|
||||
d: {
|
||||
dsp: "d",
|
||||
inc: 1,
|
||||
val: days.value,
|
||||
max: maxDays.value,
|
||||
rate: 86400,
|
||||
min: minDays.value,
|
||||
},
|
||||
};
|
||||
},
|
||||
set(newValues) {
|
||||
for (const unit in newValues) {
|
||||
switch (unit) {
|
||||
case "s":
|
||||
seconds.value = newValues[unit].val;
|
||||
break;
|
||||
case "m":
|
||||
minutes.value = newValues[unit].val;
|
||||
break;
|
||||
case "h":
|
||||
hours.value = newValues[unit].val;
|
||||
break;
|
||||
case "d":
|
||||
days.value = newValues[unit].val;
|
||||
break;
|
||||
}
|
||||
const input = document.querySelector(`[data-tunit="${unit}"]`);
|
||||
if (input) {
|
||||
input.value = newValues[unit].val.toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const inputTimeFields = computed(() => {
|
||||
const paddedTimeFields = [];
|
||||
inputTypes.value.forEach((inputTypeUnit) => {
|
||||
paddedTimeFields.push(
|
||||
tUnits.value[inputTypeUnit].val.toString().padStart(2, "0"),
|
||||
);
|
||||
});
|
||||
return paddedTimeFields;
|
||||
});
|
||||
|
||||
/** Closes the time selection dropdown. */
|
||||
function onClose() {
|
||||
openTimeSelect.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all text in the focused input field.
|
||||
* @param {FocusEvent} event - The focus event.
|
||||
*/
|
||||
function onFocus(event) {
|
||||
lastInput.value = event.target;
|
||||
lastInput.value.select();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and updates the duration value when an input changes.
|
||||
* @param {Event} event - The change event from a duration input.
|
||||
*/
|
||||
function onChange(event) {
|
||||
const baseInputValue = event.target.value;
|
||||
let decoratedInputValue;
|
||||
if (isNaN(event.target.value)) {
|
||||
event.target.value = "00";
|
||||
} else {
|
||||
event.target.value = event.target.value.toString();
|
||||
}
|
||||
decoratedInputValue = event.target.value.toString();
|
||||
|
||||
const inputValue = parseInt(event.target.value, 10);
|
||||
const max = parseInt(event.target.dataset.max, 10);
|
||||
const min = parseInt(event.target.dataset.min, 10);
|
||||
if (inputValue > max) {
|
||||
decoratedInputValue = max.toString().padStart(2, "0");
|
||||
} else if (inputValue < min) {
|
||||
decoratedInputValue = min.toString();
|
||||
}
|
||||
const dsp = event.target.dataset.tunit;
|
||||
tUnits.value[dsp].val = decoratedInputValue;
|
||||
switch (dsp) {
|
||||
case "d":
|
||||
days.value = baseInputValue;
|
||||
break;
|
||||
case "h":
|
||||
hours.value = decoratedInputValue;
|
||||
break;
|
||||
case "m":
|
||||
minutes.value = decoratedInputValue;
|
||||
break;
|
||||
case "s":
|
||||
seconds.value = decoratedInputValue;
|
||||
break;
|
||||
}
|
||||
|
||||
calculateTotalSeconds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles arrow key up/down to increment/decrement duration values.
|
||||
* @param {KeyboardEvent} event - The keyup event.
|
||||
*/
|
||||
function onKeyUp(event) {
|
||||
event.target.value = event.target.value.replace(/\D/g, "");
|
||||
if (event.keyCode === 38 || event.keyCode === 40) {
|
||||
actionUpDown(event.target, event.keyCode === 38, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments or decrements a duration unit value with carry-over.
|
||||
* @param {HTMLInputElement} input - The input element to update.
|
||||
* @param {boolean} goUp - True to increment, false to decrement.
|
||||
* @param {boolean} [selectIt=false] - Whether to select the input text after.
|
||||
*/
|
||||
function actionUpDown(input, goUp, selectIt = false) {
|
||||
const tUnit = input.dataset.tunit;
|
||||
let newVal = getNewValue(input);
|
||||
|
||||
if (goUp) {
|
||||
newVal = handleArrowUp(newVal, tUnit, input);
|
||||
} else {
|
||||
newVal = handleArrowDown(newVal, tUnit);
|
||||
}
|
||||
|
||||
updateInputValue(input, newVal, tUnit);
|
||||
if (selectIt) {
|
||||
input.select();
|
||||
}
|
||||
calculateTotalSeconds();
|
||||
}
|
||||
|
||||
function getNewValue(input) {
|
||||
const newVal = parseInt(input.value, 10);
|
||||
return isNaN(newVal) ? 0 : newVal;
|
||||
}
|
||||
|
||||
function handleArrowUp(newVal, tUnit, input) {
|
||||
newVal += tUnits.value[tUnit].inc;
|
||||
if (newVal > tUnits.value[tUnit].max) {
|
||||
if (tUnits.value[tUnit].dsp === "d") {
|
||||
totalSeconds.value = maxTotal.value;
|
||||
} else {
|
||||
newVal = newVal % (tUnits.value[tUnit].max + 1);
|
||||
incrementPreviousUnit(input);
|
||||
}
|
||||
}
|
||||
return newVal;
|
||||
}
|
||||
|
||||
function handleArrowDown(newVal, tUnit) {
|
||||
newVal -= tUnits.value[tUnit].inc;
|
||||
if (newVal < 0) {
|
||||
newVal = tUnits.value[tUnit].max + 1 - tUnits.value[tUnit].inc;
|
||||
}
|
||||
return newVal;
|
||||
}
|
||||
|
||||
function incrementPreviousUnit(input) {
|
||||
if (input.dataset.index > 0) {
|
||||
const prevUnit = document.querySelector(
|
||||
`input[data-index="${parseInt(input.dataset.index) - 1}"]`,
|
||||
);
|
||||
actionUpDown(prevUnit, true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateInputValue(input, newVal, tUnit) {
|
||||
input.value = newVal.toString();
|
||||
switch (tUnit) {
|
||||
case "d":
|
||||
days.value = input.value;
|
||||
break;
|
||||
case "h":
|
||||
hours.value = input.value;
|
||||
break;
|
||||
case "m":
|
||||
minutes.value = input.value;
|
||||
break;
|
||||
case "s":
|
||||
seconds.value = input.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts total seconds to day/hour/minute/second components.
|
||||
* @param {number} totalSec - The total seconds to convert.
|
||||
* @param {string} [size] - 'max' or 'min' to set boundary days.
|
||||
*/
|
||||
function secondToDate(totalSec, size) {
|
||||
totalSec = parseInt(totalSec);
|
||||
if (!isNaN(totalSec)) {
|
||||
seconds.value = totalSec % 60;
|
||||
minutes.value = (Math.floor(totalSec - seconds.value) / 60) % 60;
|
||||
hours.value = Math.floor(totalSec / 3600) % 24;
|
||||
days.value = Math.floor(totalSec / (3600 * 24));
|
||||
|
||||
if (size === "max") {
|
||||
maxDays.value = Math.floor(totalSec / (3600 * 24));
|
||||
} else if (size === "min") {
|
||||
minDays.value = Math.floor(totalSec / (3600 * 24));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Calculates total seconds from all duration units and emits the result. */
|
||||
function calculateTotalSeconds() {
|
||||
let total = 0;
|
||||
|
||||
for (const unit in tUnits.value) {
|
||||
const val = parseInt(tUnits.value[unit].val, 10);
|
||||
if (!isNaN(val)) {
|
||||
total += val * tUnits.value[unit].rate;
|
||||
}
|
||||
}
|
||||
|
||||
if (total >= maxTotal.value) {
|
||||
total = maxTotal.value;
|
||||
secondToDate(maxTotal.value, "max");
|
||||
} else if (total <= minTotal.value) {
|
||||
total = minTotal.value;
|
||||
secondToDate(minTotal.value, "min");
|
||||
} else if (props.size === "min" && total <= maxTotal.value) {
|
||||
maxDays.value = Math.floor(maxTotal.value / (3600 * 24));
|
||||
}
|
||||
totalSeconds.value = total;
|
||||
emit("total-seconds", total);
|
||||
}
|
||||
|
||||
/** Initializes the duration display based on min/max boundaries and preset value. */
|
||||
async function createData() {
|
||||
const size = props.size;
|
||||
|
||||
if (maxTotal.value !== (await null) && minTotal.value !== (await null)) {
|
||||
switch (size) {
|
||||
case "max":
|
||||
secondToDate(minTotal.value, "min");
|
||||
secondToDate(maxTotal.value, "max");
|
||||
totalSeconds.value = maxTotal.value;
|
||||
if (props.value !== null) {
|
||||
totalSeconds.value = props.value;
|
||||
secondToDate(props.value);
|
||||
}
|
||||
break;
|
||||
case "min":
|
||||
secondToDate(maxTotal.value, "max");
|
||||
secondToDate(minTotal.value, "min");
|
||||
totalSeconds.value = minTotal.value;
|
||||
if (props.value !== null) {
|
||||
totalSeconds.value = props.value;
|
||||
secondToDate(props.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// created
|
||||
emitter.on("reset", () => {
|
||||
createData();
|
||||
});
|
||||
|
||||
// mounted
|
||||
onMounted(() => {
|
||||
inputTypes.value = display.value.split("");
|
||||
});
|
||||
|
||||
const vClosable = {
|
||||
mounted(el, { value }) {
|
||||
const handleOutsideClick = function (e) {
|
||||
let target = e.target;
|
||||
while (target && target.id !== value.id) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
const isClickOutside = target?.id !== value.id && !el.contains(e.target);
|
||||
if (isClickOutside) {
|
||||
value.handler();
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
document.addEventListener("click", handleOutsideClick);
|
||||
el._handleOutsideClick = handleOutsideClick;
|
||||
},
|
||||
unmounted(el) {
|
||||
document.removeEventListener("click", el._handleOutsideClick);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.duration-container {
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--10, #fff);
|
||||
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.25);
|
||||
height: 36px;
|
||||
width: 221px;
|
||||
}
|
||||
.duration-box {
|
||||
float: left;
|
||||
overflow: auto;
|
||||
height: var(--main-input-height);
|
||||
padding: 4px;
|
||||
}
|
||||
.duration-box > .duration {
|
||||
border: 1px solid var(--main-bg-light);
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
background-color: transparent;
|
||||
color: var(--main-bg-light);
|
||||
}
|
||||
.duration-box > .duration:nth-child(1) {
|
||||
border-left: 1px solid var(--main-bg-light);
|
||||
border-top-left-radius: var(--main-input-br);
|
||||
border-bottom-left-radius: var(--main-input-br);
|
||||
}
|
||||
.duration-box > .duration:nth-last-child(1) {
|
||||
border-right: 1px solid var(--main-bg-light);
|
||||
border-top-right-radius: var(--main-input-br);
|
||||
border-bottom-right-radius: var(--main-input-br);
|
||||
}
|
||||
.duration {
|
||||
float: left;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: var(--main-input-height);
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
margin: 0px 2px;
|
||||
}
|
||||
.duration-box > label.duration {
|
||||
line-height: 28px;
|
||||
width: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.duration-box > input[type="text"].duration {
|
||||
text-align: right;
|
||||
width: 26px;
|
||||
padding: 3px 2px 0px 0px;
|
||||
}
|
||||
.duration-box > input[type="button"].duration {
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user