[snolab] Google 日历键盘操作增强

【功能测试中, bug反馈:[email protected]】Google日历键盘增强,雪星自用,功能:双击复制日程视图里的文本内容, Alt+hjkl 移动日程

当前为 2022-07-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
// }