ScoDoc-Lille/app/templates/assiduites/widgets/timeline.j2

438 lines
13 KiB
Django/Jinja

<div id="timeline">
<div class="inputs">
<input type="text" name="deb" id="deb" class="timepicker">
<input type="text" name="fin" id="fin" class="timepicker">
</div>
<div class="timeline-container">
<div class="period" style="left: 0%; width: 20%">
<div class="period-handle left"></div>
<div class="period-handle right"></div>
<div class="period-time">Time</div>
</div>
</div>
</div>
<script>
const timelineContainer = document.querySelector(".timeline-container");
const periodTimeLine = document.querySelector(".period");
const t_start = {{ t_start }};
const t_mid = {{ t_mid }};
const t_end = {{ t_end }};
const tick_time = 60 / {{ tick_time }};
const tick_delay = 1 / tick_time;
const period_default = 2;
let handleMoving = false;
function createTicks() {
let i = t_start;
while (i <= t_end) {
const hourTick = document.createElement("div");
hourTick.classList.add("tick", "hour");
hourTick.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`;
timelineContainer.appendChild(hourTick);
const tickLabel = document.createElement("div");
tickLabel.classList.add("tick-label");
tickLabel.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`;
tickLabel.textContent = numberToTime(i);
timelineContainer.appendChild(tickLabel);
if (i < t_end) {
let j = Math.floor(i + 1);
while (i < j) {
i += tick_delay;
if (i <= t_end) {
const quarterTick = document.createElement("div");
quarterTick.classList.add("tick", "quarter");
quarterTick.style.left = `${computePercentage(i, t_start)}%`;
timelineContainer.appendChild(quarterTick);
}
}
i = j;
} else {
i++;
}
}
}
function numberToTime(num) {
const integer = Math.floor(num);
const decimal = Math.round((num % 1) * 60);
let dec = `:${decimal}`;
if (decimal < 10) {
dec = `:0${decimal}`;
}
let int = `${integer}`;
if (integer < 10) {
int = `0${integer}`;
}
return int + dec;
}
function snapToQuarter(value) {
return Math.round(value * tick_time) / tick_time;
}
function updatePeriodTimeLabel() {
const values = getPeriodValues();
const deb = numberToTime(values[0])
const fin = numberToTime(values[1])
const text = `${deb} - ${fin}`
periodTimeLine.querySelector('.period-time').textContent = text;
//Mise à jour des inputs
try{
$('#deb').val(deb);
$('#fin').val(fin);
}catch{}
}
function timelineMainEvent(event) {
const startX = (event.clientX || event.changedTouches[0].clientX);
if (event.target.classList.contains("period-handle")) {
const startWidth = parseFloat(periodTimeLine.style.width);
const startLeft = parseFloat(periodTimeLine.style.left);
const isLeftHandle = event.target.classList.contains("left");
handleMoving = true
const onMouseMove = (moveEvent) => {
if (!handleMoving) return;
const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX;
const containerWidth = timelineContainer.clientWidth;
const newWidth =
startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100;
if (isLeftHandle) {
const newLeft = startLeft + (deltaX / containerWidth) * 100;
adjustPeriodPosition(newLeft, newWidth);
} else {
adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth);
}
updatePeriodTimeLabel();
};
const mouseUp = () => {
snapHandlesToQuarters();
timelineContainer.removeEventListener("mousemove", onMouseMove);
handleMoving = false;
func_call();
savePeriodInLocalStorage();
}
timelineContainer.addEventListener("mousemove", onMouseMove);
timelineContainer.addEventListener("touchmove", onMouseMove);
document.addEventListener(
"mouseup",
mouseUp,
{ once: true }
);
document.addEventListener(
"touchend",
mouseUp,
{ once: true }
);
} else if (event.target === periodTimeLine) {
const startLeft = parseFloat(periodTimeLine.style.left);
const onMouseMove = (moveEvent) => {
if (handleMoving) return;
const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX;
const containerWidth = timelineContainer.clientWidth;
const newLeft = startLeft + (deltaX / containerWidth) * 100;
adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width));
updatePeriodTimeLabel();
};
const mouseUp = () => {
snapHandlesToQuarters();
timelineContainer.removeEventListener("mousemove", onMouseMove);
func_call();
savePeriodInLocalStorage();
}
timelineContainer.addEventListener("mousemove", onMouseMove);
timelineContainer.addEventListener("touchmove", onMouseMove);
document.addEventListener(
"mouseup",
mouseUp,
{ once: true }
);
document.addEventListener(
"touchend",
mouseUp,
{ once: true }
);
}
}
let func_call = () => { };
function setupTimeLine(callback) {
func_call = callback;
timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) });
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) });
const updateFromInputs = ()=>{
let deb = $('#deb').val();
let fin = $('#fin').val();
if (deb != '' && fin != '') {
deb = fromTime(deb);
fin = fromTime(fin);
try {
setPeriodValues(deb, fin);
} catch {
setPeriodValues(...getPeriodValues());
}
}
}
$('#deb').data('TimePicker').options.change = updateFromInputs;
$('#fin').data('TimePicker').options.change = updateFromInputs;
updatePeriodTimeLabel();
}
function adjustPeriodPosition(newLeft, newWidth) {
const snappedLeft = snapToQuarter(newLeft);
const snappedWidth = snapToQuarter(newWidth);
const minLeft = 0;
const maxLeft = 100 - snappedWidth;
const clampedLeft = Math.min(Math.max(snappedLeft, minLeft), maxLeft);
periodTimeLine.style.left = `${clampedLeft}%`;
periodTimeLine.style.width = `${snappedWidth}%`;
}
function getPeriodValues() {
const leftPercentage = parseFloat(periodTimeLine.style.left);
const widthPercentage = parseFloat(periodTimeLine.style.width);
const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start;
const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start;
const startValue = snapToQuarter(startHour);
const endValue = snapToQuarter(endHour);
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)];
if (computedValues[0] > t_end || computedValues[1] < t_start) {
return [t_start, Math.min(t_end, t_start + period_default)];
}
if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
computedValues[1] += tick_delay;
}
return computedValues;
}
function setPeriodValues(deb, fin) {
if (fin < deb) {
throw new RangeError(`le paramètre 'deb' doit être inférieur au paramètre 'fin' ([${deb};${fin}])`)
}
if (deb < 0 || fin < 0) {
throw new RangeError(`Les paramètres doivent être des entiers positifis ([${deb};${fin}])`)
}
deb = snapToQuarter(deb);
fin = snapToQuarter(fin);
let leftPercentage = (deb - t_start) / (t_end - t_start) * 100;
let widthPercentage = (fin - deb) / (t_end - t_start) * 100;
periodTimeLine.style.left = `${leftPercentage}%`;
periodTimeLine.style.width = `${widthPercentage}%`;
snapHandlesToQuarters();
updatePeriodTimeLabel()
func_call();
savePeriodInLocalStorage();
}
function snapHandlesToQuarters() {
const periodValues = getPeriodValues();
let lef = Math.min(computePercentage(Math.abs(periodValues[0]), t_start), computePercentage(Math.abs(t_end), tick_delay));
if (lef < 0) {
lef = 0;
}
const left = `${lef}%`;
let wid = Math.max(computePercentage(Math.abs(periodValues[1]), Math.abs(periodValues[0])), computePercentage(tick_delay, 0));
if (wid > 100) {
wid = 100;
}
const width = `${wid}%`
periodTimeLine.style.left = left;
periodTimeLine.style.width = width;
updatePeriodTimeLabel()
}
function computePercentage(a, b) {
return ((a - b) / (t_end - t_start)) * 100;
}
function fromTime(time, separator = ":") {
const [hours, minutes] = time.split(separator).map((el) => Number(el))
return hours + minutes / 60
}
function getPeriodAsDate(){
let [deb, fin] = getPeriodValues();
deb = numberToTime(deb);
fin = numberToTime(fin);
const dateStr = $("#date")
.datepicker("getDate")
.format("yyyy-mm-dd")
.substring(0, 10);
return {
deb: new Date(`${dateStr}T${deb}`),
fin: new Date(`${dateStr}T${fin}`)
}
}
function savePeriodInLocalStorage(){
const dates = getPeriodValues();
localStorage.setItem("sco-timeline-values", JSON.stringify(dates));
}
function loadPeriodFromLocalStorage(){
const dates = JSON.parse(localStorage.getItem("sco-timeline-values"));
if(dates){
setPeriodValues(...dates);
}else{
setPeriodValues(t_start, t_start + period_default);
}
}
createTicks();
loadPeriodFromLocalStorage();
{% if heures %}
let [heure_deb, heure_fin] = [{{ heures | safe }}]
if (heure_deb != '' && heure_fin != '') {
heure_deb = fromTime(heure_deb);
heure_fin = fromTime(heure_fin);
setPeriodValues(heure_deb, heure_fin)
}
{% endif %}
</script>
<style>
#timeline {
display: flex;
justify-content: start;
}
.inputs {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 5px;
margin-bottom: 10px;
width: 5em;
}
.timeline-container {
width: 75%;
margin-left: 25px;
background-color: white;
border-radius: 15px;
position: relative;
height: 40px;
margin-bottom: 25px;
}
/* ... */
.tick {
position: absolute;
bottom: 0;
width: 1px;
background-color: rgba(0, 0, 0, 0.5);
}
.tick.hour {
height: 100%;
}
.tick.quarter {
height: 50%;
}
.tick-label {
position: absolute;
bottom: 0;
font-size: 12px;
text-align: center;
transform: translateY(100%) translateX(-50%);
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.period {
position: absolute;
height: 100%;
background-color: var(--color-secondary);
border-radius: 15px;
}
.period-handle {
position: absolute;
top: 0;
bottom: 0;
width: 8px;
border-radius: 0 4px 4px 0;
cursor: col-resize;
}
.period-handle.right {
right: 0;
border-radius: 4px 0 0 4px;
}
.period .period-time {
display: none;
position: absolute;
left: calc(50% - var(--w)/2 - 5px);
justify-content: center;
align-content: center;
top: calc(-60% - 10px);
--w: 10em;
width: var(--w);
}
.period:hover .period-time {
display: flex;
background-color: var(--color-secondary);
border-radius: 15px;
padding: 5px;
}
</style>