Event Merge for Google Calendar™ (by @imightbeAmy)

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

当前为 2018-02-23 提交的版本,查看 最新版本

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