notion TOC sider & go to top

display a fixed sider on the top-right of notion page which has a table of content block inside

目前為 2022-06-17 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name notion TOC sider & go to top
  3. // @namespace https://www.notion.so/
  4. // @version 0.0.4
  5. // @description display a fixed sider on the top-right of notion page which has a table of content block inside
  6. // @author nan chen
  7. // @match *://www.notion.so/*
  8. // @icon https://www.google.com/s2/favicons?domain=notion.so
  9. // @grant none
  10. // @run-at document-idle
  11. // @license MIT
  12. // ==/UserScript==
  13. (function () {
  14. setTimeout(function () {
  15. var tocEle = document.querySelector(
  16. '.notion-table_of_contents-block'
  17. );
  18. var parent = document.querySelector('#notion-app');
  19.  
  20. function refresh() {
  21. var tocMenu = document.querySelector('#notion-toc-menu-sider');
  22. var closeButton = document.querySelector(
  23. '#notion-toc-menu-sider-close-button'
  24. );
  25. if (tocMenu) {
  26. parent.removeChild(tocMenu);
  27. }
  28. if (closeButton) {
  29. parent.removeChild(closeButton);
  30. }
  31. }
  32. function buildSider() {
  33. var headings = [];
  34. var parseLevel = function (outerHtml) {
  35. if (outerHtml.includes('margin-left: 24px')) {
  36. return 2;
  37. }
  38. if (outerHtml.includes('margin-left: 48px')) {
  39. return 3;
  40. }
  41. return 1;
  42. };
  43. var tocEleToBuild = document.querySelector(
  44. '.notion-table_of_contents-block'
  45. );
  46. if (!tocEleToBuild) return;
  47.  
  48. tocEleToBuild.childNodes[0].childNodes.forEach(function (n) {
  49. if (n.nodeType === 1) {
  50. var an = n.querySelector('a');
  51. headings.push({
  52. title: n.innerText,
  53. level: parseLevel(n.outerHTML),
  54. href: an.href,
  55. });
  56. }
  57. });
  58.  
  59. var menu = document.createElement('aside');
  60. menu.id = 'notion-toc-menu-sider';
  61. var isExpanded = true;
  62. parent.appendChild(menu);
  63.  
  64. menu.style.minWidth = '120px';
  65. menu.style.maxHeight = '600px';
  66. menu.style.overflowY = 'auto';
  67. menu.style.position = 'fixed';
  68. menu.style.top = '55px';
  69. menu.style.right = '10px';
  70. menu.style.zIndex = 100000;
  71. menu.style.boxShadow =
  72. 'rgb(15 15 15 / 5%) 0px 0px 0px 1px, rgb(15 15 15 / 10%) 0px 3px 6px, rgb(15 15 15 / 20%) 0px 9px 24px';
  73. var menuList = document.createElement('div');
  74. menuList.style.maxWidth = '160px';
  75. menuList.style.backgroundColor = '#fff';
  76. menuList.style.padding = '10px';
  77.  
  78. menu.appendChild(menuList);
  79. var closeButtonWrapper = document.createElement('div');
  80. closeButtonWrapper.id = 'notion-toc-menu-sider-close-button';
  81. var closeButton = document.createElement('div');
  82. closeButtonWrapper.style.borderRadius = '12px';
  83. closeButtonWrapper.style.backgroundColor = '#fff';
  84. closeButtonWrapper.style.width = '24px';
  85. closeButtonWrapper.style.height = '24px';
  86. closeButtonWrapper.style.cursor = 'pointer';
  87. closeButtonWrapper.style.zIndex = 100000;
  88. closeButtonWrapper.style.position = 'absolute';
  89. closeButtonWrapper.style.top = '30px';
  90. closeButtonWrapper.style.right = '10px';
  91.  
  92. closeButton.style.width = '10px';
  93. closeButton.style.height = '10px';
  94. closeButton.style.borderLeft = '1px solid black';
  95. closeButton.style.borderBottom = '1px solid black';
  96. closeButton.style.transform = 'rotate(-45deg)';
  97. closeButton.style.position = 'absolute';
  98. closeButton.style.top = '10px';
  99. closeButton.style.left = '7px';
  100. parent.appendChild(closeButtonWrapper);
  101. closeButtonWrapper.appendChild(closeButton);
  102.  
  103. setTimeout(function () {
  104. closeButton.addEventListener('click', function () {
  105. if (!isExpanded) {
  106. closeButton.style.transform = 'rotate(-45deg)';
  107. menu.style.display = 'block';
  108. } else {
  109. closeButton.style.transform = 'rotate(135deg)';
  110. menu.style.display = 'none';
  111. }
  112.  
  113. isExpanded = !isExpanded;
  114. });
  115. });
  116.  
  117. var lists = headings.map(function (h) {
  118. var anchor = document.createElement('a');
  119. anchor.innerText = h.title;
  120. anchor.style.fontSize = '10px';
  121. anchor.href = h.href;
  122. anchor.style.cursor = 'pointer';
  123. anchor.style.display = 'block';
  124. anchor.style.textOverflow = 'ellipsis';
  125. anchor.style.whiteSpace = 'nowrap';
  126. anchor.style.overflow = 'hidden';
  127. anchor.style.color = 'rgb(141,141,141)';
  128.  
  129. switch (h.level) {
  130. case 1:
  131. break;
  132. case 2:
  133. anchor.style.marginLeft = '16px';
  134. break;
  135. case 3:
  136. anchor.style.marginLeft = '24px';
  137. break;
  138. default:
  139. break;
  140. }
  141. return anchor;
  142. });
  143. lists.forEach(function (l) {
  144. menuList.appendChild(l);
  145. });
  146.  
  147. /**** to top ******/
  148. var topAnchor = document.createElement('a');
  149. topAnchor.name = 'top';
  150.  
  151. parent.prepend(topAnchor);
  152. var toTop = document.createElement('div');
  153. toTop.className = 'to-top';
  154. toTop.innerHTML = `<a id="go-to-top" href="#top" target="_Self"></a><svg
  155. class="to-top-button"
  156. role="button"
  157. viewBox="0 0 1024 1024"
  158. xmlns="http://www.w3.org/2000/svg"
  159. style="cursor: pointer;
  160. position: fixed;
  161. bottom: 5rem;
  162. right: 2rem;
  163. width: 2rem;
  164. color: rgb(235, 235, 235);
  165. background-color: #fff;
  166. border-radius: 50%;
  167. overflow: hidden;
  168. z-index: 1;
  169. fill: currentcolor;"
  170. >
  171. <path
  172. d="M512 0C229.517 0 0 229.517 0 512s227.752 512 512 512c282.483 0 512-227.752 512-512C1024 229.517 794.483
  173. 0 512 0zM351.338 271.89h305.434c14.125 0 26.483 12.358 26.483 26.482s-12.358 26.483-26.483
  174. 26.483H351.338c-14.124 0-26.483-12.358-26.483-26.483 0-15.89 12.359-26.482 26.483-26.482z
  175. m331.917 303.669c-12.358 12.358-33.545 12.358-45.903 0L531.42 471.393v270.124c0 14.124-12.359
  176. 26.483-26.483 26.483s-26.483-12.359-26.483-26.483v-271.89l-105.93 104.166c-12.36 12.359-33.546 12.359-45.904
  177. 0-12.359-12.359-12.359-31.78 0-45.903l155.365-151.835c7.062-7.062 14.124-8.827 22.952-8.827s15.89 3.53 22.952
  178. 8.827L683.255 527.89c12.359 15.89 12.359 35.31 0 47.669z"
  179. fill="currentColor"
  180. ></path>
  181. </svg>`;
  182. parent.appendChild(toTop);
  183. var toTopButton = document.querySelector('.to-top-button');
  184. document.addEventListener('scroll', scrollFunction);
  185. function scrollFunction() {
  186. console.log('scrolling');
  187. if (
  188. document.body.scrollTop > 20 ||
  189. document.documentElement.scrollTop > 20
  190. ) {
  191. toTopButton.style.display = 'block';
  192. } else {
  193. toTopButton.style.display = 'none';
  194. }
  195. }
  196.  
  197. toTopButton.addEventListener('click', function (e) {
  198. document.querySelector('#go-to-top').click();
  199. });
  200. }
  201. buildSider();
  202.  
  203. var lastUrlWithoutHash = location.origin + location.pathname;
  204. var lastUrl = location.href;
  205. new MutationObserver(() => {
  206. var url = location.href;
  207. const urlWithoutHash = location.origin + location.pathname;
  208. if (lastUrlWithoutHash !== urlWithoutHash) {
  209. lastUrl = url;
  210. lastUrlWithoutHash = urlWithoutHash;
  211. onUrlChange();
  212. }
  213. }).observe(document, { subtree: true, childList: true });
  214.  
  215. function onUrlChange() {
  216. setTimeout(function () {
  217. var toc = document.querySelector(
  218. '.notion-table_of_contents-block'
  219. );
  220. refresh();
  221. if (toc) {
  222. buildSider();
  223. }
  224. }, 0);
  225.  
  226. // console.log('change');
  227. }
  228. }, 2000);
  229. })();