<template> <div id="vue-datepicker"> <div id="dp-modal" @click.self="hideModal" @mousedown.self="canCloseModal = true" v-if="modalShow"> <div id="picker"> <div class="equal-width" id="vuejs-tabs"> <div :class="tabIndex == 0?'active-tab':''" @click="tabIndex = 0;"> {{ pTitle }} </div> <div :class="tabIndex == 1?'active-tab':''" @click="tabIndex = 1;"> {{ gTitle }} </div> <div v-if="timepicker" :class="tabIndex == 2?'active-tab':''" @click="tabIndex = 2;"> {{ tTitle }} </div> </div> <div class="equal-width" v-if="pDate !== null && tabIndex < 2"> <div @click="next" class="vuejsbtn"> <i class="ri-arrow-left-s-line"></i> </div> <div @click="monthPick"> <span v-if="tabIndex == 0"> {{ pMonths[parseInt(peDate[1]) - 1] }} </span> <span v-if="tabIndex == 1"> {{ gMonths[geDate[1]] }} </span> </div> <div @click="yearPick"> <span v-if="tabIndex == 0"> {{ pDate.parseHindi(peDate[0]) }} </span> <span v-if="tabIndex == 1"> {{ geDate[0] }} </span> </div> <div @click="previous" class="vuejsbtn"> <i class="ri-arrow-right-s-line"></i> </div> </div> <div style="overflow: hidden" @mousedown="startSwipe($event)" @touchstart="startSwipe($event, $event.touches[0])" @mousemove="handleSwipe($event)" @touchmove="handleSwipe($event, $event.touches[0])" @mouseup="endSwipe($event)" @touchend="endSwipe($event)" @mouseleave="endSwipe($event)" > <div id="calendar-container" :style="`top:${calContainerTop}px;left:${calContainerLeft}px;`"> <div class="sub-picker" v-if="yPicker" dir="rtl"> <div class="equal-width month-list" v-for="j in 5"> <template v-for="i in 5"> <template v-if="i == 1 && j == 1"> <div @click="startYear -= 23"> <i class="ri-arrow-right-s-line"></i> </div> </template> <template v-else-if="i == 5 && j == 5"> <div @click="startYear += 23"> <i class="ri-arrow-left-s-line"></i> </div> </template> <div v-else class="year" @click="yearPicking( (startYear - 13) + ( i + ((j - 1)*5)) )"> <span v-if="tabIndex == 0"> {{ pDate.parseHindi((startYear - 13) + (i + ((j - 1) * 5))) }} </span> <span v-else> {{ (startYear - 13) + (i + ((j - 1) * 5)) }} </span> </div> </template> </div> </div> <div class="sub-picker" v-if="pmPicker" dir="rtl"> <div class="equal-width month-list" v-for="(ms,j) in chunkArray(pMonths,3)"> <div v-for="(m,i) in ms" class="month" @click="pMonthPicking((i + (j * 3)))"> {{ m }} </div> </div> </div> <div class="sub-picker" v-if="gmPicker"> <div class="equal-width month-list" v-for="(ms,j) in chunkArray(gMonths,3)"> <div v-for="(m,i) in ms" class="month" @click="gMonthPicking((i + (j * 3)))"> {{ m }} </div> </div> </div> <div v-if="tabIndex == 0 || _debug" dir="rtl"> <table> <thead> <tr> <th v-for="day in pWeekDays"> {{ day }} </th> </tr> </thead> <tbody v-if="pDate !== null"> <tr v-for="week in pArray"> <td v-for="d in week" :class="d.class +' '+ isActive(d)" :title="d.date" @click="select(d)"> {{ pDate.parseHindi(d.pDay) }} </td> </tr> </tbody> </table> </div> <div v-if="tabIndex == 1 || _debug"> <table> <thead> <tr> <th v-for="day in gWeekDays"> {{ day }} </th> </tr> </thead> <tbody v-if="pDate !== null"> <tr v-for="week in gArray"> <td v-for="d in week" :class="d.class +' '+ isActive(d) " :title="d.pdate" @click="select(d)"> {{ d.day }} </td> </tr> </tbody> </table> </div> <div v-if="tabIndex == 2"> <div style="padding-top: 10px"> <div id="clock" @mousedown="startDrag" @mousemove="pickMinutes" @mouseup="endDrag" @touchstart="startDrag" @touchmove.prevent="pickMinutes" @touchend="endDrag" > <div id="modes"> <div :class="`vuejs-btn ${mode == 'AM'?'active-selected':''}`" @click="changeMode('AM')" @touchend="changeMode('AM')" > AM </div> <div :class="`vuejs-btn ${mode == 'PM'?'active-selected':''}`" @click="changeMode('PM')" @touchend="changeMode('PM')" > PM </div> </div> <div id="time"> {{ pDate.make2number(cTime[0]) }} : {{ pDate.make2number(cTime[1]) }} </div> <div id="clock-container"> <div class="wrapper"> <div id="circle"></div> <div class="bar-seconds"> <span v-for="i in 60" :key="i" :style="{ '--index': i }"> <i :class="{ 'thick-bar': i % 5 === 0 }"></i> </span> </div> <div class="number-hours"> <span v-for="i in 12" :key="i" :style="{ '--index': i }"> <i> <b @touchend.self="pickHour(i)" @click="pickHour(i)" :class="((cTime[0] % 12) == i?'active-selected':'')"> {{ i }} </b> </i> </span> </div> <div class="hands-box"> <div class="hand minutes" :style="`transform: rotate(${cTime[1] * 6}deg)`"> <i></i></div> <div class="hand hours" :style="`transform: rotate(${cTime[0] * 30 + cTime[1] / 2}deg)`"> <i></i></div> </div> </div> </div> </div> </div> </div> </div> </div> <div id="bottom-bar"> <div> <div class="vuejs-btn" title="Clear" @click="clear"> <i class="ri-eraser-line"></i> </div> </div> <div class="centered"> <span v-if="pDate != null && '1970-0-1' != vgeDate.join('-')"> [{{ pDate.parseHindi(vpeDate.join('/')) }}] [{{ vgeDate.join('-') }}] </span> </div> <div> <div class="vuejs-btn" title="Now" @click="nowSelect"> <i class="ri-time-line"></i> </div> </div> </div> </div> </div> <div id="datepicker"> <input @focus="modalShow = true" :id="xid" :placeholder="xtitle" :class="getClass" type="text" :value="(val == null || val == ''?'':selectedDateTime)"> <input type="hidden" :name="xname" :value="val"> </div> </div> </template> <script> import persianDate from './../components/libs/persian-date.js'; const ONE_DAY = 86400; const ONE_YEAR = ONE_DAY * 365; function chunkArray(arr, count) { const result = []; for (let i = 0; i < arr.length; i += count) { result.push(arr.slice(i, i + count)); } return result; } export default { name: "vue-datetimepicker", components: {}, data: () => { return { _debug: false, // debug all tabs /** * to handling swiping calendar */ startX: 0, startY: 0, swipeDirection: null, swipeDistance: 0, tableRect: null, calContainerTop: 0, calContainerLeft: 0, isSwiping: false, canCloseModal: false, /** * to handling dragging clock */ isDragging: false, fullData: {}, // full data of this date modalShow: false, // modal handle pDate: null, // persian date update startYear: 1970, // start year for year picking tabIndex: 0, // active tab index current: null, // current is not selected value just for show calendar gmPicker: false, // handle gr month picker modal pmPicker: false,// handle persian month picker modal yPicker: false, // handle year picker modal val: null, // selected value pWeekDays: ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'آ'], gWeekDays: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], pMonths: ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'], gMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], } }, emits: ['update:modelValue'], props: { modelValue: { default: NaN, }, xvalue: { default: null, type: Number, }, xmax: { default: null, type: Number, }, xmin: { default: null, type: Number, }, xshow: { default: 'pdate', // show value type: String, }, onSelect: { default: function (date) { }, type: Function, }, pTitle: { default: 'Persian', type: String, }, gTitle: { default: 'Gregorian', type: String, }, tTitle: { default: 'Time', type: String, }, defTab: { default: 0, type: Number, }, xname: { default: "", type: String, }, xtitle: { default: "", type: String, }, xid: { default: "", type: String, }, customClass: { default: "", type: String, }, err: { default: false, type: Boolean, }, timepicker: { default: false, type: Boolean, }, closeOnSelect: { default: false, type: Boolean, }, }, mounted() { this.pDate = new persianDate(); let dt; // check value changed by user or not, then ignore xvalue if (this.val == null) { if (!isNaN(this.modelValue)) { dt = new Date(parseInt(this.modelValue) * 1000); if (this.modelValue == null || this.modelValue == '' || this.modelValue == 'null') { dt = new Date(); this.val = null; this.current = Math.floor(new Date() / 1000); } else { this.current = new Date(parseInt(this.modelValue)); this.val = this.modelValue; } } else { dt = new Date(parseInt(this.xvalue) * 1000); if (this.xvalue == null || this.xvalue == '' || this.xvalue == 'null') { dt = new Date(); this.val = null; this.current = Math.floor(new Date() / 1000); } else { this.current = new Date(parseInt(this.xvalue)); this.val = this.xvalue; } } // tab fix this.tabIndex = parseInt(this.defTab); } else { this.current = this.val; } this.fullData = this.makeDateObject(dt) // if (this.xvalue != this.val){ // // } }, computed: { selectedDateTime() { // fullData[xshow] const dt = new Date(this.val * 1000); return this.makeDateObject(dt)[this.xshow]; }, // get input class getClass: function () { if (this.err == true || (typeof this.err == 'String' && this.err.trim() == '1')) { return 'form-control is-invalid ' + this.customClass; } return 'form-control ' + this.customClass; }, /* * make array of this month days [gregorian] */ gArray: function () { let result = []; const baseDate = this.current * 1000; let d = new Date(baseDate); const currentMonth = d.getMonth(baseDate); // find days of last month end by dayweek for (let i = 0; i >= -7; i--) { d = new Date(baseDate); d.setDate(i); result.push(this.makeDateObject(d, 'previous')); if (d.getDay() == 0) { break; } } result = result.reverse(); // fix sort let nextCount = 0; // find days of this month and start of next end by dayweek for (let i = 1; i <= 45; i++) { d = new Date(baseDate); d.setDate(i); if (d.getMonth() == currentMonth) { result.push(this.makeDateObject(d)); } else { if (d.getDay() == 0 && nextCount > 0) { break; } result.push(this.makeDateObject(d, 'next')); nextCount++; } } return chunkArray(result, 7); }, /* * make array of this month days [persian] */ pArray: function () { let result = []; const baseDate = this.current * 1000; let d = this.pDate.convertDate2Persian(new Date(baseDate)); const currentMonth = d[1]; // get current date to prev until last month end by day week for (let i = 0; i > -40; i--) { let dt = new Date(baseDate + (i * ONE_DAY * 1000)); let pdt = this.pDate.convertDate2Persian(dt); if (pdt[1] == currentMonth) { result.push(this.makeDateObject(dt)); } else { result.push(this.makeDateObject(dt, 'previous')); if (this.makePWeek(dt) == 0) { break; } } } // fix sort result = result.reverse(); // get current date to next until next month end by day week for (let i = 1; i < 40; i++) { let dt = new Date(baseDate + (i * ONE_DAY * 1000)); let pdt = this.pDate.convertDate2Persian(dt); if (pdt[1] == currentMonth) { result.push(this.makeDateObject(dt)); } else { result.push(this.makeDateObject(dt, 'next')); if (this.makePWeek(dt) == 6) { break; } } } return chunkArray(result, 7); }, // gregorian date geDate() { const baseDate = this.current * 1000; let d = new Date(baseDate); return [d.getFullYear(), d.getMonth(), d.getDate()]; }, // persian date peDate() { const baseDate = this.current * 1000; let d = new Date(baseDate); return this.pDate.convertDate2Persian(d); }, // gregorian date by value vgeDate() { const baseDate = this.val * 1000; let d = new Date(baseDate); return [d.getFullYear(), d.getMonth(), d.getDate()]; }, // persian date by value vpeDate() { const baseDate = this.val * 1000; let d = new Date(baseDate); return this.pDate.convertDate2Persian(d); }, // current time cTime() { const baseDate = this.val * 1000; let d = new Date(baseDate); return [d.getHours(), d.getMinutes()]; }, mode() { const date = new Date(this.val * 1000); // Convert Unix timestamp to milliseconds const hours = date.getHours(); if (hours >= 12) { return 'PM'; } else { return 'AM'; } }, }, methods: { // clear input clear() { this.hideModal(); this.val = null; this.fullData[this.xshow] = ''; }, // select now time nowSelect() { this.val = Math.floor(new Date() / 1000); this.select(this.makeDateObject(new Date())); }, // handle select select(obj) { if (this.xmax != null && obj.unix > this.xmax) { return; } if (this.xmin != null && obj.unix < this.xmin) { return; } if (this.isSwiping) { return false; } if (obj.class == 'next') { this.next(); return false; } if (obj.class == 'previous') { this.previous(); return false; } // reset values this.onSelect(obj); this.val = obj.unix; this.fullData = obj; this.current = this.val = obj.unix; if (this.closeOnSelect) { this.canCloseModal = true; this.hideModal(); } return true; }, // select year yearPicking(i) { let dt = this.current * 1000; dt = new Date(dt); // for gregorian if (this.tabIndex == 1) { dt.setFullYear(i); this.current = Math.floor(dt / 1000); } else { // for persian let cYear = parseInt(this.peDate[0]); let diff = ONE_YEAR * (i - cYear); this.current = Math.floor((dt / 1000) + diff); } this.yPicker = false; }, // change gregorian current month gMonthPicking(i) { let dt = this.current * 1000; dt = new Date(dt); dt.setMonth(parseInt(i)); this.current = Math.floor(dt / 1000); this.gmPicker = false; }, // change persian current month pMonthPicking(i) { let dt = this.current * 1000; dt = new Date(dt); let x = 10; if ((i + 1) != parseInt(this.peDate[1])) { if ((i + 1) < parseInt(this.peDate[1])) { x = -10; } // for persian find by loop for dec tolerance do { dt.setDate(dt.getDate() + x); } while ((i + 1) != this.pDate.convertDate2Persian(dt)[1]); this.current = Math.floor(dt / 1000); } this.pmPicker = false; }, // next month next() { let dt = this.current * 1000; dt = new Date(dt); // for gregorian if (this.tabIndex == 1) { dt.setMonth(dt.getMonth() + 1); } else { let currentMonth = this.pDate.convertDate2Persian(new Date(dt))[1]; // for persian find by loop for dec tolerance do { dt.setDate(dt.getDate() + 10); } while (currentMonth == this.pDate.convertDate2Persian(dt)[1]); } this.current = Math.floor(dt / 1000); }, // previous month previous() { let dt = this.current * 1000; dt = new Date(dt); // for gregorian if (this.tabIndex == 1) { dt.setMonth(dt.getMonth() - 1); } else { // persian let currentMonth = this.pDate.convertDate2Persian(new Date(dt))[1]; // for persian find by loop for dec tolerance do { dt.setDate(dt.getDate() - 10); } while (currentMonth == this.pDate.convertDate2Persian(dt)[1]); } this.current = Math.floor(dt / 1000); }, makeDateObject(dt, cls) { dt.setHours(this.cTime[0], this.cTime[1]); return { day: this.pDate.make2number(dt.getDate()), // day pDay: this.pDate.convertDate2Persian(dt)[2], // persian date date: dt.getFullYear() + '-' + dt.getMonth() + '-' + dt.getDate(), // gregorian date datetime: dt.getFullYear() + '-' + dt.getMonth() + '-' + dt.getDate() + ' ' + this.pDate.make2number(this.cTime[0]) + ':' + this.pDate.make2number(this.cTime[1]), // gregorian datetime pdatetime: this.pDate.convertDate2Persian(dt).join('/') + ' ' + this.pDate.make2number(this.cTime[0]) + ':' + this.pDate.make2number(this.cTime[1]), // persian date pdate: this.pDate.convertDate2Persian(dt).join('/'), // persian date hpdatetime: this.pDate.parseHindi(this.pDate.convertDate2Persian(dt).join('/') + ' ' + this.pDate.make2number(this.cTime[0]) + ':' + this.pDate.make2number(this.cTime[1])), // persian date hindi number hpdate: this.pDate.parseHindi(this.pDate.convertDate2Persian(dt).join('/')), // persian date hindi number weekDay: dt.getDay(), // week day class: cls, // class of d unix: Math.floor(dt / 1000) // unix time stamp }; }, // make persian week day makePWeek(dt) { let t = dt.getDay() + 1 % 7; if (t == 7) { return 0 } return t; }, // show month pick modal monthPick() { // gregorian if (this.tabIndex == 1) { this.gmPicker = !this.gmPicker; this.pmPicker = false; } else { // persian this.pmPicker = !this.pmPicker; this.gmPicker = false; } }, // show year pick modal yearPick() { // gregorian if (this.tabIndex == 1) { this.startYear = parseInt(this.geDate[0]); } else { // persian this.startYear = parseInt(this.peDate[0]); } this.yPicker = !this.yPicker; }, // is selected this td isActive(obj) { let dt = new Date(this.val * 1000); let r = ''; if (dt.getFullYear() + '-' + dt.getMonth() + '-' + dt.getDate() == obj.date) { r = 'active-selected'; } if (this.xmax != null && obj.unix > this.xmax) { r += ' disabled-date'; } if (this.xmin != null && obj.unix < this.xmin) { r += ' disabled-date'; } return r; }, // select hour pickHour(i, ignore = false) { let dt = new Date(this.val * 1000); if (ignore) { dt.setHours(i); } else { dt.setHours((this.mode == 'AM' ? i : (i + 12))); } dt.setMinutes(this.cTime[1]); this.val = Math.floor(dt.getTime() / 1000); }, /** * drag handling for clock select */ startDrag(e) { if (e.target.tagName != 'B') { this.isDragging = true; } this.dragHandle(e); }, pickMinutes(e) { if (!this.isDragging) return; this.dragHandle(e); }, endDrag(e) { this.isDragging = false; this.dragHandle(e); }, dragHandle(e) { e.preventDefault(); if (!this.isDragging) { return; } // calc polar system delta const touch = e.touches ? e.touches[0] : null; const eventX = touch ? touch.clientX : e.clientX; const eventY = touch ? touch.clientY : e.clientY; const rect = e.currentTarget.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const deltaX = eventX - centerX; const deltaY = eventY - centerY; const r = Math.sqrt(deltaX ** 2 + deltaY ** 2); let theta = Math.atan2(deltaY, deltaX); theta = ((theta * 180 / Math.PI) + 450) % 360; // console.log('r:', r); // console.log('theta:', theta); if (r > 90 && r < 160) { const minutes = Math.floor((theta / 360) * 60); let dt = new Date(this.val * 1000); dt.setHours(dt.getHours()); dt.setMinutes(minutes); this.val = Math.floor(dt / 1000); } }, /** * swipe calendar next / perv ( month / year) */ startSwipe(e, touch) { this.isSwiping = true; this.canCloseModal = false; this.contentRect = e.currentTarget.getBoundingClientRect(); this.startX = touch ? touch.clientX : e.clientX; this.startY = touch ? touch.clientY : e.clientY; this.swipeDirection = null; this.swipeDistance = 0; this.hasSwipedOnce = false; // Reset the swipe flag }, handleSwipe(e, touch) { e.preventDefault(); if (this.tabIndex == 2) { return false; } if (!this.startX || !this.startY) return; const currentX = touch ? touch.clientX : e.clientX; const currentY = touch ? touch.clientY : e.clientY; const deltaX = currentX - this.startX; const deltaY = currentY - this.startY; if (Math.abs(deltaY) > Math.abs(deltaX)) { this.calContainerTop = deltaY * .5; this.calContainerLeft = 0; } if (Math.abs(deltaY) < Math.abs(deltaX)) { this.calContainerTop = 0; this.calContainerLeft = deltaX * .5; } this.swipeDirection = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX > 0 ? 'right' : 'left' : deltaY > 0 ? 'down' : 'up'; this.swipeDistance = this.swipeDirection === 'right' || this.swipeDirection === 'left' ? Math.abs(deltaX) : Math.abs(deltaY); // Update content padding based on swipe distance and direction (handle 40%) if (this.swipeDistance > this.contentRect.width * 0.4 && !this.hasSwipedOnce) { this.triggerSwipe(this.swipeDirection); this.hasSwipedOnce = true; // Set the swipe flag to true } }, endSwipe() { this.startX = 0; this.startY = 0; this.swipeDirection = null; this.swipeDistance = 0; this.calContainerTop = 0; this.calContainerLeft = 0; this.isSwiping = false; }, triggerSwipe(direction) { // Update content padding based on swipe direction let y = parseInt(this.peDate[0]); if (this.tabIndex == 1) { y = parseInt(this.geDate[1]); } switch (direction) { case 'right': this.previous(); break; case 'left': this.next(); break; case 'up': this.yearPicking(y + 1); break; case 'down': this.yearPicking(y - 1); break; } this.endSwipe(); }, // change mode am/pm changeMode(mode) { // ignore AM while AM if (this.mode == 'AM' && mode == 'AM') { return; } // ignore PM while PM if (this.mode == 'PM' && mode == 'PM') { return; } if (mode == 'AM') { if (this.cTime[0] == 12) { this.pickHour(12); } else { this.pickHour(this.cTime[0] - 12, true); } } else { this.pickHour(this.cTime[0] + 12, true); } }, selfUpdate() { let dt; // check value changed by user or not, then ignore xvalue if (this.val == null) { dt = new Date(parseInt(this.xvalue) * 1000); if (this.xvalue == null || this.xvalue == '' || this.xvalue == 'null') { dt = new Date(); this.val = null; this.current = Math.floor(new Date() / 1000); } else { this.current = new Date(parseInt(this.xvalue)); this.val = this.xvalue; } } else { this.current = this.val; } // this.fullData = this.makeDateObject(dt); }, // hide modal hideModal() { if (this.canCloseModal) { this.modalShow = false; } }, chunkArray: chunkArray, }, watch: { val(newValue) { if (!isNaN(this.modelValue)) { this.$emit('update:modelValue', newValue); } } } } </script> <style scoped> #vue-datepicker { font-size: 12pt; direction: ltr; } #dp-modal { position: fixed; //display: none; left: 0; right: 0; top: 0; bottom: 0; z-index: 999; background: #00000033; backdrop-filter: blur(4px); } #picker { max-width: 400px; min-height: 450px; margin: calc(50vh - 225px) auto; background: #ffffffdd; backdrop-filter: blur(4px); user-select: none; color: black; padding: 5px; font-family: 'Vazir', 'Vazirmatn', sans-serif; } #picker table { border: 1px solid black; width: 100%; margin-top: 5px; } #picker table td, #picker table th { border: 1px solid silver; width: calc(100% / 7); text-align: center; padding: 7px; transition: 500ms; } #picker table td:hover { background: deepskyblue; cursor: pointer; } #picker .next, #picker .previous { color: gray; } .equal-width { display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; text-align: center; cursor: pointer; } .equal-width div { padding: .5rem 5px; font-weight: 800; } .equal-width div i { font-size: 25px; } #bottom-bar { display: grid; grid-template-columns: 1fr 2fr 1fr; text-align: center; } #bottom-bar > div { padding: 7px 4px; } #vuejs-tabs { border: 1px solid gray; margin-bottom: .5rem; } .equal-width div:hover { background: teal; color: white;; } .active-tab { background: deepskyblue; color: white; } .vuejsbtn { padding: 1px !important; } #calendar-container { position: relative; min-height: 285px; } .sub-picker { position: absolute; left: 0; top: 0; bottom: 0; right: 0; background: white; } .month { padding: 1.4rem 0 !important; } .year { padding: .97rem 0 !important; } .vuejs-btn { padding: 3px; background: silver; display: block; cursor: pointer; } .vuejs-btn:hover { background: deepskyblue; color: white; } .vuejs-btn i { font-size: 20px; } .centered { display: flex; align-items: center; justify-content: center; } .centered span { margin-top: 6px; display: inline-block; } .active-selected { background: teal; color: yellow !important; } #clock { padding: 33.5px 65px; } #circle { position: absolute; background: deepskyblue; width: 6px; height: 6px; left: calc(50% - 3px); top: calc(50% - 3px); border-radius: 50%; z-index: 5; } .wrapper { position: relative; width: 255px; height: 255px; border-radius: 50%; display: flex; justify-content: center; align-items: center; } .bar-seconds, .number-hours { position: absolute; width: 100%; height: 100%; border-radius: 50%; } .bar-seconds span { position: absolute; transform: rotate(calc(var(--index) * 6deg)); inset: -20px; text-align: center; pointer-events: none; } .bar-seconds span i { display: inline-block; width: 2px; height: 12px; background: deepskyblue; border-radius: 2px; box-shadow: 0 0 10px deepskyblue; pointer-events: none; font-style: normal; } .bar-seconds span:nth-child(5n) i { /* 5n pour dire tout les mutliples de 5 */ width: 6px; height: 18px; transform: translateY(1px); } .number-hours span { position: absolute; transform: rotate(calc(var(--index) * 30deg)); inset: 6px; text-align: center; pointer-events: none; } .number-hours span i { font-size: 25px; color: deepskyblue; transform: rotate(calc(var(--index) * -30deg)); pointer-events: none; font-style: normal; } .number-hours span i b { text-decoration: none; color: deepskyblue; pointer-events: all; display: inline-block; width: 40px; border-radius: 50%; height: 40px; box-sizing: border-box; padding: 2px; font-weight: 400; } .number-hours span i b:hover { background: teal; color: white; } .hands-box { position: relative; display: flex; justify-content: center; align-items: center; pointer-events: none; } .hands-box .hand { position: absolute; border-radius: 50%; display: flex; justify-content: center; } .hands-box .hand i { display: inline-block; transform-origin: bottom; border-radius: 50%; box-shadow: 0 0 10px deepskyblue; } .hands-box .hours { width: 180px; height: 180px; } .hands-box .hours i { width: 8px; height: 90px; background: deepskyblue; } .hands-box .minutes { width: 275px; height: 275px; } .hands-box .minutes i { width: 6px; height: 140px; background: dodgerblue; border-radius: 2px; } #modes { position: absolute; left: 5px; top: 5px; } #modes .vuejs-btn { padding: 5px; width: 40px; text-align: center; padding-top: 10px; } #time { position: absolute; right: 5px; top: 5px; font-size: 25px; } .disabled-date { background: silver; } </style>