ui.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. import useSWR, {
  2. useSWRConfig, SWRResponse, Key, Fetcher, mutate,
  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 {
  11. useCurrentPagePath, useIsEditable, useIsPageExist, useIsTrashPage, useIsUserPage,
  12. useIsNotCreatable, useIsSharedUser, useNotFoundTargetPathOrId, useIsForbidden, useIsIdenticalPath,
  13. } from './context';
  14. import { IFocusable } from '~/client/interfaces/focusable';
  15. import { isSharedPage } from '^/../core/src/utils/page-path-utils';
  16. const logger = loggerFactory('growi:stores:ui');
  17. const isServer = typeof window === 'undefined';
  18. type Nullable<T> = T | null;
  19. /** **********************************************************
  20. * Unions
  21. *********************************************************** */
  22. export const EditorMode = {
  23. View: 'view',
  24. Editor: 'editor',
  25. HackMD: 'hackmd',
  26. } as const;
  27. export type EditorMode = typeof EditorMode[keyof typeof EditorMode];
  28. /** **********************************************************
  29. * SWR Hooks
  30. * for switching UI
  31. *********************************************************** */
  32. export const useIsMobile = (): SWRResponse<boolean, Error> => {
  33. const key = isServer ? null : 'isMobile';
  34. let configuration;
  35. if (!isServer) {
  36. const userAgent = window.navigator.userAgent.toLowerCase();
  37. configuration = {
  38. fallbackData: /iphone|ipad|android/.test(userAgent),
  39. };
  40. }
  41. return useStaticSWR<boolean, Error>(key, undefined, configuration);
  42. };
  43. const updateBodyClassesByEditorMode = (newEditorMode: EditorMode) => {
  44. switch (newEditorMode) {
  45. case EditorMode.View:
  46. $('body').removeClass('on-edit');
  47. $('body').removeClass('builtin-editor');
  48. $('body').removeClass('hackmd');
  49. $('body').removeClass('pathname-sidebar');
  50. break;
  51. case EditorMode.Editor:
  52. $('body').addClass('on-edit');
  53. $('body').addClass('builtin-editor');
  54. $('body').removeClass('hackmd');
  55. // editing /Sidebar
  56. if (window.location.pathname === '/Sidebar') {
  57. $('body').addClass('pathname-sidebar');
  58. }
  59. break;
  60. case EditorMode.HackMD:
  61. $('body').addClass('on-edit');
  62. $('body').addClass('hackmd');
  63. $('body').removeClass('builtin-editor');
  64. $('body').removeClass('pathname-sidebar');
  65. break;
  66. }
  67. };
  68. const updateHashByEditorMode = (newEditorMode: EditorMode) => {
  69. const { pathname } = window.location;
  70. switch (newEditorMode) {
  71. case EditorMode.View:
  72. window.history.replaceState(null, '', pathname);
  73. break;
  74. case EditorMode.Editor:
  75. window.history.replaceState(null, '', `${pathname}#edit`);
  76. break;
  77. case EditorMode.HackMD:
  78. window.history.replaceState(null, '', `${pathname}#hackmd`);
  79. break;
  80. }
  81. };
  82. export const determineEditorModeByHash = (): EditorMode => {
  83. const { hash } = window.location;
  84. switch (hash) {
  85. case '#edit':
  86. return EditorMode.Editor;
  87. case '#hackmd':
  88. return EditorMode.HackMD;
  89. default:
  90. return EditorMode.View;
  91. }
  92. };
  93. let isEditorModeLoaded = false;
  94. export const useEditorMode = (): SWRResponse<EditorMode, Error> => {
  95. const { data: _isEditable } = useIsEditable();
  96. const editorModeByHash = determineEditorModeByHash();
  97. const isLoading = _isEditable === undefined;
  98. const isEditable = !isLoading && _isEditable;
  99. const initialData = isEditable ? editorModeByHash : EditorMode.View;
  100. const swrResponse = useSWRImmutable(
  101. isLoading ? null : ['editorMode', isEditable],
  102. null,
  103. { fallbackData: initialData },
  104. );
  105. // initial updating
  106. if (!isEditorModeLoaded && !isLoading && swrResponse.data != null) {
  107. if (isEditable) {
  108. updateBodyClassesByEditorMode(swrResponse.data);
  109. }
  110. isEditorModeLoaded = true;
  111. }
  112. return {
  113. ...swrResponse,
  114. // overwrite mutate
  115. mutate: (editorMode: EditorMode, shouldRevalidate?: boolean) => {
  116. if (!isEditable) {
  117. return Promise.resolve(EditorMode.View); // fixed if not editable
  118. }
  119. updateBodyClassesByEditorMode(editorMode);
  120. updateHashByEditorMode(editorMode);
  121. return swrResponse.mutate(editorMode, shouldRevalidate);
  122. },
  123. };
  124. };
  125. export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
  126. const key: Key = isServer ? null : 'isDeviceSmallerThanMd';
  127. const { cache, mutate } = useSWRConfig();
  128. if (!isServer) {
  129. const mdOrAvobeHandler = function(this: MediaQueryList): void {
  130. // sm -> md: matches will be true
  131. // md -> sm: matches will be false
  132. mutate(key, !this.matches);
  133. };
  134. const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
  135. // initialize
  136. if (cache.get(key) == null) {
  137. document.addEventListener('DOMContentLoaded', () => {
  138. mutate(key, !mql.matches);
  139. });
  140. }
  141. }
  142. return useStaticSWR(key);
  143. };
  144. export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
  145. const key: Key = isServer ? null : 'isDeviceSmallerThanLg';
  146. const { cache, mutate } = useSWRConfig();
  147. if (!isServer) {
  148. const lgOrAvobeHandler = function(this: MediaQueryList): void {
  149. // md -> lg: matches will be true
  150. // lg -> md: matches will be false
  151. mutate(key, !this.matches);
  152. };
  153. const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
  154. // initialize
  155. if (cache.get(key) == null) {
  156. document.addEventListener('DOMContentLoaded', () => {
  157. mutate(key, !mql.matches);
  158. });
  159. }
  160. }
  161. return useStaticSWR(key);
  162. };
  163. export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
  164. return useStaticSWR('preferDrawerModeByUser', initialData, { fallbackData: false });
  165. };
  166. export const usePreferDrawerModeOnEditByUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
  167. return useStaticSWR('preferDrawerModeOnEditByUser', initialData, { fallbackData: true });
  168. };
  169. export const useSidebarCollapsed = (initialData?: boolean): SWRResponse<boolean, Error> => {
  170. return useStaticSWR('isSidebarCollapsed', initialData, { fallbackData: false });
  171. };
  172. export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
  173. return useStaticSWR('sidebarContents', initialData, { fallbackData: SidebarContentsType.RECENT });
  174. };
  175. export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
  176. return useStaticSWR('productNavWidth', initialData, { fallbackData: 320 });
  177. };
  178. export const useDrawerMode = (): SWRResponse<boolean, Error> => {
  179. const { data: editorMode } = useEditorMode();
  180. const { data: preferDrawerModeByUser } = usePreferDrawerModeByUser();
  181. const { data: preferDrawerModeOnEditByUser } = usePreferDrawerModeOnEditByUser();
  182. const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
  183. const condition = editorMode != null || preferDrawerModeByUser != null || preferDrawerModeOnEditByUser != null || isDeviceSmallerThanMd != null;
  184. const calcDrawerMode: Fetcher<boolean> = (
  185. key: Key, editorMode: EditorMode, preferDrawerModeByUser: boolean, preferDrawerModeOnEditByUser: boolean, isDeviceSmallerThanMd: boolean,
  186. ): boolean => {
  187. // get preference on view or edit
  188. const preferDrawerMode = editorMode !== EditorMode.View ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
  189. return isDeviceSmallerThanMd || preferDrawerMode;
  190. };
  191. return useSWRImmutable(
  192. condition ? ['isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
  193. calcDrawerMode,
  194. {
  195. fallback: calcDrawerMode,
  196. },
  197. );
  198. };
  199. export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
  200. return useStaticSWR('isDrawerOpened', isOpened, { fallbackData: false });
  201. };
  202. export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<boolean, Error> => {
  203. return useStaticSWR('isSidebarResizeDisabled', isDisabled, { fallbackData: false });
  204. };
  205. type CreateModalStatus = {
  206. isOpened: boolean,
  207. path?: string,
  208. }
  209. type CreateModalStatusUtils = {
  210. open(path?: string): Promise<CreateModalStatus | undefined>
  211. close(): Promise<CreateModalStatus | undefined>
  212. }
  213. export const useCreateModalStatus = (status?: CreateModalStatus): SWRResponse<CreateModalStatus, Error> & CreateModalStatusUtils => {
  214. const initialData: CreateModalStatus = { isOpened: false };
  215. const swrResponse = useStaticSWR<CreateModalStatus, Error>('modalStatus', status, { fallbackData: initialData });
  216. return {
  217. ...swrResponse,
  218. open: (path?: string) => swrResponse.mutate({ isOpened: true, path }),
  219. close: () => swrResponse.mutate({ isOpened: false }),
  220. };
  221. };
  222. export const useCreateModalOpened = (): SWRResponse<boolean, Error> => {
  223. const { data } = useCreateModalStatus();
  224. return useSWR(
  225. data != null ? ['isModalOpened', data] : null,
  226. () => {
  227. return data != null ? data.isOpened : false;
  228. },
  229. );
  230. };
  231. export const useCreateModalPath = (): SWRResponse<string | null | undefined, Error> => {
  232. const { data: currentPagePath } = useCurrentPagePath();
  233. const { data: status } = useCreateModalStatus();
  234. return useSWR(
  235. currentPagePath != null && status != null ? [currentPagePath, status] : null,
  236. (currentPagePath, status) => {
  237. return status?.path || currentPagePath;
  238. },
  239. );
  240. };
  241. export const useSelectedGrant = (initialData?: Nullable<number>): SWRResponse<Nullable<number>, Error> => {
  242. return useStaticSWR<Nullable<number>, Error>('grant', initialData);
  243. };
  244. export const useSelectedGrantGroupId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
  245. return useStaticSWR<Nullable<string>, Error>('grantGroupId', initialData);
  246. };
  247. export const useSelectedGrantGroupName = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
  248. return useStaticSWR<Nullable<string>, Error>('grantGroupName', initialData);
  249. };
  250. export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
  251. return useStaticSWR('globalSearchTypeahead', initialData);
  252. };
  253. export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> => {
  254. const key = 'isAbleToShowPageManagement';
  255. const { data: isPageExist } = useIsPageExist();
  256. const { data: isTrashPage } = useIsTrashPage();
  257. const { data: isSharedUser } = useIsSharedUser();
  258. const includesUndefined = [isPageExist, isTrashPage, isSharedUser].some(v => v === undefined);
  259. return useSWRImmutable(
  260. includesUndefined ? null : key,
  261. () => isPageExist! && !isTrashPage && !isSharedUser,
  262. );
  263. };
  264. export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
  265. const key = 'isAbleToShowTagLabel';
  266. const { data: isUserPage } = useIsUserPage();
  267. const { data: currentPagePath } = useCurrentPagePath();
  268. const { data: isIdenticalPath } = useIsIdenticalPath();
  269. const { data: notFoundTargetPathOrId } = useNotFoundTargetPathOrId();
  270. const { data: editorMode } = useEditorMode();
  271. const includesUndefined = [isUserPage, currentPagePath, isIdenticalPath, notFoundTargetPathOrId, editorMode].some(v => v === undefined);
  272. const isViewMode = editorMode === EditorMode.View;
  273. const isNotFoundPage = notFoundTargetPathOrId != null;
  274. return useSWRImmutable(
  275. includesUndefined ? null : key,
  276. () => !isUserPage && !isSharedPage(currentPagePath!) && !isIdenticalPath && !(isViewMode && isNotFoundPage),
  277. );
  278. };
  279. export const useIsAbleToShowPageEditorModeManager = (): SWRResponse<boolean, Error> => {
  280. const key = 'isAbleToShowPageEditorModeManager';
  281. const { data: isNotCreatable } = useIsNotCreatable();
  282. const { data: isForbidden } = useIsForbidden();
  283. const { data: isTrashPage } = useIsTrashPage();
  284. const { data: isSharedUser } = useIsSharedUser();
  285. const includesUndefined = [isNotCreatable, isForbidden, isTrashPage, isSharedUser].some(v => v === undefined);
  286. return useSWRImmutable(
  287. includesUndefined ? null : key,
  288. () => !isNotCreatable && !isForbidden && !isTrashPage && !isSharedUser);
  289. };
  290. export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
  291. const key = 'isAbleToShowPageAuthors';
  292. const { data: isPageExist } = useIsPageExist();
  293. const { data: isUserPage } = useIsUserPage();
  294. const includesUndefined = [isPageExist, isUserPage].some(v => v === undefined);
  295. return useSWRImmutable(
  296. includesUndefined ? null : key,
  297. () => isPageExist! && !isUserPage,
  298. );
  299. };