Event Merge for Google Calendar™ (by @imightbeAmy)

Script that visually merges the same event on multiple Google Calendars into one event.

  1. // ==UserScript==
  2. // @name Event Merge for Google Calendar™ (by @imightbeAmy)
  3. // @namespace gcal-multical-event-merge
  4. // @include https://www.google.com/calendar/*
  5. // @include http://www.google.com/calendar/*
  6. // @include https://calendar.google.com/*
  7. // @include http://calendar.google.com/*
  8. // @version 1
  9. // @grant none
  10. // @description Script that visually merges the same event on multiple Google Calendars into one event.
  11. // ==/UserScript==
  12.  
  13. 'use strict';
  14.  
  15. const stripesGradient = (colors, width, angle) => {
  16. let gradient = `repeating-linear-gradient( ${angle}deg,`;
  17. let pos = 0;
  18.  
  19. const colorCounts = colors.reduce((counts, color) => {
  20. counts[color] = (counts[color] || 0) + 1;
  21. return counts;
  22. }, {});
  23.  
  24. colors.forEach((color, i) => {
  25. colorCounts[color] -= 1;
  26. color = chroma(color).darken(colorCounts[color]/3).css();
  27.  
  28. gradient += color + " " + pos + "px,";
  29. pos += width;
  30. gradient += color + " " + pos + "px,";
  31. });
  32. gradient = gradient.slice(0, -1);
  33. gradient += ")";
  34. return gradient;
  35. };
  36.  
  37. const dragType = e => parseInt(e.dataset.dragsourceType);
  38.  
  39. const calculatePosition = (event, parentPosition) => {
  40. const eventPosition = event.getBoundingClientRect();
  41. return {
  42. left: Math.max(eventPosition.left - parentPosition.left, 0),
  43. right: parentPosition.right - eventPosition.right,
  44. }
  45. }
  46.  
  47. const mergeEventElements = (events) => {
  48. events.sort((e1, e2) => dragType(e1) - dragType(e2));
  49. const colors = events.map(event =>
  50. event.style.backgroundColor || // Week day and full day events marked 'attending'
  51. event.style.borderColor || // Not attending or not responded week view events
  52. event.parentElement.style.borderColor // Timed month view events
  53. );
  54.  
  55. const parentPosition = events[0].parentElement.getBoundingClientRect();
  56. const positions = events.map(event => {
  57. event.originalPosition = event.originalPosition || calculatePosition(event, parentPosition);
  58. return event.originalPosition;
  59. });
  60.  
  61. const eventToKeep = events.shift();
  62. events.forEach(event => {
  63. event.style.visibility = "hidden";
  64. });
  65.  
  66.  
  67. if (eventToKeep.style.backgroundColor || eventToKeep.style.borderColor) {
  68. eventToKeep.originalStyle = eventToKeep.originalStyle || {
  69. backgroundImage: eventToKeep.style.backgroundImage,
  70. backgroundSize: eventToKeep.style.backgroundSize,
  71. left: eventToKeep.style.left,
  72. right: eventToKeep.style.right,
  73. visibility: eventToKeep.style.visibility,
  74. width: eventToKeep.style.width,
  75. border: eventToKeep.style.border,
  76. borderColor: eventToKeep.style.borderColor,
  77. textShadow: eventToKeep.style.textShadow,
  78. };
  79. eventToKeep.style.backgroundImage = stripesGradient(colors, 10, 45);
  80. eventToKeep.style.backgroundSize = "initial";
  81. eventToKeep.style.left = Math.min.apply(Math, positions.map(s => s.left)) + 'px';
  82. eventToKeep.style.right = Math.min.apply(Math, positions.map(s => s.right)) + 'px';
  83. eventToKeep.style.visibility = "visible";
  84. eventToKeep.style.width = null;
  85. eventToKeep.style.border = "solid 1px #FFF";
  86.  
  87. // Clear setting color for declined events
  88. eventToKeep.querySelector('[aria-hidden="true"]').style.color = null;
  89.  
  90. const computedSpanStyle = window.getComputedStyle(eventToKeep.querySelector('span'));
  91. if (computedSpanStyle.color == "rgb(255, 255, 255)") {
  92. eventToKeep.style.textShadow = "0px 0px 2px black";
  93. } else {
  94. eventToKeep.style.textShadow = "0px 0px 2px white";
  95. }
  96.  
  97. events.forEach(event => {
  98. event.style.visibility = "hidden";
  99. });
  100. } else {
  101. const dots = eventToKeep.querySelector('[role="button"] div:first-child');
  102. const dot = dots.querySelector('div');
  103. dot.style.backgroundImage = stripesGradient(colors, 4, 90);
  104. dot.style.width = colors.length * 4 + 'px';
  105. dot.style.borderWidth = 0;
  106. dot.style.height = '8px';
  107.  
  108. events.forEach(event => {
  109. event.style.visibility = "hidden";
  110. });
  111. }
  112. }
  113.  
  114. const resetMergedEvents = (events) => {
  115. events.forEach(event => {
  116. for (var k in event.originalStyle) {
  117. event.style[k] = event.originalStyle[k];
  118. }
  119. event.style.visibility = "visible";
  120. });
  121. }
  122.  
  123. const merge = (mainCalender) => {
  124. const eventSets = {};
  125. const days = mainCalender.querySelectorAll("[role=\"gridcell\"]");
  126. days.forEach((day, index) => {
  127. const events = Array.from(day.querySelectorAll("[data-eventid][role=\"button\"], [data-eventid] [role=\"button\"]"));
  128. events.forEach(event => {
  129. const eventTitleEls = event.querySelectorAll('[aria-hidden="true"]');
  130. if (!eventTitleEls.length) {
  131. return;
  132. }
  133. let eventKey = Array.from(eventTitleEls).map(el => el.textContent).join('').replace(/\\s+/g,"");
  134. eventKey = index + eventKey + event.style.height;
  135. eventSets[eventKey] = eventSets[eventKey] || [];
  136. eventSets[eventKey].push(event);
  137. });
  138. });
  139.  
  140. Object.values(eventSets)
  141. .forEach(events => {
  142. if (events.length > 1) {
  143. mergeEventElements(events);
  144. } else {
  145. resetMergedEvents(events)
  146. }
  147. });
  148. }
  149.  
  150. const init = (mutationsList) => {
  151. mutationsList && mutationsList
  152. .map(mutation => mutation.addedNodes[0] || mutation.target)
  153. .filter(node => node.matches && node.matches("[role=\"main\"], [role=\"dialog\"], [role=\"grid\"]"))
  154. .map(merge);
  155. }
  156.  
  157. setTimeout(() => chrome.storage.local.get('disabled', storage => {
  158. console.log(`Event merge is ${storage.disabled ? 'disabled' : 'enabled'}`);
  159. if (!storage.disabled) {
  160. const observer = new MutationObserver(init);
  161. observer.observe(document.querySelector('body'), { childList: true, subtree: true, attributes: true });
  162. }
  163.  
  164. chrome.storage.onChanged.addListener(() => window.location.reload())
  165. }), 10);

QingJ © 2025

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