|
|
@@ -1,6 +1,12 @@
|
|
|
-import React, { useMemo, memo } from 'react';
|
|
|
+import React, {
|
|
|
+ useMemo, memo, useState, useCallback, useRef, useEffect,
|
|
|
+} from 'react';
|
|
|
|
|
|
+import { pathUtils } from '@growi/core/dist/utils';
|
|
|
+import { isCreatablePagePathPattern } from '../../../../utils/is-creatable-page-path-pattern';
|
|
|
+import { useRect } from '@growi/ui/dist/utils';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
+import AutosizeInput from 'react-input-autosize';
|
|
|
|
|
|
import { type SelectablePage } from '../../../../interfaces/selectable-page';
|
|
|
|
|
|
@@ -11,8 +17,7 @@ const moduleClass = styles['selectable-page-page-list'] ?? '';
|
|
|
type MethodButtonProps = {
|
|
|
page: SelectablePage;
|
|
|
disablePagePaths: string[];
|
|
|
- methodButtonColor: string;
|
|
|
- methodButtonIconName: string;
|
|
|
+ method: 'add' | 'remove' | 'delete'
|
|
|
onClickMethodButton: (page: SelectablePage) => void;
|
|
|
}
|
|
|
|
|
|
@@ -20,15 +25,40 @@ const MethodButton = memo((props: MethodButtonProps) => {
|
|
|
const {
|
|
|
page,
|
|
|
disablePagePaths,
|
|
|
- methodButtonColor,
|
|
|
- methodButtonIconName,
|
|
|
+ method,
|
|
|
onClickMethodButton,
|
|
|
} = props;
|
|
|
|
|
|
+ const iconName = useMemo(() => {
|
|
|
+ switch (method) {
|
|
|
+ case 'add':
|
|
|
+ return 'add_circle';
|
|
|
+ case 'remove':
|
|
|
+ return 'do_not_disturb_on';
|
|
|
+ case 'delete':
|
|
|
+ return 'delete';
|
|
|
+ default:
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }, [method]);
|
|
|
+
|
|
|
+ const color = useMemo(() => {
|
|
|
+ switch (method) {
|
|
|
+ case 'add':
|
|
|
+ return 'text-primary';
|
|
|
+ case 'remove':
|
|
|
+ return 'text-secondary';
|
|
|
+ case 'delete':
|
|
|
+ return 'text-secondary';
|
|
|
+ default:
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }, [method]);
|
|
|
+
|
|
|
return (
|
|
|
<button
|
|
|
type="button"
|
|
|
- className={`btn border-0 ${methodButtonColor}`}
|
|
|
+ className={`btn border-0 ${color}`}
|
|
|
disabled={disablePagePaths.includes(page.path)}
|
|
|
onClick={(e) => {
|
|
|
e.stopPropagation();
|
|
|
@@ -36,18 +66,113 @@ const MethodButton = memo((props: MethodButtonProps) => {
|
|
|
}}
|
|
|
>
|
|
|
<span className="material-symbols-outlined">
|
|
|
- {methodButtonIconName}
|
|
|
+ {iconName}
|
|
|
</span>
|
|
|
</button>
|
|
|
);
|
|
|
});
|
|
|
|
|
|
|
|
|
+type EditablePagePathProps = {
|
|
|
+ isEditable?: boolean;
|
|
|
+ page: SelectablePage;
|
|
|
+ disablePagePaths: string[];
|
|
|
+ methodButtonPosition?: 'left' | 'right';
|
|
|
+}
|
|
|
+
|
|
|
+const EditablePagePath = memo((props: EditablePagePathProps): JSX.Element => {
|
|
|
+ const {
|
|
|
+ page,
|
|
|
+ isEditable,
|
|
|
+ disablePagePaths = [],
|
|
|
+ methodButtonPosition = 'left',
|
|
|
+ } = props;
|
|
|
+
|
|
|
+ const [editingPagePath, setEditingPagePath] = useState<string | null>(null);
|
|
|
+ const [inputValue, setInputValue] = useState('');
|
|
|
+
|
|
|
+ const inputRef = useRef<HTMLInputElement & AutosizeInput | null>(null);
|
|
|
+ const editingContainerRef = useRef<HTMLDivElement>(null);
|
|
|
+ const [editingContainerRect] = useRect(editingContainerRef);
|
|
|
+
|
|
|
+ const isEditing = isEditable && editingPagePath === page.path;
|
|
|
+
|
|
|
+ const handlePagePathClick = useCallback((page: SelectablePage) => {
|
|
|
+ if (!isEditable || disablePagePaths.includes(page.path)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setEditingPagePath(page.path);
|
|
|
+ setInputValue(page.path);
|
|
|
+ }, [disablePagePaths, isEditable]);
|
|
|
+
|
|
|
+ const handleInputBlur = useCallback(() => {
|
|
|
+ setEditingPagePath(null);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleInputKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+
|
|
|
+ // Validate page path
|
|
|
+ const pagePathWithSlash = pathUtils.addHeadingSlash(inputValue);
|
|
|
+ if (inputValue === '' || disablePagePaths.includes(pagePathWithSlash) || !isCreatablePagePathPattern(pagePathWithSlash)) {
|
|
|
+ handleInputBlur();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update page path
|
|
|
+ page.path = pagePathWithSlash;
|
|
|
+
|
|
|
+ handleInputBlur();
|
|
|
+ }
|
|
|
+ }, [disablePagePaths, handleInputBlur, inputValue, page]);
|
|
|
+
|
|
|
+ // Autofocus
|
|
|
+ useEffect(() => {
|
|
|
+ if (editingPagePath != null && inputRef.current != null) {
|
|
|
+ inputRef.current.focus();
|
|
|
+ }
|
|
|
+ }, [editingPagePath]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ ref={editingContainerRef}
|
|
|
+ className={`flex-grow-1 ${methodButtonPosition === 'left' ? 'me-2' : 'mx-2'}`}
|
|
|
+ style={{ minWidth: 0 }}
|
|
|
+ >
|
|
|
+ {isEditing
|
|
|
+ ? (
|
|
|
+ <AutosizeInput
|
|
|
+ id="page-path-input"
|
|
|
+ inputClassName="page-path-input"
|
|
|
+ type="text"
|
|
|
+ ref={inputRef}
|
|
|
+ value={inputValue}
|
|
|
+ onBlur={handleInputBlur}
|
|
|
+ onChange={e => setInputValue(e.target.value)}
|
|
|
+ onKeyDown={handleInputKeyDown}
|
|
|
+ inputStyle={{ maxWidth: (editingContainerRect?.width ?? 0) - 10 }}
|
|
|
+ />
|
|
|
+ )
|
|
|
+ : (
|
|
|
+ <span
|
|
|
+ className={`page-path ${isEditable && !disablePagePaths.includes(page.path) ? 'page-path-editable' : ''}`}
|
|
|
+ onClick={() => handlePagePathClick(page)}
|
|
|
+ title={page.path}
|
|
|
+ >
|
|
|
+ {page.path}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+
|
|
|
type SelectablePagePageListProps = {
|
|
|
pages: SelectablePage[],
|
|
|
method: 'add' | 'remove' | 'delete'
|
|
|
methodButtonPosition?: 'left' | 'right',
|
|
|
disablePagePaths?: string[],
|
|
|
+ isEditable?: boolean,
|
|
|
onClickMethodButton: (page: SelectablePage) => void,
|
|
|
}
|
|
|
|
|
|
@@ -57,38 +182,12 @@ export const SelectablePagePageList = (props: SelectablePagePageListProps): JSX.
|
|
|
method,
|
|
|
methodButtonPosition = 'left',
|
|
|
disablePagePaths = [],
|
|
|
+ isEditable,
|
|
|
onClickMethodButton,
|
|
|
} = props;
|
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
- const methodButtonIconName = useMemo(() => {
|
|
|
- switch (method) {
|
|
|
- case 'add':
|
|
|
- return 'add_circle';
|
|
|
- case 'remove':
|
|
|
- return 'do_not_disturb_on';
|
|
|
- case 'delete':
|
|
|
- return 'delete';
|
|
|
- default:
|
|
|
- return '';
|
|
|
- }
|
|
|
- }, [method]);
|
|
|
-
|
|
|
- const methodButtonColor = useMemo(() => {
|
|
|
- switch (method) {
|
|
|
- case 'add':
|
|
|
- return 'text-primary';
|
|
|
- case 'remove':
|
|
|
- return 'text-secondary';
|
|
|
- case 'delete':
|
|
|
- return 'text-secondary';
|
|
|
- default:
|
|
|
- return '';
|
|
|
- }
|
|
|
- }, [method]);
|
|
|
-
|
|
|
-
|
|
|
if (pages.length === 0) {
|
|
|
return (
|
|
|
<div className={moduleClass}>
|
|
|
@@ -103,32 +202,28 @@ export const SelectablePagePageList = (props: SelectablePagePageListProps): JSX.
|
|
|
<div className={`list-group ${moduleClass}`}>
|
|
|
{pages.map((page) => {
|
|
|
return (
|
|
|
- <button
|
|
|
+ <div
|
|
|
key={page.path}
|
|
|
- type="button"
|
|
|
- className="list-group-item border-0 list-group-item-action page-list-item d-flex align-items-center p-1 mb-2 rounded"
|
|
|
- onClick={(e) => {
|
|
|
- e.stopPropagation();
|
|
|
- }}
|
|
|
+ className="list-group-item border-0 page-list-item d-flex align-items-center p-1 mb-2 rounded"
|
|
|
>
|
|
|
|
|
|
{methodButtonPosition === 'left'
|
|
|
&& (
|
|
|
<MethodButton
|
|
|
page={page}
|
|
|
+ method={method}
|
|
|
disablePagePaths={disablePagePaths}
|
|
|
- methodButtonColor={methodButtonColor}
|
|
|
- methodButtonIconName={methodButtonIconName}
|
|
|
onClickMethodButton={onClickMethodButton}
|
|
|
/>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
- <div className={`flex-grow-1 ${methodButtonPosition === 'left' ? 'me-4' : 'ms-2'}`}>
|
|
|
- <span>
|
|
|
- {page.path}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
+ <EditablePagePath
|
|
|
+ page={page}
|
|
|
+ isEditable={isEditable}
|
|
|
+ disablePagePaths={disablePagePaths}
|
|
|
+ methodButtonPosition={methodButtonPosition}
|
|
|
+ />
|
|
|
|
|
|
<span className={`badge bg-body-secondary rounded-pill ${methodButtonPosition === 'left' ? 'me-2' : ''}`}>
|
|
|
<span className="text-body-tertiary">
|
|
|
@@ -140,14 +235,13 @@ export const SelectablePagePageList = (props: SelectablePagePageListProps): JSX.
|
|
|
&& (
|
|
|
<MethodButton
|
|
|
page={page}
|
|
|
+ method={method}
|
|
|
disablePagePaths={disablePagePaths}
|
|
|
- methodButtonColor={methodButtonColor}
|
|
|
- methodButtonIconName={methodButtonIconName}
|
|
|
onClickMethodButton={onClickMethodButton}
|
|
|
/>
|
|
|
)
|
|
|
}
|
|
|
- </button>
|
|
|
+ </div>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|