NavigationContainer.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import { Container } from 'unstated';
  2. import loggerFactory from '@alias/logger';
  3. const logger = loggerFactory('growi:services:NavigationContainer');
  4. /**
  5. * Service container related to options for Application
  6. * @extends {Container} unstated Container
  7. */
  8. const SCROLL_THRES_SKIP = 200;
  9. const WIKI_HEADER_LINK = 120;
  10. export default class NavigationContainer extends Container {
  11. constructor(appContainer) {
  12. super();
  13. this.appContainer = appContainer;
  14. this.appContainer.registerContainer(this);
  15. const { localStorage } = window;
  16. this.state = {
  17. editorMode: 'view',
  18. isDeviceSmallerThanMd: null,
  19. preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
  20. preferDrawerModeOnEditByUser: // default: true
  21. localStorage.preferDrawerModeOnEditByUser == null || localStorage.preferDrawerModeOnEditByUser === 'true',
  22. isDrawerMode: null,
  23. isDrawerOpened: false,
  24. sidebarContentsId: localStorage.sidebarContentsId || 'recent',
  25. isScrollTop: true,
  26. isPageCreateModalShown: false,
  27. };
  28. this.openPageCreateModal = this.openPageCreateModal.bind(this);
  29. this.closePageCreateModal = this.closePageCreateModal.bind(this);
  30. this.setEditorMode = this.setEditorMode.bind(this);
  31. this.initDeviceSize();
  32. this.initScrollEvent();
  33. }
  34. /**
  35. * Workaround for the mangling in production build to break constructor.name
  36. */
  37. static getClassName() {
  38. return 'NavigationContainer';
  39. }
  40. getPageContainer() {
  41. return this.appContainer.getContainer('PageContainer');
  42. }
  43. initDeviceSize() {
  44. const mdOrAvobeHandler = async(mql) => {
  45. let isDeviceSmallerThanMd;
  46. // sm -> md
  47. if (mql.matches) {
  48. isDeviceSmallerThanMd = false;
  49. }
  50. // md -> sm
  51. else {
  52. isDeviceSmallerThanMd = true;
  53. }
  54. this.setState({ isDeviceSmallerThanMd });
  55. this.updateDrawerMode({ ...this.state, isDeviceSmallerThanMd }); // generate newest state object
  56. };
  57. this.appContainer.addBreakpointListener('md', mdOrAvobeHandler, true);
  58. }
  59. initScrollEvent() {
  60. window.addEventListener('scroll', () => {
  61. const currentYOffset = window.pageYOffset;
  62. // original throttling
  63. if (SCROLL_THRES_SKIP < currentYOffset) {
  64. return;
  65. }
  66. this.setState({
  67. isScrollTop: currentYOffset === 0,
  68. });
  69. });
  70. }
  71. setEditorMode(editorMode) {
  72. const { isNotCreatable } = this.getPageContainer().state;
  73. if (this.appContainer.currentUser == null) {
  74. logger.warn('Please login or signup to edit the page or use hackmd.');
  75. return;
  76. }
  77. if (isNotCreatable) {
  78. logger.warn('This page could not edit.');
  79. return;
  80. }
  81. this.setState({ editorMode });
  82. if (editorMode === 'view') {
  83. $('body').removeClass('on-edit');
  84. $('body').removeClass('builtin-editor');
  85. $('body').removeClass('hackmd');
  86. $('body').removeClass('pathname-sidebar');
  87. window.history.replaceState(null, '', window.location.pathname);
  88. }
  89. if (editorMode === 'edit') {
  90. $('body').addClass('on-edit');
  91. $('body').addClass('builtin-editor');
  92. $('body').removeClass('hackmd');
  93. // editing /Sidebar
  94. if (window.location.pathname === '/Sidebar') {
  95. $('body').addClass('pathname-sidebar');
  96. }
  97. window.location.hash = '#edit';
  98. }
  99. if (editorMode === 'hackmd') {
  100. $('body').addClass('on-edit');
  101. $('body').addClass('hackmd');
  102. $('body').removeClass('builtin-editor');
  103. $('body').removeClass('pathname-sidebar');
  104. window.location.hash = '#hackmd';
  105. }
  106. this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
  107. }
  108. toggleDrawer() {
  109. const { isDrawerOpened } = this.state;
  110. this.setState({ isDrawerOpened: !isDrawerOpened });
  111. }
  112. /**
  113. * Set Sidebar mode preference by user
  114. * @param {boolean} preferDockMode
  115. */
  116. async setDrawerModePreference(bool) {
  117. this.setState({ preferDrawerModeByUser: bool });
  118. this.updateDrawerMode({ ...this.state, preferDrawerModeByUser: bool }); // generate newest state object
  119. // store settings to localStorage
  120. const { localStorage } = window;
  121. localStorage.preferDrawerModeByUser = bool;
  122. }
  123. /**
  124. * Set Sidebar mode preference by user
  125. * @param {boolean} preferDockMode
  126. */
  127. async setDrawerModePreferenceOnEdit(bool) {
  128. this.setState({ preferDrawerModeOnEditByUser: bool });
  129. this.updateDrawerMode({ ...this.state, preferDrawerModeOnEditByUser: bool }); // generate newest state object
  130. // store settings to localStorage
  131. const { localStorage } = window;
  132. localStorage.preferDrawerModeOnEditByUser = bool;
  133. }
  134. /**
  135. * Update drawer related state by specified 'newState' object
  136. * @param {object} newState A newest state object
  137. *
  138. * Specify 'newState' like following code:
  139. *
  140. * { ...this.state, overwriteParam: overwriteValue }
  141. *
  142. * because updating state of unstated container will be delayed unless you use await
  143. */
  144. updateDrawerMode(newState) {
  145. const {
  146. editorMode, isDeviceSmallerThanMd, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
  147. } = newState;
  148. // get preference on view or edit
  149. const preferDrawerMode = editorMode !== 'view' ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
  150. const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
  151. const isDrawerOpened = false; // close Drawer anyway
  152. this.setState({ isDrawerMode, isDrawerOpened });
  153. }
  154. selectSidebarContents(contentsId) {
  155. window.localStorage.setItem('sidebarContentsId', contentsId);
  156. this.setState({ sidebarContentsId: contentsId });
  157. }
  158. openPageCreateModal() {
  159. if (this.appContainer.currentUser == null) {
  160. logger.warn('Please login or signup to create a new page.');
  161. return;
  162. }
  163. this.setState({ isPageCreateModalShown: true });
  164. }
  165. closePageCreateModal() {
  166. this.setState({ isPageCreateModalShown: false });
  167. }
  168. /**
  169. * Function that implements the click event for realizing smooth scroll
  170. * @param {array} elements
  171. */
  172. addSmoothScrollEvent(elements = {}) {
  173. elements.forEach(link => link.addEventListener('click', (e) => {
  174. e.preventDefault();
  175. const href = link.getAttribute('href').replace('#', '');
  176. window.location.hash = href;
  177. const targetDom = document.getElementById(href);
  178. this.smoothScrollIntoView(targetDom, WIKI_HEADER_LINK);
  179. }));
  180. }
  181. smoothScrollIntoView(element = null, offsetTop = 0) {
  182. const targetElement = element || window.document.body;
  183. // get the distance to the target element top
  184. const rectTop = targetElement.getBoundingClientRect().top;
  185. const top = window.pageYOffset + rectTop - offsetTop;
  186. window.scrollTo({
  187. top,
  188. behavior: 'smooth',
  189. });
  190. }
  191. }