Codex commited on
Commit ·
4d1cba1
1
Parent(s): b132f4e
Fix planner resize anchoring and alignment
Browse files- static/planner.js +151 -37
- static/v020.css +85 -6
static/planner.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
activePage: 0,
|
| 8 |
dragTaskId: null,
|
| 9 |
interaction: null,
|
|
|
|
| 10 |
};
|
| 11 |
|
| 12 |
const PIXELS_PER_MINUTE = 1.16;
|
|
@@ -15,9 +16,12 @@
|
|
| 15 |
const CANVAS_GAP = 18;
|
| 16 |
const SNAP_MINUTES = 5;
|
| 17 |
const MIN_DURATION = 15;
|
|
|
|
| 18 |
const AXIS_LABEL_MIN_GAP = 28;
|
|
|
|
| 19 |
|
| 20 |
const pageTrack = document.getElementById("pageTrack");
|
|
|
|
| 21 |
const plannerDateInput = document.getElementById("plannerDateInput");
|
| 22 |
const plannerPrevDay = document.getElementById("plannerPrevDay");
|
| 23 |
const plannerNextDay = document.getElementById("plannerNextDay");
|
|
@@ -185,12 +189,55 @@
|
|
| 185 |
if (task && task.schedule) {
|
| 186 |
return Math.max(MIN_DURATION, toMinutes(task.schedule.end_time) - toMinutes(task.schedule.start_time));
|
| 187 |
}
|
| 188 |
-
return Number((state.planner.settings && state.planner.settings.default_task_duration_minutes) || 45);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
function setActivePage(index) {
|
| 192 |
state.activePage = clamp(index, 0, 1);
|
| 193 |
pageTrack.style.transform = `translateX(-${state.activePage * 100}%)`;
|
|
|
|
|
|
|
|
|
|
| 194 |
document.querySelectorAll(".story-tab").forEach((tab) => {
|
| 195 |
tab.classList.toggle("is-active", Number(tab.dataset.goPage) === state.activePage);
|
| 196 |
});
|
|
@@ -361,8 +408,8 @@
|
|
| 361 |
|
| 362 |
const band = document.createElement("div");
|
| 363 |
band.className = "timeline-slot-band";
|
| 364 |
-
band.style.top = `${(slotStart - dayStart)
|
| 365 |
-
band.style.height = `${
|
| 366 |
band.innerHTML = `
|
| 367 |
<strong>${escapeHtml(slot.label)}</strong>
|
| 368 |
<span>${escapeHtml(slot.start)} - ${escapeHtml(slot.end)}</span>
|
|
@@ -378,16 +425,16 @@
|
|
| 378 |
Array.from(lineMarkers).sort((left, right) => left - right).forEach((minute) => {
|
| 379 |
const line = document.createElement("div");
|
| 380 |
line.className = `timeline-line ${minute % 60 === 0 ? "is-hour" : "is-slot"}`;
|
| 381 |
-
line.style.top = `${(minute - dayStart)
|
| 382 |
canvasLayer.appendChild(line);
|
| 383 |
});
|
| 384 |
|
| 385 |
const axisLabelMinutes = [];
|
| 386 |
const appendAxisLabel = (minute) => {
|
| 387 |
-
const top = (minute - dayStart)
|
| 388 |
const previous = axisLabelMinutes[axisLabelMinutes.length - 1];
|
| 389 |
if (previous !== undefined) {
|
| 390 |
-
const previousTop = (previous - dayStart)
|
| 391 |
if (top - previousTop < AXIS_LABEL_MIN_GAP) {
|
| 392 |
return;
|
| 393 |
}
|
|
@@ -406,7 +453,7 @@
|
|
| 406 |
axisLabelMinutes.forEach((minute) => {
|
| 407 |
const tick = document.createElement("div");
|
| 408 |
tick.className = "timeline-axis-tick";
|
| 409 |
-
tick.style.top = `${(minute - dayStart)
|
| 410 |
tick.textContent = minutesToTime(minute);
|
| 411 |
axisLayer.appendChild(tick);
|
| 412 |
});
|
|
@@ -423,8 +470,8 @@
|
|
| 423 |
const endMinutes = toMinutes(block.end);
|
| 424 |
const overlay = document.createElement("div");
|
| 425 |
overlay.className = "timeline-major-block";
|
| 426 |
-
overlay.style.top = `${(startMinutes - dayStart)
|
| 427 |
-
overlay.style.height = `${
|
| 428 |
overlay.innerHTML = `<span>${escapeHtml(block.label)}</span>`;
|
| 429 |
canvasLayer.appendChild(overlay);
|
| 430 |
});
|
|
@@ -434,10 +481,9 @@
|
|
| 434 |
const leftPercent = widthPercent * (item.column || 0);
|
| 435 |
const block = document.createElement("article");
|
| 436 |
block.className = `planner-event ${item.kind === "course" ? "course-event" : "task-event"} ${item.completed ? "is-complete" : ""}`;
|
| 437 |
-
block.style.top = `${(item.startMinutes - dayStart) * PIXELS_PER_MINUTE}px`;
|
| 438 |
-
block.style.height = `${Math.max((item.endMinutes - item.startMinutes) * PIXELS_PER_MINUTE, 54)}px`;
|
| 439 |
block.style.left = `${leftPercent}%`;
|
| 440 |
block.style.width = `calc(${widthPercent}% - 8px)`;
|
|
|
|
| 441 |
|
| 442 |
if (item.kind === "course") {
|
| 443 |
if (item.color) {
|
|
@@ -476,27 +522,37 @@
|
|
| 476 |
return;
|
| 477 |
}
|
| 478 |
|
|
|
|
|
|
|
|
|
|
| 479 |
const mode = event.target.closest("[data-resize-task-start]")
|
| 480 |
? "resize-start"
|
| 481 |
: event.target.closest("[data-resize-task-end]")
|
| 482 |
? "resize-end"
|
| 483 |
: "move";
|
| 484 |
|
| 485 |
-
|
| 486 |
-
if (!rect) {
|
| 487 |
-
return;
|
| 488 |
-
}
|
| 489 |
-
|
| 490 |
state.interaction = {
|
| 491 |
mode,
|
| 492 |
block,
|
| 493 |
taskId: item.task_id,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
startMinutes: item.startMinutes,
|
| 495 |
endMinutes: item.endMinutes,
|
| 496 |
duration: item.endMinutes - item.startMinutes,
|
| 497 |
-
offsetY: event.clientY - (rect.top + ((item.startMinutes - dayStart) * PIXELS_PER_MINUTE)),
|
| 498 |
};
|
| 499 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
block.classList.add("is-dragging");
|
| 501 |
});
|
| 502 |
}
|
|
@@ -517,8 +573,7 @@
|
|
| 517 |
const duration = getTaskDuration(task);
|
| 518 |
const startMinutes = clamp(clientYToMinutes(event.clientY), dayStart, dayEnd - duration);
|
| 519 |
preview.style.display = "block";
|
| 520 |
-
preview
|
| 521 |
-
preview.style.height = `${Math.max(duration * PIXELS_PER_MINUTE, 54)}px`;
|
| 522 |
preview.textContent = `${minutesToTime(startMinutes)} - ${minutesToTime(startMinutes + duration)}`;
|
| 523 |
});
|
| 524 |
|
|
@@ -601,6 +656,13 @@
|
|
| 601 |
}
|
| 602 |
}
|
| 603 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
document.addEventListener("click", (event) => {
|
| 605 |
const pageButton = event.target.closest("[data-go-page]");
|
| 606 |
if (pageButton) {
|
|
@@ -634,6 +696,7 @@
|
|
| 634 |
state.dragTaskId = card.dataset.plannerTaskId;
|
| 635 |
event.dataTransfer.setData("text/plain", state.dragTaskId);
|
| 636 |
event.dataTransfer.effectAllowed = "move";
|
|
|
|
| 637 |
card.classList.add("is-dragging");
|
| 638 |
});
|
| 639 |
|
|
@@ -643,6 +706,7 @@
|
|
| 643 |
card.classList.remove("is-dragging");
|
| 644 |
}
|
| 645 |
state.dragTaskId = null;
|
|
|
|
| 646 |
const preview = document.getElementById("timelineDropPreview");
|
| 647 |
if (preview) {
|
| 648 |
preview.style.display = "none";
|
|
@@ -650,51 +714,71 @@
|
|
| 650 |
});
|
| 651 |
|
| 652 |
document.addEventListener("pointermove", (event) => {
|
| 653 |
-
if (!state.interaction) {
|
| 654 |
return;
|
| 655 |
}
|
| 656 |
|
|
|
|
|
|
|
| 657 |
const { dayStart, dayEnd } = getPlannerConfig();
|
|
|
|
|
|
|
| 658 |
if (state.interaction.mode === "move") {
|
|
|
|
| 659 |
const startMinutes = clamp(
|
| 660 |
-
|
| 661 |
dayStart,
|
| 662 |
-
dayEnd -
|
| 663 |
);
|
| 664 |
state.interaction.startMinutes = startMinutes;
|
| 665 |
-
state.interaction.endMinutes = startMinutes +
|
|
|
|
| 666 |
} else if (state.interaction.mode === "resize-start") {
|
| 667 |
const startMinutes = clamp(
|
| 668 |
-
|
| 669 |
dayStart,
|
| 670 |
-
state.interaction.
|
| 671 |
);
|
| 672 |
state.interaction.startMinutes = startMinutes;
|
|
|
|
| 673 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
| 674 |
} else {
|
| 675 |
const endMinutes = clamp(
|
| 676 |
-
|
| 677 |
-
state.interaction.
|
| 678 |
dayEnd
|
| 679 |
);
|
|
|
|
| 680 |
state.interaction.endMinutes = endMinutes;
|
| 681 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
| 682 |
}
|
| 683 |
|
| 684 |
-
|
| 685 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 686 |
updateEventTimeLabel(state.interaction.block, state.interaction.startMinutes, state.interaction.endMinutes);
|
| 687 |
});
|
| 688 |
|
| 689 |
-
|
| 690 |
-
if (
|
| 691 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
}
|
|
|
|
| 693 |
|
| 694 |
-
|
| 695 |
-
current.block.classList.remove("is-dragging");
|
| 696 |
-
state.interaction = null;
|
| 697 |
-
|
| 698 |
requestJSON(`/api/tasks/${current.taskId}/schedule`, {
|
| 699 |
method: "PATCH",
|
| 700 |
body: JSON.stringify({
|
|
@@ -706,7 +790,36 @@
|
|
| 706 |
.then(() => loadPlanner(state.selectedDate, true))
|
| 707 |
.then(() => showToast("规划时间已更新"))
|
| 708 |
.catch((error) => showToast(error.message, "error"));
|
| 709 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
|
| 711 |
plannerDateInput.addEventListener("change", () => {
|
| 712 |
if (plannerDateInput.value) {
|
|
@@ -738,4 +851,5 @@
|
|
| 738 |
});
|
| 739 |
|
| 740 |
renderPlanner();
|
|
|
|
| 741 |
})();
|
|
|
|
| 7 |
activePage: 0,
|
| 8 |
dragTaskId: null,
|
| 9 |
interaction: null,
|
| 10 |
+
suppressClickUntil: 0,
|
| 11 |
};
|
| 12 |
|
| 13 |
const PIXELS_PER_MINUTE = 1.16;
|
|
|
|
| 16 |
const CANVAS_GAP = 18;
|
| 17 |
const SNAP_MINUTES = 5;
|
| 18 |
const MIN_DURATION = 15;
|
| 19 |
+
const MIN_BLOCK_HEIGHT = MIN_DURATION * PIXELS_PER_MINUTE;
|
| 20 |
const AXIS_LABEL_MIN_GAP = 28;
|
| 21 |
+
const CLICK_SUPPRESS_MS = 260;
|
| 22 |
|
| 23 |
const pageTrack = document.getElementById("pageTrack");
|
| 24 |
+
const pageSlides = Array.from(document.querySelectorAll(".page-slide"));
|
| 25 |
const plannerDateInput = document.getElementById("plannerDateInput");
|
| 26 |
const plannerPrevDay = document.getElementById("plannerPrevDay");
|
| 27 |
const plannerNextDay = document.getElementById("plannerNextDay");
|
|
|
|
| 189 |
if (task && task.schedule) {
|
| 190 |
return Math.max(MIN_DURATION, toMinutes(task.schedule.end_time) - toMinutes(task.schedule.start_time));
|
| 191 |
}
|
| 192 |
+
return Math.max(MIN_DURATION, Number((state.planner.settings && state.planner.settings.default_task_duration_minutes) || 45));
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
function minutesToPixels(minutes) {
|
| 196 |
+
return minutes * PIXELS_PER_MINUTE;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function getBlockHeight(startMinutes, endMinutes) {
|
| 200 |
+
return Math.max(minutesToPixels(endMinutes - startMinutes), MIN_BLOCK_HEIGHT);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
function setBlockBounds(block, startMinutes, endMinutes, dayStart) {
|
| 204 |
+
const height = getBlockHeight(startMinutes, endMinutes);
|
| 205 |
+
block.style.top = `${minutesToPixels(startMinutes - dayStart)}px`;
|
| 206 |
+
block.style.height = `${height}px`;
|
| 207 |
+
return height;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
function updateEventLayout(block, startMinutes, endMinutes, dayStart) {
|
| 211 |
+
const height = setBlockBounds(block, startMinutes, endMinutes, dayStart);
|
| 212 |
+
block.classList.toggle("is-compact", height < 64);
|
| 213 |
+
block.classList.toggle("is-tight", height < 32);
|
| 214 |
+
return height;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
function getPointerDeltaMinutes(pointerStartY, clientY) {
|
| 218 |
+
return snapMinutes((clientY - pointerStartY) / PIXELS_PER_MINUTE);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
function suppressRecentClicks(duration = CLICK_SUPPRESS_MS) {
|
| 222 |
+
state.suppressClickUntil = Date.now() + duration;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
function beginPlannerInteraction() {
|
| 226 |
+
document.body.classList.add("planner-interacting");
|
| 227 |
+
suppressRecentClicks();
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function finishPlannerInteraction() {
|
| 231 |
+
document.body.classList.remove("planner-interacting");
|
| 232 |
+
suppressRecentClicks();
|
| 233 |
}
|
| 234 |
|
| 235 |
function setActivePage(index) {
|
| 236 |
state.activePage = clamp(index, 0, 1);
|
| 237 |
pageTrack.style.transform = `translateX(-${state.activePage * 100}%)`;
|
| 238 |
+
pageSlides.forEach((slide, slideIndex) => {
|
| 239 |
+
slide.classList.toggle("is-active", slideIndex === state.activePage);
|
| 240 |
+
});
|
| 241 |
document.querySelectorAll(".story-tab").forEach((tab) => {
|
| 242 |
tab.classList.toggle("is-active", Number(tab.dataset.goPage) === state.activePage);
|
| 243 |
});
|
|
|
|
| 408 |
|
| 409 |
const band = document.createElement("div");
|
| 410 |
band.className = "timeline-slot-band";
|
| 411 |
+
band.style.top = `${minutesToPixels(slotStart - dayStart)}px`;
|
| 412 |
+
band.style.height = `${minutesToPixels(slotEnd - slotStart)}px`;
|
| 413 |
band.innerHTML = `
|
| 414 |
<strong>${escapeHtml(slot.label)}</strong>
|
| 415 |
<span>${escapeHtml(slot.start)} - ${escapeHtml(slot.end)}</span>
|
|
|
|
| 425 |
Array.from(lineMarkers).sort((left, right) => left - right).forEach((minute) => {
|
| 426 |
const line = document.createElement("div");
|
| 427 |
line.className = `timeline-line ${minute % 60 === 0 ? "is-hour" : "is-slot"}`;
|
| 428 |
+
line.style.top = `${minutesToPixels(minute - dayStart)}px`;
|
| 429 |
canvasLayer.appendChild(line);
|
| 430 |
});
|
| 431 |
|
| 432 |
const axisLabelMinutes = [];
|
| 433 |
const appendAxisLabel = (minute) => {
|
| 434 |
+
const top = minutesToPixels(minute - dayStart);
|
| 435 |
const previous = axisLabelMinutes[axisLabelMinutes.length - 1];
|
| 436 |
if (previous !== undefined) {
|
| 437 |
+
const previousTop = minutesToPixels(previous - dayStart);
|
| 438 |
if (top - previousTop < AXIS_LABEL_MIN_GAP) {
|
| 439 |
return;
|
| 440 |
}
|
|
|
|
| 453 |
axisLabelMinutes.forEach((minute) => {
|
| 454 |
const tick = document.createElement("div");
|
| 455 |
tick.className = "timeline-axis-tick";
|
| 456 |
+
tick.style.top = `${minutesToPixels(minute - dayStart)}px`;
|
| 457 |
tick.textContent = minutesToTime(minute);
|
| 458 |
axisLayer.appendChild(tick);
|
| 459 |
});
|
|
|
|
| 470 |
const endMinutes = toMinutes(block.end);
|
| 471 |
const overlay = document.createElement("div");
|
| 472 |
overlay.className = "timeline-major-block";
|
| 473 |
+
overlay.style.top = `${minutesToPixels(startMinutes - dayStart)}px`;
|
| 474 |
+
overlay.style.height = `${minutesToPixels(endMinutes - startMinutes)}px`;
|
| 475 |
overlay.innerHTML = `<span>${escapeHtml(block.label)}</span>`;
|
| 476 |
canvasLayer.appendChild(overlay);
|
| 477 |
});
|
|
|
|
| 481 |
const leftPercent = widthPercent * (item.column || 0);
|
| 482 |
const block = document.createElement("article");
|
| 483 |
block.className = `planner-event ${item.kind === "course" ? "course-event" : "task-event"} ${item.completed ? "is-complete" : ""}`;
|
|
|
|
|
|
|
| 484 |
block.style.left = `${leftPercent}%`;
|
| 485 |
block.style.width = `calc(${widthPercent}% - 8px)`;
|
| 486 |
+
updateEventLayout(block, item.startMinutes, item.endMinutes, dayStart);
|
| 487 |
|
| 488 |
if (item.kind === "course") {
|
| 489 |
if (item.color) {
|
|
|
|
| 522 |
return;
|
| 523 |
}
|
| 524 |
|
| 525 |
+
event.preventDefault();
|
| 526 |
+
event.stopPropagation();
|
| 527 |
+
|
| 528 |
const mode = event.target.closest("[data-resize-task-start]")
|
| 529 |
? "resize-start"
|
| 530 |
: event.target.closest("[data-resize-task-end]")
|
| 531 |
? "resize-end"
|
| 532 |
: "move";
|
| 533 |
|
| 534 |
+
beginPlannerInteraction();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
state.interaction = {
|
| 536 |
mode,
|
| 537 |
block,
|
| 538 |
taskId: item.task_id,
|
| 539 |
+
pointerId: event.pointerId,
|
| 540 |
+
pointerStartY: event.clientY,
|
| 541 |
+
initialStartMinutes: item.startMinutes,
|
| 542 |
+
initialEndMinutes: item.endMinutes,
|
| 543 |
startMinutes: item.startMinutes,
|
| 544 |
endMinutes: item.endMinutes,
|
| 545 |
duration: item.endMinutes - item.startMinutes,
|
|
|
|
| 546 |
};
|
| 547 |
|
| 548 |
+
if (typeof block.setPointerCapture === "function") {
|
| 549 |
+
try {
|
| 550 |
+
block.setPointerCapture(event.pointerId);
|
| 551 |
+
} catch (error) {
|
| 552 |
+
// Ignore browsers that reject capture for synthetic pointer sequences.
|
| 553 |
+
}
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
block.classList.add("is-dragging");
|
| 557 |
});
|
| 558 |
}
|
|
|
|
| 573 |
const duration = getTaskDuration(task);
|
| 574 |
const startMinutes = clamp(clientYToMinutes(event.clientY), dayStart, dayEnd - duration);
|
| 575 |
preview.style.display = "block";
|
| 576 |
+
setBlockBounds(preview, startMinutes, startMinutes + duration, dayStart);
|
|
|
|
| 577 |
preview.textContent = `${minutesToTime(startMinutes)} - ${minutesToTime(startMinutes + duration)}`;
|
| 578 |
});
|
| 579 |
|
|
|
|
| 656 |
}
|
| 657 |
}
|
| 658 |
|
| 659 |
+
document.addEventListener("click", (event) => {
|
| 660 |
+
if (Date.now() < state.suppressClickUntil) {
|
| 661 |
+
event.preventDefault();
|
| 662 |
+
event.stopPropagation();
|
| 663 |
+
}
|
| 664 |
+
}, true);
|
| 665 |
+
|
| 666 |
document.addEventListener("click", (event) => {
|
| 667 |
const pageButton = event.target.closest("[data-go-page]");
|
| 668 |
if (pageButton) {
|
|
|
|
| 696 |
state.dragTaskId = card.dataset.plannerTaskId;
|
| 697 |
event.dataTransfer.setData("text/plain", state.dragTaskId);
|
| 698 |
event.dataTransfer.effectAllowed = "move";
|
| 699 |
+
beginPlannerInteraction();
|
| 700 |
card.classList.add("is-dragging");
|
| 701 |
});
|
| 702 |
|
|
|
|
| 706 |
card.classList.remove("is-dragging");
|
| 707 |
}
|
| 708 |
state.dragTaskId = null;
|
| 709 |
+
finishPlannerInteraction();
|
| 710 |
const preview = document.getElementById("timelineDropPreview");
|
| 711 |
if (preview) {
|
| 712 |
preview.style.display = "none";
|
|
|
|
| 714 |
});
|
| 715 |
|
| 716 |
document.addEventListener("pointermove", (event) => {
|
| 717 |
+
if (!state.interaction || event.pointerId !== state.interaction.pointerId) {
|
| 718 |
return;
|
| 719 |
}
|
| 720 |
|
| 721 |
+
event.preventDefault();
|
| 722 |
+
|
| 723 |
const { dayStart, dayEnd } = getPlannerConfig();
|
| 724 |
+
const deltaMinutes = getPointerDeltaMinutes(state.interaction.pointerStartY, event.clientY);
|
| 725 |
+
|
| 726 |
if (state.interaction.mode === "move") {
|
| 727 |
+
const duration = state.interaction.initialEndMinutes - state.interaction.initialStartMinutes;
|
| 728 |
const startMinutes = clamp(
|
| 729 |
+
state.interaction.initialStartMinutes + deltaMinutes,
|
| 730 |
dayStart,
|
| 731 |
+
dayEnd - duration
|
| 732 |
);
|
| 733 |
state.interaction.startMinutes = startMinutes;
|
| 734 |
+
state.interaction.endMinutes = startMinutes + duration;
|
| 735 |
+
state.interaction.duration = duration;
|
| 736 |
} else if (state.interaction.mode === "resize-start") {
|
| 737 |
const startMinutes = clamp(
|
| 738 |
+
state.interaction.initialStartMinutes + deltaMinutes,
|
| 739 |
dayStart,
|
| 740 |
+
state.interaction.initialEndMinutes - MIN_DURATION
|
| 741 |
);
|
| 742 |
state.interaction.startMinutes = startMinutes;
|
| 743 |
+
state.interaction.endMinutes = state.interaction.initialEndMinutes;
|
| 744 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
| 745 |
} else {
|
| 746 |
const endMinutes = clamp(
|
| 747 |
+
state.interaction.initialEndMinutes + deltaMinutes,
|
| 748 |
+
state.interaction.initialStartMinutes + MIN_DURATION,
|
| 749 |
dayEnd
|
| 750 |
);
|
| 751 |
+
state.interaction.startMinutes = state.interaction.initialStartMinutes;
|
| 752 |
state.interaction.endMinutes = endMinutes;
|
| 753 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
| 754 |
}
|
| 755 |
|
| 756 |
+
updateEventLayout(
|
| 757 |
+
state.interaction.block,
|
| 758 |
+
state.interaction.startMinutes,
|
| 759 |
+
state.interaction.endMinutes,
|
| 760 |
+
dayStart
|
| 761 |
+
);
|
| 762 |
updateEventTimeLabel(state.interaction.block, state.interaction.startMinutes, state.interaction.endMinutes);
|
| 763 |
});
|
| 764 |
|
| 765 |
+
function releaseInteractionPointer(current) {
|
| 766 |
+
if (
|
| 767 |
+
current
|
| 768 |
+
&& typeof current.block.releasePointerCapture === "function"
|
| 769 |
+
&& typeof current.block.hasPointerCapture === "function"
|
| 770 |
+
&& current.pointerId !== undefined
|
| 771 |
+
&& current.block.hasPointerCapture(current.pointerId)
|
| 772 |
+
) {
|
| 773 |
+
try {
|
| 774 |
+
current.block.releasePointerCapture(current.pointerId);
|
| 775 |
+
} catch (error) {
|
| 776 |
+
// Ignore browsers that already released pointer capture.
|
| 777 |
+
}
|
| 778 |
}
|
| 779 |
+
}
|
| 780 |
|
| 781 |
+
function persistInteraction(current) {
|
|
|
|
|
|
|
|
|
|
| 782 |
requestJSON(`/api/tasks/${current.taskId}/schedule`, {
|
| 783 |
method: "PATCH",
|
| 784 |
body: JSON.stringify({
|
|
|
|
| 790 |
.then(() => loadPlanner(state.selectedDate, true))
|
| 791 |
.then(() => showToast("规划时间已更新"))
|
| 792 |
.catch((error) => showToast(error.message, "error"));
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
function finishInteraction(event) {
|
| 796 |
+
if (!state.interaction) {
|
| 797 |
+
return;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
if (event && event.pointerId !== undefined && event.pointerId !== state.interaction.pointerId) {
|
| 801 |
+
return;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
const current = state.interaction;
|
| 805 |
+
current.block.classList.remove("is-dragging");
|
| 806 |
+
state.interaction = null;
|
| 807 |
+
releaseInteractionPointer(current);
|
| 808 |
+
finishPlannerInteraction();
|
| 809 |
+
|
| 810 |
+
if (
|
| 811 |
+
current.startMinutes === current.initialStartMinutes
|
| 812 |
+
&& current.endMinutes === current.initialEndMinutes
|
| 813 |
+
) {
|
| 814 |
+
return;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
persistInteraction(current);
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
document.addEventListener("pointerup", finishInteraction);
|
| 821 |
+
document.addEventListener("pointercancel", finishInteraction);
|
| 822 |
+
window.addEventListener("blur", finishInteraction);
|
| 823 |
|
| 824 |
plannerDateInput.addEventListener("change", () => {
|
| 825 |
if (plannerDateInput.value) {
|
|
|
|
| 851 |
});
|
| 852 |
|
| 853 |
renderPlanner();
|
| 854 |
+
setActivePage(0);
|
| 855 |
})();
|
static/v020.css
CHANGED
|
@@ -60,6 +60,17 @@
|
|
| 60 |
max-width: 100%;
|
| 61 |
min-width: 100%;
|
| 62 |
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
.page-home {
|
|
@@ -225,6 +236,7 @@
|
|
| 225 |
position: absolute;
|
| 226 |
left: 10px;
|
| 227 |
right: 10px;
|
|
|
|
| 228 |
padding: 12px;
|
| 229 |
border-radius: 18px;
|
| 230 |
background: rgba(255, 255, 255, 0.04);
|
|
@@ -261,6 +273,7 @@
|
|
| 261 |
position: absolute;
|
| 262 |
left: 0;
|
| 263 |
right: 0;
|
|
|
|
| 264 |
border-radius: 22px;
|
| 265 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.015) 100%);
|
| 266 |
pointer-events: none;
|
|
@@ -278,6 +291,8 @@
|
|
| 278 |
.planner-event,
|
| 279 |
.timeline-drop-preview {
|
| 280 |
position: absolute;
|
|
|
|
|
|
|
| 281 |
border-radius: 20px;
|
| 282 |
padding: 14px;
|
| 283 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
@@ -285,6 +300,10 @@
|
|
| 285 |
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
|
| 286 |
}
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
.planner-event::before,
|
| 289 |
.timeline-drop-preview::before {
|
| 290 |
content: "";
|
|
@@ -304,6 +323,7 @@
|
|
| 304 |
|
| 305 |
.planner-event-top strong {
|
| 306 |
max-width: 72%;
|
|
|
|
| 307 |
}
|
| 308 |
|
| 309 |
.planner-event-meta {
|
|
@@ -338,20 +358,21 @@
|
|
| 338 |
|
| 339 |
.planner-event-resize {
|
| 340 |
position: absolute;
|
| 341 |
-
left:
|
| 342 |
-
right:
|
| 343 |
-
height:
|
| 344 |
border-radius: 999px;
|
| 345 |
-
background: rgba(
|
| 346 |
cursor: ns-resize;
|
|
|
|
| 347 |
}
|
| 348 |
|
| 349 |
.planner-event-resize-top {
|
| 350 |
-
top:
|
| 351 |
}
|
| 352 |
|
| 353 |
.planner-event-resize-bottom {
|
| 354 |
-
bottom:
|
| 355 |
}
|
| 356 |
|
| 357 |
.planner-event.is-dragging,
|
|
@@ -359,6 +380,64 @@
|
|
| 359 |
opacity: 0.8;
|
| 360 |
}
|
| 361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
.planner-sidebar {
|
| 363 |
padding: 18px;
|
| 364 |
display: grid;
|
|
|
|
| 60 |
max-width: 100%;
|
| 61 |
min-width: 100%;
|
| 62 |
overflow: hidden;
|
| 63 |
+
pointer-events: none;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.page-slide.is-active {
|
| 67 |
+
pointer-events: auto;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
body.planner-interacting,
|
| 71 |
+
body.planner-interacting * {
|
| 72 |
+
user-select: none !important;
|
| 73 |
+
-webkit-user-select: none !important;
|
| 74 |
}
|
| 75 |
|
| 76 |
.page-home {
|
|
|
|
| 236 |
position: absolute;
|
| 237 |
left: 10px;
|
| 238 |
right: 10px;
|
| 239 |
+
box-sizing: border-box;
|
| 240 |
padding: 12px;
|
| 241 |
border-radius: 18px;
|
| 242 |
background: rgba(255, 255, 255, 0.04);
|
|
|
|
| 273 |
position: absolute;
|
| 274 |
left: 0;
|
| 275 |
right: 0;
|
| 276 |
+
box-sizing: border-box;
|
| 277 |
border-radius: 22px;
|
| 278 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.015) 100%);
|
| 279 |
pointer-events: none;
|
|
|
|
| 291 |
.planner-event,
|
| 292 |
.timeline-drop-preview {
|
| 293 |
position: absolute;
|
| 294 |
+
box-sizing: border-box;
|
| 295 |
+
overflow: hidden;
|
| 296 |
border-radius: 20px;
|
| 297 |
padding: 14px;
|
| 298 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
| 300 |
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
|
| 301 |
}
|
| 302 |
|
| 303 |
+
.planner-event {
|
| 304 |
+
touch-action: none;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
.planner-event::before,
|
| 308 |
.timeline-drop-preview::before {
|
| 309 |
content: "";
|
|
|
|
| 323 |
|
| 324 |
.planner-event-top strong {
|
| 325 |
max-width: 72%;
|
| 326 |
+
line-height: 1.2;
|
| 327 |
}
|
| 328 |
|
| 329 |
.planner-event-meta {
|
|
|
|
| 358 |
|
| 359 |
.planner-event-resize {
|
| 360 |
position: absolute;
|
| 361 |
+
left: 10px;
|
| 362 |
+
right: 10px;
|
| 363 |
+
height: 6px;
|
| 364 |
border-radius: 999px;
|
| 365 |
+
background: linear-gradient(90deg, rgba(123, 231, 234, 0.9), rgba(97, 210, 159, 0.72));
|
| 366 |
cursor: ns-resize;
|
| 367 |
+
z-index: 2;
|
| 368 |
}
|
| 369 |
|
| 370 |
.planner-event-resize-top {
|
| 371 |
+
top: 0;
|
| 372 |
}
|
| 373 |
|
| 374 |
.planner-event-resize-bottom {
|
| 375 |
+
bottom: 0;
|
| 376 |
}
|
| 377 |
|
| 378 |
.planner-event.is-dragging,
|
|
|
|
| 380 |
opacity: 0.8;
|
| 381 |
}
|
| 382 |
|
| 383 |
+
.planner-event.is-compact {
|
| 384 |
+
border-radius: 16px;
|
| 385 |
+
padding: 8px 12px 10px;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.planner-event.is-compact::before {
|
| 389 |
+
border-radius: 16px 0 0 16px;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.task-event.is-compact .planner-event-top {
|
| 393 |
+
display: grid;
|
| 394 |
+
gap: 4px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.task-event.is-compact .planner-event-meta,
|
| 398 |
+
.task-event.is-compact .planner-event-clear {
|
| 399 |
+
display: none;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.course-event.is-compact .planner-lock-badge {
|
| 403 |
+
display: none;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.course-event.is-compact .planner-event-meta {
|
| 407 |
+
margin-top: 4px;
|
| 408 |
+
font-size: 0.76rem;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.course-event.is-compact .planner-event-meta span:last-child {
|
| 412 |
+
display: none;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.planner-event.is-tight {
|
| 416 |
+
padding: 6px 10px;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.planner-event.is-tight .planner-event-meta,
|
| 420 |
+
.planner-event.is-tight .planner-event-clear,
|
| 421 |
+
.planner-event.is-tight .planner-lock-badge,
|
| 422 |
+
.planner-event.is-tight .planner-event-time {
|
| 423 |
+
display: none;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.planner-event.is-tight .planner-event-top {
|
| 427 |
+
display: block;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.planner-event.is-tight .planner-event-top strong {
|
| 431 |
+
max-width: 100%;
|
| 432 |
+
font-size: 0.74rem;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.planner-event.is-tight .planner-event-resize {
|
| 436 |
+
left: 8px;
|
| 437 |
+
right: 8px;
|
| 438 |
+
height: 5px;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
.planner-sidebar {
|
| 442 |
padding: 18px;
|
| 443 |
display: grid;
|