dsh-calendar 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +2 -0
- package/lib/client.js +21 -16
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1(2026-09-19)
|
|
4
|
+
|
|
5
|
+
- **新增真 DOM 行为测试层**(`test/render.test.mjs`,devDependencies 里加 `react` / `react-dom` / `jsdom`,不进发布包):用 jsdom + 真 React 渲染周视图,然后真派发 `pointerdown/move/up`、`click`、`keydown`。7 条覆盖:渲染不抛、点事件不会顺手弹「新建」、拖动只提交一次且按视口坐标换算、拖动中 Esc 零请求、卸载后不留监听器且之后的 pointerup 不提交、拖到边缘 rAF 持续滚动、重复日程被面板挡住。
|
|
6
|
+
- **它的第一批产出就是三个真问题**:
|
|
7
|
+
① **同一批次内拖动不提交**:提交前的「有没有落点」判断读的是 React state(`ghost`),而 `pointermove` 与 `pointerup` 落在同一批次时 state 还没刷新 → 明明拖了却不落盘。改为在 `dragRef` 上记同步标记。
|
|
8
|
+
② `requestAnimationFrame` / `cancelAnimationFrame` 按全局调用:浏览器里有,但模块作用域下应当显式走 `window.*`。
|
|
9
|
+
③ 三处 React key 警告(日期头、事件块、幽灵块、日列的子节点用了数组却没给 key)——dev 模式控制台一直在吵。
|
|
10
|
+
|
|
3
11
|
## 0.8.0(2026-09-19)
|
|
4
12
|
|
|
5
13
|
- **新增:周视图拖拽改期**。拖动事件块上下改时间、左右换天(15 分钟吸附);拖块底部 6px 改时长;拖动中显示幽灵落点,与已有日程重叠时描成黄色(允许重叠,只是提醒);拖到容器上下边缘自动滚动,长会议不用先滚再拖。
|
package/README.md
CHANGED
|
@@ -230,6 +230,8 @@ Nextcloud / 自定义 Basic 服务:检查账号、密码或服务要求的应
|
|
|
230
230
|
|
|
231
231
|
## 开发
|
|
232
232
|
|
|
233
|
+
- **UI 行为测试**:`test/render.test.mjs` 用 jsdom + 真 React 渲染周视图并派发真实事件(点击 / 拖动 / Esc / 卸载 / 边缘滚动)。改前端交互时它会替你点一遍;`pnpm test` 会一起跑。
|
|
234
|
+
|
|
233
235
|
```bash
|
|
234
236
|
pnpm install
|
|
235
237
|
pnpm test # 构建 + node --test
|
package/lib/client.js
CHANGED
|
@@ -834,9 +834,9 @@ function WeekView(props) {
|
|
|
834
834
|
}
|
|
835
835
|
if (commit !== false && moved === true && canEdit === true) {
|
|
836
836
|
const slot = drag.previewSlot === undefined ? drag.slot : drag.previewSlot;
|
|
837
|
-
//
|
|
838
|
-
|
|
839
|
-
if (
|
|
837
|
+
// 没有落点就不提交:比如纵向拖出本周(跨到下周)时幽灵没地方画,这时盲提交会让事件从视图消失。
|
|
838
|
+
// 用 dragRef 上的同步标记,而不是 React state(见 applyPreviewAt 的注释)。
|
|
839
|
+
if (drag.previewVisible === true) onReschedule(drag.event, rangeOfSlot(slot), drag.mode);
|
|
840
840
|
}
|
|
841
841
|
};
|
|
842
842
|
|
|
@@ -858,6 +858,9 @@ function WeekView(props) {
|
|
|
858
858
|
? resizeSlot(drag.slot, (dy / HOUR_PX) * 60, SNAP_MINUTES)
|
|
859
859
|
: slotFromDrag(drag.slot, { columns, pixelsY: dy, hourPx: HOUR_PX }, SNAP_MINUTES);
|
|
860
860
|
drag.previewSlot = slot;
|
|
861
|
+
// 可见性判断必须同步落在 dragRef 上:pointermove 与 pointerup 可能在同一批次里,
|
|
862
|
+
// 此时读 React state(ghost)拿到的是旧值,会导致「拖了却不提交」。
|
|
863
|
+
drag.previewVisible = days.some((day) => dayKey(day) === slot.dayKey);
|
|
861
864
|
setGhost({ uid: drag.uid, slot, conflicts: conflictsForSlot(events, slot, drag.uid).length });
|
|
862
865
|
};
|
|
863
866
|
|
|
@@ -868,7 +871,7 @@ function WeekView(props) {
|
|
|
868
871
|
*/
|
|
869
872
|
const autoScroll = useRef({ raf: 0, x: 0, y: 0 });
|
|
870
873
|
const stopAutoScroll = () => {
|
|
871
|
-
if (autoScroll.current.raf !== 0) { cancelAnimationFrame(autoScroll.current.raf); autoScroll.current.raf = 0; }
|
|
874
|
+
if (autoScroll.current.raf !== 0) { window.cancelAnimationFrame(autoScroll.current.raf); autoScroll.current.raf = 0; }
|
|
872
875
|
};
|
|
873
876
|
const stepAutoScroll = () => {
|
|
874
877
|
autoScroll.current.raf = 0;
|
|
@@ -883,7 +886,7 @@ function WeekView(props) {
|
|
|
883
886
|
if (delta === 0) return; // 指针离开边缘:循环自然停下,下次移动会再起
|
|
884
887
|
node.scrollTop += delta;
|
|
885
888
|
applyPreviewAt(autoScroll.current.x, y, drag); // 滚动后重算落点(含滚动补偿)
|
|
886
|
-
autoScroll.current.raf = requestAnimationFrame(stepAutoScroll);
|
|
889
|
+
autoScroll.current.raf = window.requestAnimationFrame(stepAutoScroll);
|
|
887
890
|
};
|
|
888
891
|
const ensureAutoScroll = (pointerEvent) => {
|
|
889
892
|
autoScroll.current.x = pointerEvent.clientX;
|
|
@@ -892,7 +895,7 @@ function WeekView(props) {
|
|
|
892
895
|
if (node === null || node === undefined) return;
|
|
893
896
|
const rect = node.getBoundingClientRect();
|
|
894
897
|
const nearEdge = pointerEvent.clientY < rect.top + 40 || pointerEvent.clientY > rect.bottom - 40;
|
|
895
|
-
if (nearEdge === true && autoScroll.current.raf === 0) autoScroll.current.raf = requestAnimationFrame(stepAutoScroll);
|
|
898
|
+
if (nearEdge === true && autoScroll.current.raf === 0) autoScroll.current.raf = window.requestAnimationFrame(stepAutoScroll);
|
|
896
899
|
};
|
|
897
900
|
|
|
898
901
|
const updatePreview = (pointerEvent) => {
|
|
@@ -929,7 +932,7 @@ function WeekView(props) {
|
|
|
929
932
|
window.removeEventListener("pointerup", onUp);
|
|
930
933
|
window.removeEventListener("pointercancel", onCancel);
|
|
931
934
|
window.removeEventListener("keydown", onKey, true);
|
|
932
|
-
if (autoScroll.current.raf !== 0) { cancelAnimationFrame(autoScroll.current.raf); autoScroll.current.raf = 0; }
|
|
935
|
+
if (autoScroll.current.raf !== 0) { window.cancelAnimationFrame(autoScroll.current.raf); autoScroll.current.raf = 0; }
|
|
933
936
|
window.removeEventListener("blur", onBlur);
|
|
934
937
|
dragRef.current = null; // 卸载时不能留着半截拖拽状态
|
|
935
938
|
};
|
|
@@ -1009,24 +1012,25 @@ function WeekView(props) {
|
|
|
1009
1012
|
if (suppressClick.current === true) { suppressClick.current = false; return; }
|
|
1010
1013
|
onPick(event);
|
|
1011
1014
|
}
|
|
1012
|
-
},
|
|
1015
|
+
},
|
|
1013
1016
|
h("div", { className: "dshc-slot-title" }, event.summary),
|
|
1014
1017
|
h("div", { className: "dshc-slot-time" }, hhmm(from) + "\u2013" + hhmm(to)),
|
|
1015
1018
|
canEdit === true ? h("div", { className: "dshc-resize", title: t("drag.resizeTitle"), onPointerDown: (e) => { e.stopPropagation(); beginDrag(event, e, "resize"); } }) : null
|
|
1016
|
-
|
|
1019
|
+
);
|
|
1017
1020
|
});
|
|
1018
1021
|
const ghostBlock = ghost !== null && ghost.slot.dayKey === key
|
|
1019
1022
|
? h("div", {
|
|
1020
1023
|
className: "dshc-slot dshc-ghost" + (ghost.conflicts > 0 ? " conflict" : ""),
|
|
1021
1024
|
style: Object.assign({ top: (ghost.slot.minutes * (HOUR_PX / 60)) + "px", height: Math.max(18, ghost.slot.durationMinutes * (HOUR_PX / 60)) + "px" }, laneStyle(0)),
|
|
1022
1025
|
key: "__ghost"
|
|
1023
|
-
},
|
|
1026
|
+
},
|
|
1024
1027
|
h("div", { className: "dshc-slot-title" }, ghost.conflicts > 0 ? t("drag.conflict", { n: ghost.conflicts }) + " · " + t("drag.keep") : t("drag.drop")),
|
|
1025
1028
|
h("div", { className: "dshc-slot-time" }, pad2(Math.floor(ghost.slot.minutes / 60)) + ":" + pad2(ghost.slot.minutes % 60) + " \u2192 " + pad2(Math.floor((ghost.slot.minutes + ghost.slot.durationMinutes) / 60)) + ":" + pad2((ghost.slot.minutes + ghost.slot.durationMinutes) % 60))
|
|
1026
|
-
|
|
1029
|
+
)
|
|
1027
1030
|
: null;
|
|
1028
|
-
|
|
1029
|
-
|
|
1031
|
+
// 可变参数:数组子节点要求 key,而这两块在这里各只出现一次。
|
|
1032
|
+
return h("div", { key, style: { minWidth: 0 } },
|
|
1033
|
+
h("div", { className: "dshc-dayhead" }, h("b", null, String(day.getDate())), " ", relativeDay(day)),
|
|
1030
1034
|
h("div", {
|
|
1031
1035
|
className: "dshc-daycol" + (key === today ? " today" : ""),
|
|
1032
1036
|
"data-day": key,
|
|
@@ -1039,16 +1043,17 @@ function WeekView(props) {
|
|
|
1039
1043
|
},
|
|
1040
1044
|
title: t("action.new")
|
|
1041
1045
|
}, blocks.concat(ghostBlock === null ? [] : [ghostBlock]))
|
|
1042
|
-
|
|
1046
|
+
);
|
|
1043
1047
|
});
|
|
1044
1048
|
|
|
1045
|
-
|
|
1049
|
+
// 可变参数而不是数组:数组子节点要求 key,而这三个都只在 WeekView 里出现一次。
|
|
1050
|
+
return h("div", null,
|
|
1046
1051
|
allDay.length > 0
|
|
1047
1052
|
? h("div", { style: { marginBottom: "8px" } }, allDay.map((event) => h(Row, { key: event.uid, event, onClick: onPick })))
|
|
1048
1053
|
: null,
|
|
1049
1054
|
h("div", { className: "dshc-week", ref: scroller }, [hoursColumn].concat(dayColumns)),
|
|
1050
1055
|
canEdit === true ? h("div", { className: "dshc-draghint" }, t("drag.hint")) : null
|
|
1051
|
-
|
|
1056
|
+
);
|
|
1052
1057
|
}
|
|
1053
1058
|
|
|
1054
1059
|
/** 议程视图:从今天起按天分组,最像「接下来要干什么」。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-calendar",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "DSH 日历插件:CalDAV 日程查询、创建、修改、删除与搜索,支持 Google OAuth2、iCloud、Nextcloud 与自建服务。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -60,6 +60,9 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^24.0.0",
|
|
63
|
+
"jsdom": "^30.1.0",
|
|
64
|
+
"react": "^19.3.0",
|
|
65
|
+
"react-dom": "^19.3.0",
|
|
63
66
|
"typescript": "^5.6.0"
|
|
64
67
|
},
|
|
65
68
|
"repository": {
|