import { RefObject } from 'react'; import { useSWRConfig, SWRResponse, Key, Fetcher, } from 'swr'; import useSWRImmutable from 'swr/immutable'; import SimpleBar from 'simplebar-react'; import { Breakpoint, addBreakpointListener } from '@growi/ui'; import { pagePathUtils } from '@growi/core'; import { SidebarContentsType } from '~/interfaces/ui'; import loggerFactory from '~/utils/logger'; import { useStaticSWR } from './use-static-swr'; import { useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage, useIsNotCreatable, useIsSharedUser, useNotFoundTargetPathOrId, useIsForbidden, useIsIdenticalPath, useIsNotFoundPermalink, } from './context'; import { IFocusable } from '~/client/interfaces/focusable'; import { Nullable } from '~/interfaces/common'; import { UpdateDescCountData } from '~/interfaces/websocket'; const { isSharedPage } = pagePathUtils; const logger = loggerFactory('growi:stores:ui'); const isServer = typeof window === 'undefined'; /** ********************************************************** * Unions *********************************************************** */ export const EditorMode = { View: 'view', Editor: 'editor', HackMD: 'hackmd', } as const; export type EditorMode = typeof EditorMode[keyof typeof EditorMode]; /** ********************************************************** * Storing RefObjects *********************************************************** */ export const useSidebarScrollerRef = (initialData?: RefObject): SWRResponse, Error> => { return useStaticSWR, Error>('sidebarScrollerRef', initialData); }; /** ********************************************************** * SWR Hooks * for switching UI *********************************************************** */ export const useIsMobile = (): SWRResponse => { const key = isServer ? null : 'isMobile'; let configuration; if (!isServer) { const userAgent = window.navigator.userAgent.toLowerCase(); configuration = { fallbackData: /iphone|ipad|android/.test(userAgent), }; } return useStaticSWR(key, undefined, configuration); }; const updateBodyClassesByEditorMode = (newEditorMode: EditorMode) => { switch (newEditorMode) { case EditorMode.View: $('body').removeClass('on-edit'); $('body').removeClass('builtin-editor'); $('body').removeClass('hackmd'); $('body').removeClass('pathname-sidebar'); break; case EditorMode.Editor: $('body').addClass('on-edit'); $('body').addClass('builtin-editor'); $('body').removeClass('hackmd'); // editing /Sidebar if (window.location.pathname === '/Sidebar') { $('body').addClass('pathname-sidebar'); } break; case EditorMode.HackMD: $('body').addClass('on-edit'); $('body').addClass('hackmd'); $('body').removeClass('builtin-editor'); $('body').removeClass('pathname-sidebar'); break; } }; const updateHashByEditorMode = (newEditorMode: EditorMode) => { const { pathname } = window.location; switch (newEditorMode) { case EditorMode.View: window.history.replaceState(null, '', pathname); break; case EditorMode.Editor: window.history.replaceState(null, '', `${pathname}#edit`); break; case EditorMode.HackMD: window.history.replaceState(null, '', `${pathname}#hackmd`); break; } }; export const determineEditorModeByHash = (): EditorMode => { const { hash } = window.location; switch (hash) { case '#edit': return EditorMode.Editor; case '#hackmd': return EditorMode.HackMD; default: return EditorMode.View; } }; let isEditorModeLoaded = false; export const useEditorMode = (): SWRResponse => { const { data: _isEditable } = useIsEditable(); const editorModeByHash = determineEditorModeByHash(); const isLoading = _isEditable === undefined; const isEditable = !isLoading && _isEditable; const initialData = isEditable ? editorModeByHash : EditorMode.View; const swrResponse = useSWRImmutable( isLoading ? null : ['editorMode', isEditable], null, { fallbackData: initialData }, ); // initial updating if (!isEditorModeLoaded && !isLoading && swrResponse.data != null) { if (isEditable) { updateBodyClassesByEditorMode(swrResponse.data); } isEditorModeLoaded = true; } return { ...swrResponse, // overwrite mutate mutate: (editorMode: EditorMode, shouldRevalidate?: boolean) => { if (!isEditable) { return Promise.resolve(EditorMode.View); // fixed if not editable } updateBodyClassesByEditorMode(editorMode); updateHashByEditorMode(editorMode); return swrResponse.mutate(editorMode, shouldRevalidate); }, }; }; export const useIsDeviceSmallerThanMd = (): SWRResponse => { const key: Key = isServer ? null : 'isDeviceSmallerThanMd'; const { cache, mutate } = useSWRConfig(); if (!isServer) { const mdOrAvobeHandler = function(this: MediaQueryList): void { // sm -> md: matches will be true // md -> sm: matches will be false mutate(key, !this.matches); }; const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler); // initialize if (cache.get(key) == null) { document.addEventListener('DOMContentLoaded', () => { mutate(key, !mql.matches); }); } } return useStaticSWR(key); }; export const useIsDeviceSmallerThanLg = (): SWRResponse => { const key: Key = isServer ? null : 'isDeviceSmallerThanLg'; const { cache, mutate } = useSWRConfig(); if (!isServer) { const lgOrAvobeHandler = function(this: MediaQueryList): void { // md -> lg: matches will be true // lg -> md: matches will be false mutate(key, !this.matches); }; const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler); // initialize if (cache.get(key) == null) { document.addEventListener('DOMContentLoaded', () => { mutate(key, !mql.matches); }); } } return useStaticSWR(key); }; export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponse => { return useStaticSWR('preferDrawerModeByUser', initialData, { fallbackData: false }); }; export const usePreferDrawerModeOnEditByUser = (initialData?: boolean): SWRResponse => { return useStaticSWR('preferDrawerModeOnEditByUser', initialData, { fallbackData: true }); }; export const useSidebarCollapsed = (initialData?: boolean): SWRResponse => { return useStaticSWR('isSidebarCollapsed', initialData, { fallbackData: false }); }; export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse => { return useStaticSWR('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE }); }; export const useCurrentProductNavWidth = (initialData?: number): SWRResponse => { return useStaticSWR('productNavWidth', initialData, { fallbackData: 320 }); }; export const useDrawerMode = (): SWRResponse => { const { data: editorMode } = useEditorMode(); const { data: preferDrawerModeByUser } = usePreferDrawerModeByUser(); const { data: preferDrawerModeOnEditByUser } = usePreferDrawerModeOnEditByUser(); const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd(); const condition = editorMode != null || preferDrawerModeByUser != null || preferDrawerModeOnEditByUser != null || isDeviceSmallerThanMd != null; const calcDrawerMode: Fetcher = ( key: Key, editorMode: EditorMode, preferDrawerModeByUser: boolean, preferDrawerModeOnEditByUser: boolean, isDeviceSmallerThanMd: boolean, ): boolean => { // get preference on view or edit const preferDrawerMode = editorMode !== EditorMode.View ? preferDrawerModeOnEditByUser : preferDrawerModeByUser; return isDeviceSmallerThanMd || preferDrawerMode; }; return useSWRImmutable( condition ? ['isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null, calcDrawerMode, { fallback: calcDrawerMode, }, ); }; export const useDrawerOpened = (isOpened?: boolean): SWRResponse => { return useStaticSWR('isDrawerOpened', isOpened, { fallbackData: false }); }; export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse => { return useStaticSWR('isSidebarResizeDisabled', isDisabled, { fallbackData: false }); }; export const useSelectedGrant = (initialData?: Nullable): SWRResponse, Error> => { return useStaticSWR, Error>('grant', initialData); }; export const useSelectedGrantGroupId = (initialData?: Nullable): SWRResponse, Error> => { return useStaticSWR, Error>('grantGroupId', initialData); }; export const useSelectedGrantGroupName = (initialData?: Nullable): SWRResponse, Error> => { return useStaticSWR, Error>('grantGroupName', initialData); }; export const useGlobalSearchFormRef = (initialData?: RefObject): SWRResponse, Error> => { return useStaticSWR('globalSearchTypeahead', initialData); }; export const useIsAbleToShowPageManagement = (): SWRResponse => { const key = 'isAbleToShowPageManagement'; const { data: currentPageId } = useCurrentPageId(); const { data: isTrashPage } = useIsTrashPage(); const { data: isSharedUser } = useIsSharedUser(); const includesUndefined = [currentPageId, isTrashPage, isSharedUser].some(v => v === undefined); const isPageExist = currentPageId != null; return useSWRImmutable( includesUndefined ? null : key, () => isPageExist && !isTrashPage && !isSharedUser, ); }; export const useIsAbleToShowTagLabel = (): SWRResponse => { const key = 'isAbleToShowTagLabel'; const { data: isUserPage } = useIsUserPage(); const { data: currentPagePath } = useCurrentPagePath(); const { data: isIdenticalPath } = useIsIdenticalPath(); const { data: notFoundTargetPathOrId } = useNotFoundTargetPathOrId(); const { data: editorMode } = useEditorMode(); const includesUndefined = [isUserPage, currentPagePath, isIdenticalPath, notFoundTargetPathOrId, editorMode].some(v => v === undefined); const isViewMode = editorMode === EditorMode.View; const isNotFoundPage = notFoundTargetPathOrId != null; return useSWRImmutable( includesUndefined ? null : [key, editorMode], // eslint-disable-next-line @typescript-eslint/no-non-null-assertion () => !isUserPage && !isSharedPage(currentPagePath!) && !isIdenticalPath && !(isViewMode && isNotFoundPage), ); }; export const useIsAbleToShowPageEditorModeManager = (): SWRResponse => { const key = 'isAbleToShowPageEditorModeManager'; const { data: isNotCreatable } = useIsNotCreatable(); const { data: isForbidden } = useIsForbidden(); const { data: isTrashPage } = useIsTrashPage(); const { data: isSharedUser } = useIsSharedUser(); const { data: isNotFoundPermalink } = useIsNotFoundPermalink(); const includesUndefined = [isNotCreatable, isForbidden, isTrashPage, isSharedUser, isNotFoundPermalink].some(v => v === undefined); return useSWRImmutable( includesUndefined ? null : key, () => !isNotCreatable && !isForbidden && !isTrashPage && !isSharedUser && !isNotFoundPermalink, ); }; export const useIsAbleToShowPageAuthors = (): SWRResponse => { const key = 'isAbleToShowPageAuthors'; const { data: currentPageId } = useCurrentPageId(); const { data: isUserPage } = useIsUserPage(); const includesUndefined = [currentPageId, isUserPage].some(v => v === undefined); const isPageExist = currentPageId != null; return useSWRImmutable( includesUndefined ? null : key, () => isPageExist && !isUserPage, ); }; type PageTreeDescCountMapUtils = { update(newData?: UpdateDescCountData): Promise getDescCount(pageId?: string): number | null | undefined } export const usePageTreeDescCountMap = (initialData?: UpdateDescCountData): SWRResponse & PageTreeDescCountMapUtils => { const key = 'pageTreeDescCountMap'; const swrResponse = useStaticSWR(key, initialData, { fallbackData: new Map() }); return { ...swrResponse, getDescCount: (pageId?: string) => (pageId != null ? swrResponse.data?.get(pageId) : null), update: (newData: UpdateDescCountData) => swrResponse.mutate(new Map([...(swrResponse.data || new Map()), ...newData])), }; };