mirror of
https://github.com/docmost/docmost.git
synced 2026-08-30 18:36:26 +08:00
fix(base): place caret at end instead of selecting all when editing cells
This commit is contained in:
@@ -76,6 +76,7 @@ export const GridCell = memo(function GridCell({
|
|||||||
|
|
||||||
const tapStartRef = useRef<{ x: number; y: number } | null>(null);
|
const tapStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
const suppressClickRef = useRef(false);
|
const suppressClickRef = useRef(false);
|
||||||
|
const expandClickGuardRef = useRef(false);
|
||||||
|
|
||||||
const handleDoubleClick = useCallback(() => {
|
const handleDoubleClick = useCallback(() => {
|
||||||
if (!property || isRowNumber) return;
|
if (!property || isRowNumber) return;
|
||||||
@@ -121,6 +122,7 @@ export const GridCell = memo(function GridCell({
|
|||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
expandClickGuardRef.current = false;
|
||||||
if (e.pointerType === "mouse") return;
|
if (e.pointerType === "mouse") return;
|
||||||
suppressClickRef.current = false;
|
suppressClickRef.current = false;
|
||||||
tapStartRef.current = { x: e.clientX, y: e.clientY };
|
tapStartRef.current = { x: e.clientX, y: e.clientY };
|
||||||
@@ -140,11 +142,18 @@ export const GridCell = memo(function GridCell({
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((e.target as HTMLElement).closest("button, a, input")) return;
|
const target = e.target as HTMLElement;
|
||||||
|
if (onExpandRow && target.closest("[data-base-row-expand]")) {
|
||||||
|
suppressClickRef.current = true;
|
||||||
|
expandClickGuardRef.current = true;
|
||||||
|
onExpandRow(rowId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.closest("button, a, input")) return;
|
||||||
suppressClickRef.current = true;
|
suppressClickRef.current = true;
|
||||||
handleDoubleClick();
|
handleDoubleClick();
|
||||||
},
|
},
|
||||||
[handleDoubleClick],
|
[handleDoubleClick, onExpandRow, rowId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerCancel = useCallback(() => {
|
const handlePointerCancel = useCallback(() => {
|
||||||
@@ -262,8 +271,15 @@ export const GridCell = memo(function GridCell({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
data-base-row-expand=""
|
||||||
className={classes.rowExpandButton}
|
className={classes.rowExpandButton}
|
||||||
onClick={() => onExpandRow(rowId)}
|
onClick={() => {
|
||||||
|
if (expandClickGuardRef.current) {
|
||||||
|
expandClickGuardRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onExpandRow(rowId);
|
||||||
|
}}
|
||||||
onDoubleClick={(e) => e.stopPropagation()}
|
onDoubleClick={(e) => e.stopPropagation()}
|
||||||
aria-label={t("Expand row {{number}}", { number: rowIndex + 1 })}
|
aria-label={t("Expand row {{number}}", { number: rowIndex + 1 })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ export function CreatePropertyPopover({ pageId, properties, onPropertyCreated, r
|
|||||||
const [dropdownNode, setDropdownNode] = useState<HTMLDivElement | null>(null);
|
const [dropdownNode, setDropdownNode] = useState<HTMLDivElement | null>(null);
|
||||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [position, setPosition] = useState<"bottom-start" | "top-start">(
|
||||||
|
"bottom-start",
|
||||||
|
);
|
||||||
|
|
||||||
const createPropertyMutation = useCreatePropertyMutation();
|
const createPropertyMutation = useCreatePropertyMutation();
|
||||||
|
|
||||||
@@ -95,10 +98,20 @@ export function CreatePropertyPopover({ pageId, properties, onPropertyCreated, r
|
|||||||
setTypeOptions({});
|
setTypeOptions({});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOpen = useCallback(() => {
|
const handleOpen = useCallback(
|
||||||
resetState();
|
(event?: React.SyntheticEvent) => {
|
||||||
setOpened(true);
|
resetState();
|
||||||
}, [resetState]);
|
const trigger = event?.currentTarget as HTMLElement | undefined;
|
||||||
|
if (trigger) {
|
||||||
|
const rect = trigger.getBoundingClientRect();
|
||||||
|
const spaceAbove = rect.top;
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
|
setPosition(spaceAbove > spaceBelow ? "top-start" : "bottom-start");
|
||||||
|
}
|
||||||
|
setOpened(true);
|
||||||
|
},
|
||||||
|
[resetState],
|
||||||
|
);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
// Don't reset state here: resetting mid-close flashes the type picker.
|
// Don't reset state here: resetting mid-close flashes the type picker.
|
||||||
@@ -217,13 +230,13 @@ export function CreatePropertyPopover({ pageId, properties, onPropertyCreated, r
|
|||||||
onChange={(o) => {
|
onChange={(o) => {
|
||||||
if (!o) attemptClose();
|
if (!o) attemptClose();
|
||||||
}}
|
}}
|
||||||
position="bottom-start"
|
position={position}
|
||||||
shadow="md"
|
shadow="md"
|
||||||
closeOnClickOutside
|
closeOnClickOutside
|
||||||
closeOnEscape={false}
|
closeOnEscape={false}
|
||||||
withinPortal
|
withinPortal
|
||||||
middlewares={{
|
middlewares={{
|
||||||
flip: true,
|
flip: false,
|
||||||
shift: true,
|
shift: true,
|
||||||
size: {
|
size: {
|
||||||
padding: 8,
|
padding: 8,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
IBaseProperty,
|
IBaseProperty,
|
||||||
BasePropertyType,
|
BasePropertyType,
|
||||||
|
TypeOptions,
|
||||||
|
SelectTypeOptions,
|
||||||
} from "@/ee/base/types/base.types";
|
} from "@/ee/base/types/base.types";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { propertyMenuCloseRequestAtomFamily } from "@/ee/base/atoms/base-atoms";
|
import { propertyMenuCloseRequestAtomFamily } from "@/ee/base/atoms/base-atoms";
|
||||||
@@ -36,7 +38,11 @@ import {
|
|||||||
NON_USER_TARGET_TYPES,
|
NON_USER_TARGET_TYPES,
|
||||||
} from "./conversion-warning";
|
} from "./conversion-warning";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { isSystemPropertyType, propertyTypes } from "@/ee/base/property-types/property-type.registry";
|
import {
|
||||||
|
isSystemPropertyType,
|
||||||
|
propertyTypes,
|
||||||
|
defaultTypeOptionsFor,
|
||||||
|
} from "@/ee/base/property-types/property-type.registry";
|
||||||
import cellClasses from "@/ee/base/styles/cells.module.css";
|
import cellClasses from "@/ee/base/styles/cells.module.css";
|
||||||
import classes from "@/ee/base/styles/property.module.css";
|
import classes from "@/ee/base/styles/property.module.css";
|
||||||
|
|
||||||
@@ -58,6 +64,31 @@ type MenuPanel =
|
|||||||
| "confirmDelete"
|
| "confirmDelete"
|
||||||
| "confirmDiscard";
|
| "confirmDiscard";
|
||||||
|
|
||||||
|
const CHOICE_TYPES = new Set<BasePropertyType>([
|
||||||
|
"select",
|
||||||
|
"multiSelect",
|
||||||
|
"status",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function typeOptionsForConversion(
|
||||||
|
source: IBaseProperty,
|
||||||
|
target: BasePropertyType,
|
||||||
|
): TypeOptions {
|
||||||
|
if (!CHOICE_TYPES.has(source.type) || !CHOICE_TYPES.has(target)) {
|
||||||
|
return defaultTypeOptionsFor(target);
|
||||||
|
}
|
||||||
|
const opts = source.typeOptions as SelectTypeOptions | undefined;
|
||||||
|
const choices = opts?.choices ?? [];
|
||||||
|
const choiceOrder = opts?.choiceOrder?.length
|
||||||
|
? opts.choiceOrder
|
||||||
|
: choices.map((c) => c.id);
|
||||||
|
const carried: SelectTypeOptions = { choices, choiceOrder };
|
||||||
|
if (target === "status") {
|
||||||
|
carried.defaultValue = choices[0]?.id ?? null;
|
||||||
|
}
|
||||||
|
return carried;
|
||||||
|
}
|
||||||
|
|
||||||
export function PropertyMenuContent({
|
export function PropertyMenuContent({
|
||||||
property,
|
property,
|
||||||
opened,
|
opened,
|
||||||
@@ -185,16 +216,10 @@ export function PropertyMenuContent({
|
|||||||
propertyId: property.id,
|
propertyId: property.id,
|
||||||
pageId: property.pageId,
|
pageId: property.pageId,
|
||||||
type: pendingTargetType,
|
type: pendingTargetType,
|
||||||
typeOptions: {},
|
typeOptions: typeOptionsForConversion(property, pendingTargetType),
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
}, [
|
}, [pendingTargetType, property, updatePropertyMutation, onClose]);
|
||||||
pendingTargetType,
|
|
||||||
property.id,
|
|
||||||
property.pageId,
|
|
||||||
updatePropertyMutation,
|
|
||||||
onClose,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
deletePropertyMutation.mutate({
|
deletePropertyMutation.mutate({
|
||||||
|
|||||||
@@ -63,8 +63,12 @@ export function useEditableTextCell({
|
|||||||
} else {
|
} else {
|
||||||
setDraft(toDraftRef.current(value));
|
setDraft(toDraftRef.current(value));
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
inputRef.current?.focus();
|
const el = inputRef.current;
|
||||||
inputRef.current?.select();
|
if (el) {
|
||||||
|
el.focus();
|
||||||
|
const len = el.value.length;
|
||||||
|
el.setSelectionRange(len, len);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user