ui.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import useSWR, {
  2. useSWRConfig, SWRResponse, Key, Fetcher,
  3. } from 'swr';
  4. import useSWRImmutable from 'swr/immutable';
  5. import { Breakpoint, addBreakpointListener } from '@growi/ui';
  6. import { RefObject } from 'react';
  7. import { SidebarContentsType } from '~/interfaces/ui';
  8. import loggerFactory from '~/utils/logger';
  9. import { useStaticSWR } from './use-static-swr';
  10. import { useCurrentPagePath, useIsEditable } from './context';
  11. import { IFocusable } from '~/client/interfaces/focusable';
  12. const logger = loggerFactory('growi:stores:ui');
  13. const isServer = typeof window === 'undefined';
  14. type Nullable<T> = T | null;
  15. /** **********************************************************
  16. * Unions
  17. *********************************************************** */
  18. export const EditorMode = {
  19. View: 'view',
  20. Editor: 'editor',
  21. HackMD: 'hackmd',
  22. } as const;
  23. export type EditorMode = typeof EditorMode[keyof typeof EditorMode];
  24. /** **********************************************************
  25. * SWR Hooks
  26. * for switching UI
  27. *********************************************************** */
  28. export const useIsMobile = (): SWRResponse<boolean|null, Error> => {
  29. const key = isServer ? null : 'isMobile';
  30. let configuration;
  31. if (!isServer) {
  32. const userAgent = window.navigator.userAgent.toLowerCase();
  33. configuration = {
  34. fallbackData: /iphone|ipad|android/.test(userAgent),
  35. };
  36. }
  37. return useStaticSWR(key, null, configuration);
  38. };
  39. const updateBodyClassesForEditorMode = (newEditorMode: EditorMode) => {
  40. switch (newEditorMode) {
  41. case EditorMode.View:
  42. $('body').removeClass('on-edit');
  43. $('body').removeClass('builtin-editor');
  44. $('body').removeClass('hackmd');
  45. $('body').removeClass('pathname-sidebar');
  46. window.history.replaceState(null, '', window.location.pathname);
  47. break;
  48. case EditorMode.Editor:
  49. $('body').addClass('on-edit');
  50. $('body').addClass('builtin-editor');
  51. $('body').removeClass('hackmd');
  52. // editing /Sidebar
  53. if (window.location.pathname === '/Sidebar') {
  54. $('body').addClass('pathname-sidebar');
  55. }
  56. window.location.hash = '#edit';
  57. break;
  58. case EditorMode.HackMD:
  59. $('body').addClass('on-edit');
  60. $('body').addClass('hackmd');
  61. $('body').removeClass('builtin-editor');
  62. $('body').removeClass('pathname-sidebar');
  63. window.location.hash = '#hackmd';
  64. break;
  65. }
  66. };
  67. export const useEditorModeByHash = (): SWRResponse<EditorMode, Error> => {
  68. return useSWRImmutable(
  69. ['initialEditorMode', window.location.hash],
  70. (key: Key, hash: string) => {
  71. switch (hash) {
  72. case '#edit':
  73. return EditorMode.Editor;
  74. case '#hackmd':
  75. return EditorMode.HackMD;
  76. default:
  77. return EditorMode.View;
  78. }
  79. },
  80. );
  81. };
  82. let isEditorModeLoaded = false;
  83. export const useEditorMode = (): SWRResponse<EditorMode, Error> => {
  84. const { data: _isEditable } = useIsEditable();
  85. const { data: editorModeByHash } = useEditorModeByHash();
  86. const isLoading = _isEditable === undefined;
  87. const isEditable = !isLoading && _isEditable;
  88. const initialData = isEditable ? editorModeByHash : EditorMode.View;
  89. const swrResponse = useSWRImmutable(
  90. isLoading ? null : ['editorMode', isEditable],
  91. null,
  92. { fallbackData: initialData },
  93. );
  94. // initial updating
  95. if (!isEditorModeLoaded && !isLoading && swrResponse.data != null) {
  96. if (isEditable) {
  97. updateBodyClassesForEditorMode(swrResponse.data);
  98. }
  99. isEditorModeLoaded = true;
  100. }
  101. return {
  102. ...swrResponse,
  103. // overwrite mutate
  104. mutate: (editorMode: EditorMode, shouldRevalidate?: boolean) => {
  105. if (!isEditable) {
  106. return Promise.resolve(EditorMode.View); // fixed if not editable
  107. }
  108. updateBodyClassesForEditorMode(editorMode);
  109. return swrResponse.mutate(editorMode, shouldRevalidate);
  110. },
  111. };
  112. };
  113. export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean|null, Error> => {
  114. const key: Key = isServer ? null : 'isDeviceSmallerThanMd';
  115. const { cache, mutate } = useSWRConfig();
  116. if (!isServer) {
  117. const mdOrAvobeHandler = function(this: MediaQueryList): void {
  118. // sm -> md: matches will be true
  119. // md -> sm: matches will be false
  120. mutate(key, !this.matches);
  121. };
  122. const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
  123. // initialize
  124. if (cache.get(key) == null) {
  125. document.addEventListener('DOMContentLoaded', () => {
  126. mutate(key, !mql.matches);
  127. });
  128. }
  129. }
  130. return useStaticSWR(key);
  131. };
  132. export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean|null, Error> => {
  133. const key: Key = isServer ? null : 'isDeviceSmallerThanLg';
  134. const { cache, mutate } = useSWRConfig();
  135. if (!isServer) {
  136. const lgOrAvobeHandler = function(this: MediaQueryList): void {
  137. // md -> lg: matches will be true
  138. // lg -> md: matches will be false
  139. mutate(key, !this.matches);
  140. };
  141. const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
  142. // initialize
  143. if (cache.get(key) == null) {
  144. document.addEventListener('DOMContentLoaded', () => {
  145. mutate(key, !mql.matches);
  146. });
  147. }
  148. }
  149. return useStaticSWR(key);
  150. };
  151. export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
  152. return useStaticSWR('preferDrawerModeByUser', initialData ?? null, { fallbackData: false });
  153. };
  154. export const usePreferDrawerModeOnEditByUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
  155. return useStaticSWR('preferDrawerModeOnEditByUser', initialData ?? null, { fallbackData: true });
  156. };
  157. export const useSidebarCollapsed = (initialData?: boolean): SWRResponse<boolean, Error> => {
  158. return useStaticSWR('isSidebarCollapsed', initialData ?? null, { fallbackData: false });
  159. };
  160. export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
  161. return useStaticSWR('sidebarContents', initialData ?? null, { fallbackData: SidebarContentsType.RECENT });
  162. };
  163. export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
  164. return useStaticSWR('productNavWidth', initialData ?? null, { fallbackData: 320 });
  165. };
  166. export const useDrawerMode = (): SWRResponse<boolean, Error> => {
  167. const { data: editorMode } = useEditorMode();
  168. const { data: preferDrawerModeByUser } = usePreferDrawerModeByUser();
  169. const { data: preferDrawerModeOnEditByUser } = usePreferDrawerModeOnEditByUser();
  170. const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
  171. const condition = editorMode != null || preferDrawerModeByUser != null || preferDrawerModeOnEditByUser != null || isDeviceSmallerThanMd != null;
  172. const calcDrawerMode: Fetcher<boolean> = (
  173. key: Key, editorMode: EditorMode, preferDrawerModeByUser: boolean, preferDrawerModeOnEditByUser: boolean, isDeviceSmallerThanMd: boolean,
  174. ): boolean => {
  175. // get preference on view or edit
  176. const preferDrawerMode = editorMode !== EditorMode.View ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
  177. return isDeviceSmallerThanMd || preferDrawerMode;
  178. };
  179. return useSWRImmutable(
  180. condition ? ['isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
  181. calcDrawerMode,
  182. {
  183. fallback: calcDrawerMode,
  184. },
  185. );
  186. };
  187. export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
  188. const initialData = false;
  189. return useStaticSWR('isDrawerOpened', isOpened || null, { fallbackData: initialData });
  190. };
  191. export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<boolean, Error> => {
  192. const initialData = false;
  193. return useStaticSWR('isSidebarResizeDisabled', isDisabled || null, { fallbackData: initialData });
  194. };
  195. type CreateModalStatus = {
  196. isOpened: boolean,
  197. path?: string,
  198. }
  199. type CreateModalStatusUtils = {
  200. open(path?: string): Promise<CreateModalStatus | undefined>
  201. close(): Promise<CreateModalStatus | undefined>
  202. }
  203. export const useCreateModalStatus = (status?: CreateModalStatus): SWRResponse<CreateModalStatus, Error> & CreateModalStatusUtils => {
  204. const swrResponse = useStaticSWR<CreateModalStatus, Error>('modalStatus', status || null);
  205. return {
  206. ...swrResponse,
  207. open: (path?: string) => swrResponse.mutate({ isOpened: true, path }),
  208. close: () => swrResponse.mutate({ isOpened: false }),
  209. };
  210. };
  211. export const useCreateModalOpened = (): SWRResponse<boolean, Error> => {
  212. const { data } = useCreateModalStatus();
  213. return useSWR(
  214. data != null ? ['isModalOpened', data] : null,
  215. () => {
  216. return data != null ? data.isOpened : false;
  217. },
  218. );
  219. };
  220. export const useCreateModalPath = (): SWRResponse<string, Error> => {
  221. const { data: currentPagePath } = useCurrentPagePath();
  222. const { data: status } = useCreateModalStatus();
  223. return useSWR(
  224. [currentPagePath, status],
  225. (currentPagePath, status) => {
  226. return status.path || currentPagePath;
  227. },
  228. );
  229. };
  230. export const useSelectedGrant = (initialData?: Nullable<number>): SWRResponse<Nullable<number>, Error> => {
  231. return useStaticSWR<Nullable<number>, Error>('grant', initialData ?? null);
  232. };
  233. export const useSelectedGrantGroupId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
  234. return useStaticSWR<Nullable<string>, Error>('grantGroupId', initialData ?? null);
  235. };
  236. export const useSelectedGrantGroupName = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
  237. return useStaticSWR<Nullable<string>, Error>('grantGroupName', initialData ?? null);
  238. };
  239. export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
  240. return useStaticSWR('globalSearchTypeahead', initialData ?? null);
  241. };