Event Merge for Google Calendar™ (by @imightbeAmy)

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

当前为 2018-12-21 提交的版本,查看 最新版本

  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. colors.forEach(color => {
  20. gradient += color + " " + pos + "px,";
  21. pos += width;
  22. gradient += color + " " + pos + "px,";
  23. });
  24. gradient = gradient.slice(0, -1);
  25. gradient += ")";
  26. return gradient;
  27. };
  28.  
  29. const dragType = e => parseInt(e.dataset.dragsourceType);
  30.  
  31. const calculatePosition = (event, parentPosition) => {
  32. const eventPosition = event.getBoundingClientRect();
  33. return {
  34. left: Math.max(eventPosition.left - parentPosition.left, 0),
  35. right: parentPosition.right - eventPosition.right,
  36. }
  37. }
  38.  
  39. const mergeEventElements = (events) => {
  40. events.sort((e1, e2) => dragType(e1) - dragType(e2));
  41. const colors = events.map(event =>
  42. event.style.backgroundColor || // Week day and full day events marked 'attending'
  43. event.style.borderColor || // Not attending or not responded week view events
  44. event.parentElement.style.borderColor // Timed month view events
  45. );
  46.  
  47. const parentPosition = events[0].parentElement.getBoundingClientRect();
  48. const positions = events.map(event => {
  49. event.originalPosition = event.originalPosition || calculatePosition(event, parentPosition);
  50. return event.originalPosition;
  51. });
  52.  
  53. const eventToKeep = events.shift();
  54. events.forEach(event => {
  55. event.style.visibility = "hidden";
  56. });
  57.  
  58.  
  59. if (eventToKeep.style.backgroundColor || eventToKeep.style.borderColor) {
  60. eventToKeep.originalStyle = eventToKeep.originalStyle || {
  61. backgroundImage: eventToKeep.style.backgroundImage,
  62. backgroundSize: eventToKeep.style.backgroundSize,
  63. left: eventToKeep.style.left,
  64. right: eventToKeep.style.right,
  65. visibility: eventToKeep.style.visibility,
  66. width: eventToKeep.style.width,
  67. border: eventToKeep.style.border,
  68. };
  69. eventToKeep.style.backgroundImage = stripesGradient(colors, 10, 45);
  70. eventToKeep.style.backgroundSize = "initial";
  71. eventToKeep.style.left = Math.min.apply(Math, positions.map(s => s.left)) + 'px';
  72. eventToKeep.style.right = Math.min.apply(Math, positions.map(s => s.right)) + 'px';
  73. eventToKeep.style.visibility = "visible";
  74. eventToKeep.style.width = null;
  75. eventToKeep.style.border = "solid 1px #FFF"
  76.  
  77. events.forEach(event => {
  78. event.style.visibility = "hidden";
  79. });
  80. } else {
  81. const dots = eventToKeep.querySelector('[role="button"] div:first-child');
  82. const dot = dots.querySelector('div');
  83. dot.style.backgroundImage = stripesGradient(colors, 4, 90);
  84. dot.style.width = colors.length * 4 + 'px';
  85. dot.style.borderWidth = 0;
  86. dot.style.height = '8px';
  87.  
  88. events.forEach(event => {
  89. event.style.visibility = "hidden";
  90. });
  91. }
  92. }
  93.  
  94. const resetMergedEvents = (events) => {
  95. events.forEach(event => {
  96. for (var k in event.originalStyle) {
  97. event.style[k] = event.originalStyle[k];
  98. }
  99. });
  100. }
  101.  
  102. const merge = (mainCalender) => {
  103. const eventSets = {};
  104. const days = mainCalender.querySelectorAll("[role=\"gridcell\"]");
  105. days.forEach((day, index) => {
  106. const events = Array.from(day.querySelectorAll("[data-eventid][role=\"button\"], [data-eventid] [role=\"button\"]"));
  107. events.forEach(event => {
  108. const eventTitleEls = event.querySelectorAll('[aria-hidden="true"]');
  109. if (!eventTitleEls.length) {
  110. return;
  111. }
  112. let eventKey = Array.from(eventTitleEls).map(el => el.textContent).join('').replace(/\\s+/g,"");
  113. eventKey = index + eventKey + event.style.height;
  114. eventSets[eventKey] = eventSets[eventKey] || [];
  115. eventSets[eventKey].push(event);
  116. });
  117. });
  118.  
  119. Object.values(eventSets)
  120. .forEach(events => {
  121. if (events.length > 1) {
  122. mergeEventElements(events);
  123. } else {
  124. resetMergedEvents(events)
  125. }
  126. });
  127. }
  128.  
  129. const init = (mutationsList) => {
  130. mutationsList && mutationsList
  131. .map(mutation => mutation.addedNodes[0] || mutation.target)
  132. .filter(node => node.matches && node.matches("[role=\"main\"], [role=\"dialog\"]"))
  133. .map(merge);
  134. }
  135.  
  136. setTimeout(() => chrome.storage.local.get('disabled', storage => {
  137. console.log(`Event merge is ${storage.disabled ? 'disabled' : 'enabled'}`);
  138. if (!storage.disabled) {
  139. const observer = new MutationObserver(init);
  140. observer.observe(document.querySelector('body'), { childList: true, subtree: true, attributes: true });
  141. }
  142.  
  143. chrome.storage.onChanged.addListener(() => window.location.reload())
  144. }), 10);