// ==UserScript==
// @name [snolab] Google 日历键盘操作增强
// @name:zh [雪星实验室] Google Calendar with Keyboard Enhanced
// @namespace https://userscript.snomiao.com/
// @version 0.0.7
// @description 【功能测试中, bug反馈:[email protected]】Google日历键盘增强,雪星自用,功能:双击复制日程视图里的文本内容, Alt+hjkl 移动日程
// @author [email protected]
// @match *://calendar.google.com/*
// @grant none
// ==/UserScript==
/*
1. event move enhance
- date time input change
- event drag
2. journal view text copy for the day-summary
*/
console.clear();
const debug = false;
function qsa(sel, ele = document) {
return [...ele.querySelectorAll(sel)];
}
function eleVis(ele) {
return (ele.getClientRects().length && ele) || null;
}
function eleSelVis(sel, ele = document) {
return (typeof sel === "string" && qsa(sel, ele).filter(eleVis)[0]) || null;
}
// const nestList = (e, fn)=>e.reduce
function parentList(ele) {
return [
ele?.parentElement,
...((ele?.parentElement && parentList(ele?.parentElement)) || []),
].filter((e) => e);
}
function eleSearchVis(pattern, ele = document) {
return (
((list) =>
list?.find((e) => e.textContent?.match(pattern)) ||
list?.find((e) => e.innerHTML?.match(pattern)))(
qsa("*", ele).filter(eleVis).reverse()
) || null
);
}
// function eleSearch(sel, ele = document) {
// return ((list) => list?.find((e) => e.textContent?.match(sel)) ||
// list?.find((e) => e.innerHTML?.match(sel)))(qsa("*", ele).reverse()) ||
// null;
// }
function hotkeyNameParse(event) {
const { altKey, metaKey, ctrlKey, shiftKey, key, type } = event;
const hkName =
((altKey && "!") || "") +
((ctrlKey && "^") || "") +
((metaKey && "#") || "") +
((shiftKey && "+") || "") +
key?.toLowerCase() +
({ keydown: "", keypress: " Press", keyup: " Up" }[type] || "");
return hkName;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function inputValueSet(ele, value) {
// console.log('inputValueSet', ele, value);
if (!ele) throw new Error("no element");
if (undefined === value) throw new Error("no value");
ele.value = value;
ele.dispatchEvent(new InputEvent("input", { bubbles: true }));
ele.dispatchEvent(new Event("change", { bubbles: true }));
ele.dispatchEvent(
new KeyboardEvent("keydown", {
bubbles: true,
keyCode: 13 /* enter */,
})
);
await sleep(16);
}
async function waitFor(fn) {
let re = null;
while (!(re = fn())) await sleep(8);
return re;
}
function mouseEventOpt([x, y]) {
return {
isTrusted: true,
bubbles: true,
button: 0,
buttons: 1,
cancelBubble: false,
cancelable: true,
clientX: x,
clientY: y,
movementX: 0,
movementY: 0,
x: x,
y: y,
};
}
function centerGet(元素) {
const { x, y, width: w, height: h } = 元素.getBoundingClientRect();
return [x + w / 2, y + h / 2];
}
// function bottomGet(元素) {
// const { x, y, "width": w, "height": h } = 元素.getBoundingClientRect();
// return [x + w / 2, y + h - 2];
// }
function vec2add([x, y], [z, w]) {
return [x + z, y + w];
}
// function vec2mul([x, y], [z, w]) {
// return [x * z, y * w];
// }
function eventDragMouseMove(dx, dy) {
// a unit size is 15 min
const container = document.querySelector(
'[role="row"][data-dragsource-type="4"]'
);
const gridcells = [...container.querySelectorAll('[role="gridcell"]')];
const containerSize = container.getBoundingClientRect();
const [w, h] = [
containerSize.width / gridcells.length,
containerSize.height / 24 / 4,
];
console.log(w, h);
const [rdx, rdy] = [dx * w, dy * h];
globalThis.gckDraggingPos = vec2add(globalThis.gckDraggingPos, [rdx, rdy]);
document.dispatchEvent(
new MouseEvent("mousemove", mouseEventOpt(globalThis.gckDraggingPos))
);
}
async function eventDragStart(
[dx = 0, dy = 0] = [],
{ expand = false, immediatelyRelease = false } = {}
) {
console.log("eventDrag", [dx, dy], expand, immediatelyRelease);
if (!globalThis.gckDraggingPos) {
// console.log(eventDrag, dx, dy);
const floatingBtn = qsa('div[role="button"]').find(
(e) => getComputedStyle(e).zIndex === "5004"
);
if (!floatingBtn) throw new Error("no event selected");
const dragTarget = expand
? floatingBtn.querySelector('*[data-dragsource-type="3"]')
: floatingBtn;
// debugger;
const cPos = centerGet(dragTarget); // !expand ? : bottomGet(floatingBtn);
console.log("cpos", cPos);
// mousedown
globalThis.gckDraggingPos = cPos;
dragTarget.dispatchEvent(
new MouseEvent(
"mousedown",
mouseEventOpt(globalThis.gckDraggingPos)
)
);
dragTarget.dispatchEvent(
new MouseEvent(
"mousemove",
mouseEventOpt(globalThis.gckDraggingPos)
)
);
}
// mousemove
if (globalThis.gckDraggingPos) {
eventDragMouseMove(dx, dy);
}
// mouseup
function mouseup() {
globalThis.gckDraggingPos = null;
document.dispatchEvent(
new MouseEvent("mouseup", { bubbles: true, cancelable: true })
);
}
function release(event) {
const hkn = hotkeyNameParse(event);
console.log("hkn", hkn);
if (hkn === "!j Up") eventDragMouseMove(0, +1);
if (hkn === "!k Up") eventDragMouseMove(0, -1);
if (hkn === "!h Up") eventDragMouseMove(-1, 0);
if (hkn === "!l Up") eventDragMouseMove(+1, 0);
if (hkn === "!+j Up") eventDragMouseMove(0, +1);
if (hkn === "!+k Up") eventDragMouseMove(0, -1);
if (hkn === "!+h Up") eventDragMouseMove(-1, 0);
if (hkn === "!+l Up") eventDragMouseMove(+1, 0);
if (hkn === "alt Up") mouseup();
if (hkn === "+alt Up") mouseup();
if (hkn === "alt Up") document.removeEventListener("keyup", release);
if (hkn === "+alt Up") document.removeEventListener("keyup", release);
document.removeEventListener("keyup", release);
}
if (immediatelyRelease) {
mouseup();
document.removeEventListener("keyup", release);
} else {
document.addEventListener("keyup", release);
}
}
// const movHandle = async (e) => {
// const hktb = {
// "!j": async () => {
// const pos = bottomGet(floatingBtn);
// document.addEventListener("keyup");
// },
// };
// const f = hktb[hkName];
// if (f) f();
// };
// useHotkey('!j', () => {});
// document.onkeydown = movHandle;
// document.addEventListener('keydown', globalThis.movHandle , false)
async function inputDateTimeChange(startDT = 0, endDT = 0) {
async function isoDateInputParse(dateEle, timeEle) {
// const dateEle = eleSelVis('[aria-label="Start date"]');
const dataDate = dateEle.getAttribute("data-date");
const dataIcal = parentList(dateEle)
.find((e) => e.getAttribute("data-ical"))
.getAttribute("data-ical");
// const todayDate = new Date().toISOString().slice(0, 10);
const dateString = (dataDate || dataIcal).replace(
/(\d{4})(\d{2})(\d{2})/,
(_, a, b, c) => [a, b, c].join("-")
);
const timeString = timeEle?.value || "00:00";
return new Date(`${dateString} ${timeString} Z`);
}
function dateObjParse(dateObj) {
const [date, time] = dateObj
.toISOString()
.match(/(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d):\d\d\.\d\d\dZ/)
.slice(1);
return [date, time];
}
// All day: both dates, no time
// Date time: start date + start time + end date
const startDateEleTry = eleSelVis('[aria-label="Start date"]');
if (!startDateEleTry) {
const tz = eleSearchVis(/^Time zone$/);
const editBtn =
tz &&
parentList(tz)
?.find((e) => e.querySelector('[role="button"]'))
?.querySelector('[role="button"]');
if (!editBtn) {
throw new Error("No editable input");
// return 'No editable input';
}
editBtn.click();
await sleep(16);
}
const startDateEle =
startDateEleTry &&
(await waitFor(() => eleSelVis('[aria-label="Start date"]')));
const startTimeEle = eleSelVis('[aria-label="Start time"]');
const endDateEle = eleSelVis('[aria-label="End date"]');
const endTimeEle = eleSelVis('[aria-label="End time"]');
const startDateObj = await isoDateInputParse(startDateEle, startTimeEle);
const endDateObj = await isoDateInputParse(
endDateEle || startDateEle,
endTimeEle
);
const shiftedStartDateObj = new Date(+startDateObj + startDT);
const shiftedEndDateObj = new Date(+endDateObj + endDT);
const [
originStartDate,
originStartTime,
originEndDate,
originEndTime,
shiftedStartDate,
shiftedStartTime,
shiftedEndDate,
shiftedEndTime,
] = [
...dateObjParse(startDateObj),
...dateObjParse(endDateObj),
...dateObjParse(shiftedStartDateObj),
...dateObjParse(shiftedEndDateObj),
];
debug &&
console.table({
startDateObj: startDateObj.toISOString(),
endDateObj: endDateObj.toISOString(),
shiftedStartDateObj: shiftedStartDateObj.toISOString(),
shiftedEndDateObj: shiftedEndDateObj.toISOString(),
});
debug &&
console.table({
startDateEle: !!startDateEle,
startTimeEle: !!startTimeEle,
endDateEle: !!endDateEle,
endTimeEle: !!endTimeEle,
originStartDate,
originStartTime,
originEndDate,
originEndTime,
shiftedStartDate,
shiftedStartTime,
shiftedEndDate,
shiftedEndTime,
});
startDateEle &&
shiftedStartDate !== originStartDate &&
(await inputValueSet(startDateEle, shiftedStartDate));
endDateEle &&
shiftedEndDate !== originEndDate &&
(await inputValueSet(endDateEle, shiftedEndDate));
startTimeEle &&
shiftedStartTime !== originStartTime &&
(await inputValueSet(startTimeEle, shiftedStartTime));
endTimeEle &&
shiftedEndTime !== originEndTime &&
(await inputValueSet(endTimeEle, shiftedEndTime));
}
async function timeAdd() {
parentList(eleSearchVis(/^Add time$/))
?.find((e) => e.querySelector('[role="button"]'))
?.querySelector('[role="button"]')
.click();
await sleep(16);
return;
}
function gcksHotkeyHandler(e) {
// const isInput = ["INPUT", "BUTTON"].includes(e.target.tagName);
const hkName = hotkeyNameParse(e);
console.log(hkName);
function okay() {
e.preventDefault();
e.stopPropagation();
}
const hkft = {
"!k": async () => {
await timeAdd();
return await inputDateTimeChange(-15 * 60e3).catch(
async () => await eventDragStart([0, 0], { expand: false })
);
},
"!j": async () => {
await timeAdd();
return await inputDateTimeChange(+15 * 60e3).catch(
async () => await eventDragStart([0, 0], { expand: false })
);
},
"!h": async () =>
await inputDateTimeChange(-1 * 86400e3).catch(
async () => await eventDragStart([0, 0], { expand: false })
),
"!l": async () =>
await inputDateTimeChange(+1 * 86400e3).catch(
async () => await eventDragStart([0, 0], { expand: false })
),
"!+k": async () => {
await timeAdd();
return await inputDateTimeChange(0, -15 * 60e3).catch(
async () => await eventDragStart([0, 0], { expand: true })
);
},
"!+j": async () => {
await timeAdd();
return await inputDateTimeChange(0, +15 * 60e3).catch(
async () => await eventDragStart([0, 0], { expand: true })
);
},
"!+h": async () =>
await inputDateTimeChange(0, -1 * 86400e3).catch(
async () => await eventDragStart([0, 0], { expand: true })
),
"!+l": async () =>
await inputDateTimeChange(0, +1 * 86400e3).catch(
async () => await eventDragStart([0, 0], { expand: true })
),
};
const f = hkft[hkName];
if (f) {
okay();
f();
// .then(okay());
// .catch((e) => console.error(e));
} else {
debug && console.log(`${hkName} pressed on `, e.target.tagName, e);
}
console.log("rd");
}
// await inputDateTimeChange(-15 * 60e3);
if (globalThis.gcksHotkeyHandler)
document.removeEventListener(
"keydown",
globalThis.gcksHotkeyHandler,
false
);
globalThis.gcksHotkeyHandler = gcksHotkeyHandler;
document.addEventListener("keydown", globalThis.gcksHotkeyHandler, false);
console.log("done");
// 复制日程内容
function cpy(ele) {
ele.style.background = "lightblue";
setTimeout(() => (ele.style.background = "none"), 200);
return navigator.clipboard.writeText(
ele.innerText
// 把时间和summary拼到一起
.replace(
/.*\n(.*) – (.*)\n(.*)\n.*/gim,
(_, a, b, c) => `${a}-${b} ${c}`
)
// 删掉前2行
.replace(/^.*\n.*\n/, "")
);
}
function mdHandler() {
function dblClickCopyHooker(e) {
if (!e.flag_cpy_eventlistener) {
e.addEventListener("dblclick", () => cpy(e), false);
}
e.flag_cpy_eventlistener = 1;
}
[...document.querySelectorAll("div.L1Ysrb")]?.map(dblClickCopyHooker);
}
document.body.addEventListener("mousedown", mdHandler, true);
// function once(target, eventName, handler, capture = false) {
// const cb = (e) => (
// target.removeEventListener(eventName, cb, capture), handler(e)
// );
// target.addEventListener(eventName, cb, capture);
// }