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

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

目前為 2022-07-03 提交的版本,檢視 最新版本

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