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

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

  1. // ==UserScript==
  2. // @name [snolab] Google 日历键盘操作增强
  3. // @name:zh [雪星实验室] Google Calendar with Keyboard Enhanced
  4. // @namespace https://userscript.snomiao.com/
  5. // @version 0.0.6
  6. // @description 【功能测试中, bug反馈:snomiao@gmail.com】Google日历键盘增强,雪星自用,功能:双击复制日程视图里的文本内容, Alt+hjkl 移动日程
  7. // @author snomiao@gmail.com
  8. // @match *://calendar.google.com/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. /*
  13. 1. event move enhance
  14. - date time input change
  15. - event drag
  16. 2. journal view text copy for the day-summary
  17. */
  18. console.clear();
  19. const debug = false;
  20. const qsa = (sel, ele = document) => [...ele.querySelectorAll(sel)];
  21. const eleVis = (ele) => (ele.getClientRects().length && ele) || null;
  22. const eleSelVis = (sel, ele = document) =>
  23. (typeof sel === 'string' && qsa(sel, ele).filter(eleVis)[0]) || null;
  24. // const nestList = (e, fn)=>e.reduce
  25. const parentList = (ele) =>
  26. [
  27. ele?.parentElement,
  28. ...((ele?.parentElement && parentList(ele?.parentElement)) || []),
  29. ].filter((e) => e);
  30. const eleSearchVis = (pattern, ele = document) =>
  31. ((list) =>
  32. list?.find((e) => e.textContent?.match(pattern)) ||
  33. list?.find((e) => e.innerHTML?.match(pattern)))(
  34. qsa('*', ele).filter(eleVis).reverse()
  35. ) || null;
  36. const eleSearch = (sel, ele = document) =>
  37. ((list) =>
  38. list?.find((e) => e.textContent?.match(sel)) ||
  39. list?.find((e) => e.innerHTML?.match(sel)))(qsa('*', ele).reverse()) ||
  40. null;
  41. const hotkeyNameParse = (event) => {
  42. const { altKey, metaKey, ctrlKey, shiftKey, key, type } = event;
  43. const hkName =
  44. ((altKey && '!') || '') +
  45. ((ctrlKey && '^') || '') +
  46. ((metaKey && '#') || '') +
  47. ((shiftKey && '+') || '') +
  48. key?.toLowerCase() +
  49. ({ keydown: '', keypress: ' Press', keyup: ' Up' }[type] || '');
  50. return hkName;
  51. };
  52. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  53. const inputValueSet = async (ele, value) => {
  54. // console.log('inputValueSet', ele, value);
  55. if (!ele) throw new Error('no element');
  56. if (undefined === value) throw new Error('no value');
  57. ele.value = value;
  58. ele.dispatchEvent(new InputEvent('input', { bubbles: true }));
  59. ele.dispatchEvent(new Event('change', { bubbles: true }));
  60. ele.dispatchEvent(
  61. new KeyboardEvent('keydown', {
  62. bubbles: true,
  63. keyCode: 13 /* enter */,
  64. })
  65. );
  66. await sleep(16);
  67. };
  68. const waitFor = async (fn) => {
  69. let re = null;
  70. while (!(re = fn())) await sleep(8);
  71. return re;
  72. };
  73.  
  74. const mouseEventOpt = ([x, y]) => ({
  75. isTrusted: true,
  76. bubbles: true,
  77. button: 0,
  78. buttons: 1,
  79. cancelBubble: false,
  80. cancelable: true,
  81. clientX: x,
  82. clientY: y,
  83. movementX: 0,
  84. movementY: 0,
  85. x: x,
  86. y: y,
  87. });
  88. const centerGet = (元素) => {
  89. const { x, y, width: w, height: h } = 元素.getBoundingClientRect();
  90. return [x + w / 2, y + h / 2];
  91. };
  92. const bottomGet = (元素) => {
  93. const { x, y, width: w, height: h } = 元素.getBoundingClientRect();
  94. return [x + w / 2, y + h - 2];
  95. };
  96. const vec2add = ([x, y], [z, w]) => [x + z, y + w];
  97. const vec2mul = ([x, y], [z, w]) => [x * z, y * w];
  98. const eventDragMouseMove = (dx, dy) => {
  99. // a unit size is 15 min
  100. const container = document.querySelector(
  101. '[role="row"][data-dragsource-type="4"]'
  102. );
  103. const gridcells = [...container.querySelectorAll('[role="gridcell"]')];
  104. const containerSize = container.getBoundingClientRect();
  105. const [w, h] = [
  106. containerSize.width / gridcells.length,
  107. containerSize.height / 24 / 4,
  108. ];
  109. const [rdx, rdy] = [dx * w, dy * h];
  110. globalThis.gckDraggingPos = vec2add(globalThis.gckDraggingPos, [rdx, rdy]);
  111. document.dispatchEvent(
  112. new MouseEvent('mousemove', mouseEventOpt(globalThis.gckDraggingPos))
  113. );
  114. };
  115. const eventDragStart = async (
  116. [dx = 0, dy = 0] = [],
  117. { expand = false, immediatelyRelease = false } = {}
  118. ) => {
  119. console.log('eventDrag', [dx, dy], expand, immediatelyRelease);
  120. if (!globalThis.gckDraggingPos) {
  121. // console.log(eventDrag, dx, dy);
  122. const floatingBtn = qsa('div[role="button"]').find(
  123. (e) => getComputedStyle(e).zIndex === '5004'
  124. );
  125. if (!floatingBtn) throw new Error('no event selected');
  126. const dragTarget = expand
  127. ? floatingBtn.querySelector('*[data-dragsource-type="3"]')
  128. : floatingBtn;
  129. // debugger;
  130. const cPos = centerGet(dragTarget); // !expand ? : bottomGet(floatingBtn);
  131. console.log('cpos', cPos);
  132. // mousedown
  133. globalThis.gckDraggingPos = cPos;
  134. dragTarget.dispatchEvent(
  135. new MouseEvent(
  136. 'mousedown',
  137. mouseEventOpt(globalThis.gckDraggingPos)
  138. )
  139. );
  140. dragTarget.dispatchEvent(
  141. new MouseEvent(
  142. 'mousemove',
  143. mouseEventOpt(globalThis.gckDraggingPos)
  144. )
  145. );
  146. }
  147. // mousemove
  148. if (globalThis.gckDraggingPos) {
  149. eventDragMouseMove(dx, dy);
  150. }
  151. // mouseup
  152. const mouseup = () => {
  153. globalThis.gckDraggingPos = null;
  154. document.dispatchEvent(
  155. new MouseEvent('mouseup', { bubbles: true, cancelable: true })
  156. );
  157. };
  158. const release = (event) => {
  159. const hkn = hotkeyNameParse(event);
  160. console.log('hkn', hkn);
  161. // ;
  162. if (hkn === '!j Up') eventDragMouseMove(0, +1);
  163. if (hkn === '!k Up') eventDragMouseMove(0, -1);
  164. if (hkn === '!h Up') eventDragMouseMove(-1, 0);
  165. if (hkn === '!l Up') eventDragMouseMove(+1, 0);
  166. if (hkn === '!+j Up') eventDragMouseMove(0, +1);
  167. if (hkn === '!+k Up') eventDragMouseMove(0, -1);
  168. if (hkn === '!+h Up') eventDragMouseMove(-1, 0);
  169. if (hkn === '!+l Up') eventDragMouseMove(+1, 0);
  170. if (hkn === 'alt Up') mouseup();
  171. if (hkn === '+alt Up') mouseup();
  172. if (hkn === 'alt Up') document.removeEventListener('keyup', release);
  173. if (hkn === '+alt Up') document.removeEventListener('keyup', release);
  174. };
  175. if (immediatelyRelease) {
  176. mouseup();
  177. document.removeEventListener('keyup', release);
  178. } else {
  179. document.addEventListener('keyup', release);
  180. }
  181. };
  182. const movHandle = async (e) => {
  183. const hktb = {
  184. '!j': async () => {
  185. let pos = bottomGet(floatingBtn);
  186. document.addEventListener('keyup');
  187. },
  188. };
  189. const f = hktb[hkName];
  190. if (f) f();
  191. };
  192. // useHotkey('!j', () => {});
  193. // document.onkeydown = movHandle;
  194. // document.addEventListener('keydown', globalThis.movHandle , false)
  195.  
  196. const inputDateTimeChange = async (startDT = 0, endDT = 0) => {
  197. const isoDateInputParse = async (dateEle, timeEle) => {
  198. // const dateEle = eleSelVis('[aria-label="Start date"]');
  199. const dataDate = dateEle.getAttribute('data-date');
  200. const dataIcal = parentList(dateEle)
  201. .find((e) => e.getAttribute('data-ical'))
  202. .getAttribute('data-ical');
  203. const todayDate = new Date().toISOString().slice(0, 10);
  204. const dateString = (dataDate || dataIcal).replace(
  205. /(\d{4})(\d{2})(\d{2})/,
  206. (_, a, b, c) => [a, b, c].join('-')
  207. );
  208. const timeString = timeEle?.value || '00:00';
  209. return new Date(`${dateString} ${timeString} Z`);
  210. };
  211. const dateObjParse = (dateObj) => {
  212. const [date, time] = dateObj
  213. .toISOString()
  214. .match(/(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d):\d\d\.\d\d\dZ/)
  215. .slice(1);
  216. return [date, time];
  217. };
  218. // All day: both dates, no time
  219. // Date time: start date + start time + end date
  220. const startDateEleTry = eleSelVis('[aria-label="Start date"]');
  221. if (!startDateEleTry) {
  222. const tz = eleSearchVis(/^Time zone$/);
  223. const editBtn =
  224. tz &&
  225. parentList(tz)
  226. ?.find((e) => e.querySelector('[role="button"]'))
  227. ?.querySelector('[role="button"]');
  228. if (!editBtn) {
  229. throw new Error('No editable input');
  230. // return 'No editable input';
  231. }
  232. editBtn.click();
  233. await sleep(16);
  234. }
  235. const startDateEle =
  236. startDateEleTry &&
  237. (await waitFor(() => eleSelVis('[aria-label="Start date"]')));
  238. const startTimeEle = eleSelVis('[aria-label="Start time"]');
  239. const endDateEle = eleSelVis('[aria-label="End date"]');
  240. const endTimeEle = eleSelVis('[aria-label="End time"]');
  241. const startDateObj = await isoDateInputParse(startDateEle, startTimeEle);
  242. const endDateObj = await isoDateInputParse(
  243. endDateEle || startDateEle,
  244. endTimeEle
  245. );
  246. const shiftedStartDateObj = new Date(+startDateObj + startDT);
  247. const shiftedEndDateObj = new Date(+endDateObj + endDT);
  248. const [
  249. originStartDate,
  250. originStartTime,
  251. originEndDate,
  252. originEndTime,
  253. shiftedStartDate,
  254. shiftedStartTime,
  255. shiftedEndDate,
  256. shiftedEndTime,
  257. ] = [
  258. ...dateObjParse(startDateObj),
  259. ...dateObjParse(endDateObj),
  260. ...dateObjParse(shiftedStartDateObj),
  261. ...dateObjParse(shiftedEndDateObj),
  262. ];
  263. debug &&
  264. console.table({
  265. startDateObj: startDateObj.toISOString(),
  266. endDateObj: endDateObj.toISOString(),
  267. shiftedStartDateObj: shiftedStartDateObj.toISOString(),
  268. shiftedEndDateObj: shiftedEndDateObj.toISOString(),
  269. });
  270. debug &&
  271. console.table({
  272. startDateEle: !!startDateEle,
  273. startTimeEle: !!startTimeEle,
  274. endDateEle: !!endDateEle,
  275. endTimeEle: !!endTimeEle,
  276. originStartDate,
  277. originStartTime,
  278. originEndDate,
  279. originEndTime,
  280. shiftedStartDate,
  281. shiftedStartTime,
  282. shiftedEndDate,
  283. shiftedEndTime,
  284. });
  285. startDateEle &&
  286. shiftedStartDate !== originStartDate &&
  287. (await inputValueSet(startDateEle, shiftedStartDate));
  288. endDateEle &&
  289. shiftedEndDate !== originEndDate &&
  290. (await inputValueSet(endDateEle, shiftedEndDate));
  291. startTimeEle &&
  292. shiftedStartTime !== originStartTime &&
  293. (await inputValueSet(startTimeEle, shiftedStartTime));
  294. endTimeEle &&
  295. shiftedEndTime !== originEndTime &&
  296. (await inputValueSet(endTimeEle, shiftedEndTime));
  297. };
  298. const timeAdd = async () => {
  299. parentList(eleSearchVis(/^Add time$/))
  300. ?.find((e) => e.querySelector('[role="button"]'))
  301. ?.querySelector('[role="button"]')
  302. .click();
  303. await sleep(16);
  304. return;
  305. };
  306. const gcksHotkeyHandler = (e) => {
  307. const isInput = ['INPUT', 'BUTTON'].includes(e.target.tagName);
  308. const hkName = hotkeyNameParse(e);
  309. console.log(hkName);
  310. const okay = () => {
  311. e.preventDefault();
  312. e.stopPropagation();
  313. };
  314. const hkft = {
  315. '!k': async () => {
  316. await timeAdd();
  317. return await inputDateTimeChange(-15 * 60e3).catch(
  318. async () => await eventDragStart([0, 0], { expand: false })
  319. );
  320. },
  321. '!j': async () => {
  322. await timeAdd();
  323. return await inputDateTimeChange(+15 * 60e3).catch(
  324. async () => await eventDragStart([0, 0], { expand: false })
  325. );
  326. },
  327. '!h': async () =>
  328. await inputDateTimeChange(-1 * 86400e3).catch(
  329. async () => await eventDragStart([0, 0], { expand: false })
  330. ),
  331. '!l': async () =>
  332. await inputDateTimeChange(+1 * 86400e3).catch(
  333. async () => await eventDragStart([0, 0], { expand: false })
  334. ),
  335. '!+k': async () => {
  336. await timeAdd();
  337. return await inputDateTimeChange(0, -15 * 60e3).catch(
  338. async () => await eventDragStart([0, 0], { expand: true })
  339. );
  340. },
  341. '!+j': async () => {
  342. await timeAdd();
  343. return await inputDateTimeChange(0, +15 * 60e3).catch(
  344. async () => await eventDragStart([0, 0], { expand: true })
  345. );
  346. },
  347. '!+h': async () =>
  348. await inputDateTimeChange(0, -1 * 86400e3).catch(
  349. async () => await eventDragStart([0, 0], { expand: true })
  350. ),
  351. '!+l': async () =>
  352. await inputDateTimeChange(0, +1 * 86400e3).catch(
  353. async () => await eventDragStart([0, 0], { expand: true })
  354. ),
  355. };
  356. const f = hkft[hkName];
  357. if (f) {
  358. okay();
  359. f();
  360. // .then(okay());
  361. // .catch((e) => console.error(e));
  362. } else {
  363. debug && console.log(hkName + ' pressed on ', e.target.tagName, e);
  364. }
  365. console.log('rd');
  366. };
  367. // await inputDateTimeChange(-15 * 60e3);
  368.  
  369. globalThis.gcksHotkeyHandler &&
  370. document.removeEventListener(
  371. 'keydown',
  372. globalThis.gcksHotkeyHandler,
  373. false
  374. );
  375. globalThis.gcksHotkeyHandler = gcksHotkeyHandler;
  376. document.addEventListener('keydown', globalThis.gcksHotkeyHandler, false);
  377. console.log('done');
  378.  
  379. // 复制日程内容
  380. var cpy = (ele) => {
  381. ele.style.background = 'lightblue';
  382. setTimeout(() => (ele.style.background = 'none'), 200);
  383. return navigator.clipboard.writeText(
  384. ele.innerText
  385. // 把时间和summary拼到一起
  386. .replace(
  387. /.*\n(.*) – (.*)\n(.*)\n.*/gim,
  388. (_, a, b, c) => a + '-' + b + ' ' + c
  389. )
  390. // 删掉前2行
  391. .replace(/^.*\n.*\n/, '')
  392. );
  393. };
  394. const mdHandler = () => {
  395. const dblClickCopyHooker = (e) => {
  396. if (!e.flag_cpy_eventlistener) {
  397. e.addEventListener('dblclick', () => cpy(e), false);
  398. }
  399. e.flag_cpy_eventlistener = 1;
  400. };
  401. [...document.querySelectorAll('div.L1Ysrb')]?.map(dblClickCopyHooker);
  402. };
  403. document.body.addEventListener('mousedown', mdHandler, true);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址