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.3
  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 = '100px';
  65. menu.style.minWidth = '120px';
  66. menu.style.position = 'fixed';
  67. menu.style.top = '55px';
  68. menu.style.right = '10px';
  69. menu.style.zIndex = 100000;
  70. menu.style.boxShadow =
  71. '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';
  72. var menuList = document.createElement('div');
  73. menuList.style.maxWidth = '160px';
  74. menuList.style.backgroundColor = '#fff';
  75. menuList.style.padding = '10px';
  76.  
  77. menu.appendChild(menuList);
  78. var closeButtonWrapper = document.createElement('div');
  79. closeButtonWrapper.id = 'notion-toc-menu-sider-close-button';
  80. var closeButton = document.createElement('div');
  81. closeButtonWrapper.style.borderRadius = '12px';
  82. closeButtonWrapper.style.backgroundColor = '#fff';
  83. closeButtonWrapper.style.width = '24px';
  84. closeButtonWrapper.style.height = '24px';
  85. closeButtonWrapper.style.cursor = 'pointer';
  86. closeButtonWrapper.style.zIndex = 100000;
  87. closeButtonWrapper.style.position = 'absolute';
  88. closeButtonWrapper.style.top = '30px';
  89. closeButtonWrapper.style.right = '10px';
  90.  
  91. closeButton.style.width = '10px';
  92. closeButton.style.height = '10px';
  93. closeButton.style.borderLeft = '1px solid black';
  94. closeButton.style.borderBottom = '1px solid black';
  95. closeButton.style.transform = 'rotate(-45deg)';
  96. closeButton.style.position = 'absolute';
  97. closeButton.style.top = '10px';
  98. closeButton.style.left = '7px';
  99. parent.appendChild(closeButtonWrapper);
  100. closeButtonWrapper.appendChild(closeButton);
  101.  
  102. setTimeout(function () {
  103. closeButton.addEventListener('click', function () {
  104. if (!isExpanded) {
  105. closeButton.style.transform = 'rotate(-45deg)';
  106. menu.style.display = 'block';
  107. } else {
  108. closeButton.style.transform = 'rotate(135deg)';
  109. menu.style.display = 'none';
  110. }
  111.  
  112. isExpanded = !isExpanded;
  113. });
  114. });
  115.  
  116. var lists = headings.map(function (h) {
  117. var anchor = document.createElement('a');
  118. anchor.innerText = h.title;
  119. anchor.style.fontSize = '10px';
  120. anchor.href = h.href;
  121. anchor.style.cursor = 'pointer';
  122. anchor.style.display = 'block';
  123. anchor.style.textOverflow = 'ellipsis';
  124. anchor.style.whiteSpace = 'nowrap';
  125. anchor.style.overflow = 'hidden';
  126. anchor.style.color = 'rgb(141,141,141)';
  127.  
  128. switch (h.level) {
  129. case 1:
  130. break;
  131. case 2:
  132. anchor.style.marginLeft = '16px';
  133. break;
  134. case 3:
  135. anchor.style.marginLeft = '24px';
  136. break;
  137. default:
  138. break;
  139. }
  140. return anchor;
  141. });
  142. lists.forEach(function (l) {
  143. menuList.appendChild(l);
  144. });
  145.  
  146. /**** to top ******/
  147. var topAnchor = document.createElement('a');
  148. topAnchor.name = 'top';
  149.  
  150. parent.prepend(topAnchor);
  151. var toTop = document.createElement('div');
  152. toTop.className = 'to-top';
  153. toTop.innerHTML = `<a id="go-to-top" href="#top" target="_Self"></a><svg
  154. class="to-top-button"
  155. role="button"
  156. viewBox="0 0 1024 1024"
  157. xmlns="http://www.w3.org/2000/svg"
  158. style="cursor: pointer;
  159. position: fixed;
  160. bottom: 5rem;
  161. right: 2rem;
  162. width: 2rem;
  163. color: rgb(235, 235, 235);
  164. background-color: #fff;
  165. border-radius: 50%;
  166. overflow: hidden;
  167. z-index: 1;
  168. fill: currentcolor;"
  169. >
  170. <path
  171. 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
  172. 0 512 0zM351.338 271.89h305.434c14.125 0 26.483 12.358 26.483 26.482s-12.358 26.483-26.483
  173. 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
  174. m331.917 303.669c-12.358 12.358-33.545 12.358-45.903 0L531.42 471.393v270.124c0 14.124-12.359
  175. 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
  176. 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
  177. 8.827L683.255 527.89c12.359 15.89 12.359 35.31 0 47.669z"
  178. fill="currentColor"
  179. ></path>
  180. </svg>`;
  181. parent.appendChild(toTop);
  182. var toTopButton = document.querySelector('.to-top-button');
  183. document.addEventListener('scroll', scrollFunction);
  184. function scrollFunction() {
  185. console.log('scrolling');
  186. if (
  187. document.body.scrollTop > 20 ||
  188. document.documentElement.scrollTop > 20
  189. ) {
  190. toTopButton.style.display = 'block';
  191. } else {
  192. toTopButton.style.display = 'none';
  193. }
  194. }
  195.  
  196. toTopButton.addEventListener('click', function (e) {
  197. document.querySelector('#go-to-top').click();
  198. });
  199. }
  200. buildSider();
  201.  
  202. var lastUrlWithoutHash = location.origin + location.pathname;
  203. var lastUrl = location.href;
  204. new MutationObserver(() => {
  205. var url = location.href;
  206. const urlWithoutHash = location.origin + location.pathname;
  207. if (lastUrlWithoutHash !== urlWithoutHash) {
  208. lastUrl = url;
  209. lastUrlWithoutHash = urlWithoutHash;
  210. onUrlChange();
  211. }
  212. }).observe(document, { subtree: true, childList: true });
  213.  
  214. function onUrlChange() {
  215. setTimeout(function () {
  216. var toc = document.querySelector(
  217. '.notion-table_of_contents-block'
  218. );
  219. refresh();
  220. if (toc) {
  221. buildSider();
  222. }
  223. }, 0);
  224.  
  225. // console.log('change');
  226. }
  227. }, 2000);
  228. })();