in packages/unitables/src/bee/UnitablesBeeTable.tsx [341:829]
function UnitablesBeeTableCell({
joinedName,
rowCount,
columnCount,
}: BeeTableCellProps<ROWTYPE> & {
joinedName: string;
rowCount: number;
columnCount: number;
}) {
const [{ field, onChange: onFieldChange, name: fieldName }] = useField(joinedName, {});
const cellRef = useRef<HTMLDivElement | null>(null);
const [autoFieldKey, forceUpdate] = useReducer((x) => x + 1, 0);
const { containerCellCoordinates } = useBeeTableCoordinates();
const { isBeeTableChange } = useUnitablesContext();
const { submitRow, rowInputs } = useUnitablesRow(containerCellCoordinates?.rowIndex ?? 0);
const fieldInput = useMemo(() => getObjectValueByPath(rowInputs, fieldName), [rowInputs, fieldName]);
const [isSelectFieldOpen, setIsSelectFieldOpen] = useState(false);
const fieldCharacteristics = useMemo(() => {
if (!field) {
return;
}
return {
xDmnType: field["x-dmn-type"] as X_DMN_TYPE,
isEnum: !!field.enum,
isList: field.type === "array",
};
}, [field]);
const previousFieldInput = useRef(fieldInput);
// keep previous updated;
useEffect(() => {
previousFieldInput.current = fieldInput;
}, [fieldInput]);
// FIXME: Decouple from DMN --> https://github.com/apache/incubator-kie-issues/issues/166
const setValue = useCallback(
(newValue?: string) => {
isBeeTableChange.current = true;
const newValueWithoutSymbols = newValue?.replace(/\r/g, "") ?? "";
if (field.enum) {
if (field.enum.findIndex((value: unknown) => value === newValueWithoutSymbols) >= 0) {
onFieldChange(newValueWithoutSymbols);
} else {
onFieldChange(field.placeholder);
}
// Changing the values using onChange will not re-render <select> nodes;
// This ensure a re-render of the SelectField;
forceUpdate();
} else if (field.type === "string") {
if (field.format === "time") {
if (moment(newValueWithoutSymbols, [moment.HTML5_FMT.TIME, moment.HTML5_FMT.TIME_SECONDS], true).isValid()) {
onFieldChange(newValueWithoutSymbols);
} else {
onFieldChange("");
}
} else if (field.format === "date") {
if (moment(newValueWithoutSymbols, [moment.HTML5_FMT.DATE]).isValid()) {
onFieldChange(newValueWithoutSymbols);
} else {
onFieldChange("");
}
} else if (field.format === "date-time") {
const valueAsNumber = Date.parse(newValueWithoutSymbols);
if (!isNaN(valueAsNumber)) {
onFieldChange(newValueWithoutSymbols);
} else {
onFieldChange("");
}
} else {
onFieldChange(newValueWithoutSymbols);
}
} else if (field.type === "number") {
const numberValue = parseFloat(newValueWithoutSymbols);
onFieldChange(isNaN(numberValue) ? undefined : numberValue);
} else if (field.type === "boolean") {
onFieldChange(newValueWithoutSymbols === "true");
} else if (field.type === "array") {
// FIXME: Support lists --> https://github.com/apache/incubator-kie-issues/issues/167
} else if (field.type === "object" && typeof newValue !== "object") {
// objects are flattened in a single row - this case shouldn't happen;
} else {
onFieldChange(newValue);
}
submitRow();
},
[isBeeTableChange, field.enum, field.type, field.placeholder, field.format, submitRow, onFieldChange]
);
const { isActive, isEditing } = useBeeTableSelectableCellRef(
containerCellCoordinates?.rowIndex ?? 0,
containerCellCoordinates?.columnIndex ?? 0,
setValue,
useCallback(() => `${fieldInput ?? ""}`, [fieldInput])
);
const { mutateSelection } = useBeeTableSelectionDispatch();
const navigateVertically = useCallback(
(args: { isShiftPressed: boolean }) => {
mutateSelection({
part: SelectionPart.ActiveCell,
columnCount: () => columnCount,
rowCount,
deltaColumns: 0,
deltaRows: args.isShiftPressed ? -1 : 1,
isEditingActiveCell: false,
keepInsideSelection: true,
});
},
[mutateSelection, rowCount, columnCount]
);
const setEditingCell = useCallback(
(isEditing: boolean) => {
mutateSelection({
part: SelectionPart.ActiveCell,
columnCount: () => columnCount,
rowCount,
deltaColumns: 0,
deltaRows: 0,
isEditingActiveCell: isEditing,
keepInsideSelection: true,
});
},
[mutateSelection, rowCount, columnCount]
);
const onKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
// TAB
if (e.key.toLowerCase() === "tab") {
// ListField - START
if (fieldCharacteristics?.isList) {
// Get all uniforms components inside the ListField;
const uniformsComponents = cellRef.current?.querySelectorAll('[id^="uniforms-"]');
if (uniformsComponents === undefined) {
setEditingCell(false);
return;
}
const uniformComponentTargetIndex = Array.from(uniformsComponents ?? []).findIndex(
(component) => component.id === (e.target as HTMLElement).id
);
// If it wasn't possible to retrieve the index, it should focus on the first button;
if (uniformComponentTargetIndex < 0) {
(uniformsComponents?.item(1) as HTMLElement).parentElement?.focus();
setEditingCell(true);
e.stopPropagation();
return;
}
const nextUniformsComponent = e.shiftKey
? uniformsComponents[uniformComponentTargetIndex - 1]
: uniformsComponents[uniformComponentTargetIndex + 1];
// Should leave ListField if nextUniformsComponent doesn't exist
if (nextUniformsComponent === undefined) {
setEditingCell(false);
return;
}
// TextField, BoolField, DateTimeField, NumField, ListAddField, ListDelField
if (
nextUniformsComponent.tagName.toLowerCase() === "input" ||
nextUniformsComponent.tagName.toLowerCase() === "button"
) {
(nextUniformsComponent as HTMLButtonElement | HTMLInputElement).parentElement?.focus();
setEditingCell(true);
submitRow();
e.stopPropagation();
return;
}
// Nested ListFields or SelectField
if (nextUniformsComponent.tagName.toLowerCase() === "div") {
(nextUniformsComponent as HTMLElement)?.focus();
setEditingCell(true);
submitRow();
e.stopPropagation();
return;
}
} // ListField - END
submitRow();
setEditingCell(false);
if (fieldCharacteristics?.isEnum) {
setIsSelectFieldOpen((prev) => {
if (prev) {
cellRef.current?.getElementsByTagName("button")?.[0]?.click();
}
return false;
});
}
return;
}
// ESC
if (e.key.toLowerCase() === "escape") {
e.stopPropagation();
onFieldChange(previousFieldInput.current);
cellRef.current?.focus();
setEditingCell(false);
if (fieldCharacteristics?.isEnum) {
setIsSelectFieldOpen((prev) => {
if (prev) {
cellRef.current?.getElementsByTagName("button")?.[0]?.click();
}
return false;
});
}
return;
}
// ENTER
if (e.key.toLowerCase() === "enter") {
// ListField - START
if (fieldCharacteristics?.isList) {
e.stopPropagation();
e.preventDefault();
const uniformsComponents = cellRef.current?.querySelectorAll('[id^="uniforms-"]');
if (!uniformsComponents) {
return;
}
// To search the uniforms components avoiding returning the top ListField
// we search backwards;
const reversedUniformsComponents = Array.from(uniformsComponents).reverse();
const reversedUniformComponentTargetIndex = reversedUniformsComponents.findIndex((component) =>
component.contains(e.target as HTMLElement)
);
const uniformsComponent = reversedUniformsComponents[reversedUniformComponentTargetIndex];
// If field is selected, and the target is not present
if (!uniformsComponent) {
// check if it's a button from a SelectField
const selectFieldUl = document.querySelectorAll(`ul[name^="${fieldName}."]`)?.item(0);
if (selectFieldUl && selectFieldUl.contains(e.target as HTMLElement)) {
setIsSelectFieldOpen(false);
submitRow();
(cellRef.current?.querySelector(`[id=${selectFieldUl.id}]`) as HTMLDivElement)
?.getElementsByTagName("button")
?.item(0)
?.focus();
} else if (uniformsComponents[1].tagName.toLowerCase() === "button") {
(uniformsComponents[1] as HTMLButtonElement)?.focus();
}
} else {
// A button is the ListAddField or ListDelField
if (uniformsComponent.tagName.toLowerCase() === "button") {
(uniformsComponent as HTMLButtonElement)?.click();
// The ListField ListDelField is the last element
if (reversedUniformComponentTargetIndex === 0) {
// focus on ListField parent element;
if (uniformsComponents[1].tagName.toLowerCase() === "button") {
(uniformsComponents[1] as HTMLButtonElement)?.focus();
}
}
submitRow();
}
// SelectField
if (uniformsComponent.tagName.toLowerCase() === "div") {
setIsSelectFieldOpen(true);
}
}
return;
} // ListField - END
e.stopPropagation();
if (fieldCharacteristics?.isEnum) {
cellRef.current?.getElementsByTagName("button")?.[0]?.click();
setIsSelectFieldOpen((prev) => {
if (prev === true) {
submitRow();
}
return !prev;
});
setEditingCell(!isEditing);
return;
}
if (!isEditing) {
const inputField = cellRef.current?.getElementsByTagName("input");
if (inputField && inputField.length > 0) {
inputField?.[0]?.focus();
setEditingCell(true);
return;
}
}
submitRow();
setEditingCell(false);
navigateVertically({ isShiftPressed: e.shiftKey });
return;
}
// Normal editing;
if (isEditModeTriggeringKey(e)) {
e.stopPropagation();
setEditingCell(true);
// If the target is an input node it is already editing the cell;
if (
!fieldCharacteristics?.isList &&
!isEditing &&
(e.target as HTMLInputElement).tagName.toLowerCase() !== "input"
) {
// handle checkbox field;
const inputField = cellRef.current?.getElementsByTagName("input")?.[0];
if (e.code.toLowerCase() === "space" && fieldCharacteristics?.xDmnType === X_DMN_TYPE.BOOLEAN) {
inputField?.click();
submitRow();
return;
}
inputField?.select();
}
if (fieldCharacteristics?.isList) {
if (
e.code.toLowerCase() === "space" &&
(e.target as HTMLElement)?.tagName?.toLowerCase() === "input" &&
(e.target as HTMLInputElement)?.type === "checkbox"
) {
e.preventDefault();
(e.target as HTMLInputElement).click();
submitRow();
return;
}
if (e.code.toLowerCase() === "space" && (e.target as HTMLElement)?.tagName?.toLowerCase() === "button") {
e.preventDefault();
if ((e.target as HTMLButtonElement).id.match(/^uniforms-/g)) {
return;
}
setIsSelectFieldOpen(true);
return;
}
}
}
if (isEditing) {
e.stopPropagation();
}
},
[isEditing, submitRow, setEditingCell, fieldCharacteristics, onFieldChange, navigateVertically, fieldName]
);
// if it's active should focus on cell;
useEffect(() => {
if (!isActive) {
return;
}
if (fieldCharacteristics?.isList) {
if (isSelectFieldOpen) {
setTimeout(() => {
document.querySelectorAll(`ul[name^="${fieldName}."]`)?.[0]?.getElementsByTagName("button")?.item(0)?.focus();
}, 0);
}
} else if (fieldCharacteristics?.isEnum) {
if (isSelectFieldOpen) {
// if a SelectField is open, it takes a time to render the select options;
// After the select options are rendered we focus in the selected option;
setTimeout(() => {
const selectOptions = document.getElementsByName(fieldName)?.[0]?.getElementsByTagName("button");
Array.from(selectOptions ?? [])
?.filter((selectOption) => selectOption.innerText === cellRef.current?.innerText)?.[0]
?.focus();
}, 0);
} else {
cellRef.current?.focus();
}
}
if (!isEditing) {
cellRef.current?.focus();
}
}, [fieldName, isActive, isEditing, fieldCharacteristics?.isList, fieldCharacteristics?.isEnum, isSelectFieldOpen]);
const onBlur = useCallback(
(e: React.FocusEvent<HTMLDivElement>) => {
if (fieldCharacteristics?.isList) {
if (
e.target.tagName.toLowerCase() === "button" &&
(e.relatedTarget as HTMLElement)?.tagName.toLowerCase() === "button"
) {
// if the select field is open and it blurs to another cell, close it;
const selectFieldUl = document.querySelectorAll(`ul[name^="${fieldName}."]`).item(0);
// if relatedTarget aka button is not in the SelectField UL, it should close the SelectField
if (selectFieldUl && !selectFieldUl?.contains(e.relatedTarget as HTMLButtonElement)) {
(cellRef.current?.querySelector(`[id="${selectFieldUl?.id}"]`) as HTMLDivElement)?.click();
setIsSelectFieldOpen(false);
}
}
submitRow();
return;
}
if (e.target.tagName.toLowerCase() === "div") {
if ((e.target.getElementsByTagName("input")?.length ?? 0) > 0) {
submitRow();
}
}
if (e.target.tagName.toLowerCase() === "input") {
submitRow();
}
if (
e.target.tagName.toLowerCase() === "button" ||
(e.relatedTarget as HTMLElement)?.tagName.toLowerCase() === "button"
) {
// if the select field is open and it blurs to another cell, close it;
const selectOptions = document.getElementsByName(fieldName)?.[0]?.getElementsByTagName("button");
if ((selectOptions?.length ?? 0) > 0 && (e.relatedTarget as HTMLElement)?.tagName?.toLowerCase() === "td") {
e.target.click();
setIsSelectFieldOpen(false);
}
submitRow();
}
},
[fieldName, fieldCharacteristics?.isList, submitRow]
);
const onClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
// The "enter" key triggers the onClick if the button is inside a form
if (e.detail === 0) {
return;
}
// ListField
if (
e.isTrusted &&
fieldCharacteristics?.isList &&
((e.target as HTMLElement).tagName.toLowerCase() === "path" ||
(e.target as HTMLElement).tagName.toLowerCase() === "svg" ||
(e.target as HTMLElement).tagName.toLowerCase() === "button")
) {
// if the select field is open and it blurs to another cell, close it;
const selectField = document.querySelectorAll(`ul[name^="${fieldName}."]`).item(0);
if (selectField?.contains(e.target as HTMLButtonElement)) {
setIsSelectFieldOpen((prev) => !prev);
}
submitRow();
return;
}
// SelectField
if (e.isTrusted && (e.target as HTMLElement).tagName.toLowerCase() === "button") {
setIsSelectFieldOpen((prev) => {
if (prev === true) {
submitRow();
}
return !prev;
});
setEditingCell(!isEditing);
}
if (!isEditing && e.isTrusted && (e.target as HTMLElement).tagName.toLowerCase() === "input") {
const inputField = cellRef.current?.getElementsByTagName("input");
if (inputField && inputField.length > 0) {
inputField?.[0]?.focus();
setEditingCell(true);
return;
}
}
},
[fieldName, isEditing, fieldCharacteristics?.isList, setEditingCell, submitRow]
);
return (
<div
style={{ outline: "none" }}
tabIndex={-1}
ref={cellRef}
onKeyDown={onKeyDown}
onBlur={onBlur}
onClick={onClick}
>
<AutoField
key={joinedName + autoFieldKey}
name={joinedName}
form={`${AUTO_ROW_ID}-${containerCellCoordinates?.rowIndex ?? 0}`}
style={{ height: "60px" }}
/>
</div>
);
}