/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import * as Monaco from "@kie-tools-core/monaco-editor"; import { FeelInput, FeelInputRef } from "@kie-tools/feel-input-component"; import * as React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { NavigationKeysUtils } from "../../keysUtils/keyUtils"; import { useBoxedExpressionEditor } from "../../BoxedExpressionEditorContext"; import "./BeeTableEditableCellContent.css"; import { getOperatingSystem, OperatingSystem } from "@kie-tools-core/operating-system"; import { useBoxedExpressionEditorI18n } from "../../i18n"; const CELL_LINE_HEIGHT = 20; const MONACO_OPTIONS: Monaco.editor.IStandaloneEditorConstructionOptions = { fixedOverflowWidgets: true, lineNumbers: "off", fontSize: 13, renderLineHighlight: "none", lineDecorationsWidth: 1, automaticLayout: true, "semanticHighlighting.enabled": true, }; enum Mode { Read, Edit, } export interface BeeTableEditableCellContentProps { value: string; onChange: (value: string) => void; isReadOnly: boolean; isEditing: boolean; isActive: boolean; setEditing: React.Dispatch>; onFeelTabKeyDown?: (args: { isShiftPressed: boolean }) => void; onFeelEnterKeyDown?: (args: { isShiftPressed: boolean }) => void; expressionId?: string; } export function BeeTableEditableCellContent({ value, onChange, isReadOnly, isEditing, isActive, setEditing, onFeelTabKeyDown, onFeelEnterKeyDown, expressionId, }: BeeTableEditableCellContentProps) { const [cellHeight, setCellHeight] = useState(CELL_LINE_HEIGHT * 3); const [preview, setPreview] = useState(value); const [previousValue, setPreviousValue] = useState(value); const [editingValue, setEditingValue] = useState(value); const feelInputRef = useRef(null); const { locale } = useBoxedExpressionEditorI18n(); const mode = useMemo(() => { return isEditing && !isReadOnly ? Mode.Edit : Mode.Read; }, [isEditing, isReadOnly]); // FIXME: Tiago --> Temporary fix for the Boxed Expression Editor to work well. Ideally this wouldn't bee here, as the BeeTable should be decoupled from the DMN Editor's Boxed Expression Editor use-case. const { onRequestFeelIdentifiers } = useBoxedExpressionEditor(); const feelIdentifiers = useMemo(() => { if (mode === Mode.Edit) { return onRequestFeelIdentifiers?.(); } else { return undefined; } }, [mode, onRequestFeelIdentifiers]); useEffect(() => { setPreviousValue((prev) => (isEditing ? prev : value)); setEditingValue((prev) => (isEditing ? prev : value)); }, [isEditing, value]); const updateValue = useCallback( (newValue: string) => { if (value !== newValue) { onChange(newValue); setCellHeight(calculateCellHeight(newValue)); } }, [value, onChange] ); const onFeelBlur = useCallback( (valueOnBlur: string) => { updateValue(valueOnBlur); setEditing(false); }, [setEditing, updateValue] ); const onFeelKeyDown = useCallback( (e: Monaco.IKeyboardEvent, newValue: string) => { const eventKey = e?.code ?? ""; if (NavigationKeysUtils.isTab(eventKey)) { if (feelInputRef.current?.isSuggestionWidgetOpen()) { // Do nothing. } else { updateValue(newValue); setEditing(false); onFeelTabKeyDown?.({ isShiftPressed: e.shiftKey }); e.preventDefault(); } } if (NavigationKeysUtils.isEnter(eventKey)) { if (e.ctrlKey || e.metaKey || e.altKey) { feelInputRef.current?.insertNewLineToMonaco(); } else if (feelInputRef.current?.isSuggestionWidgetOpen()) { // Do nothing; } else { // This line below is commented on because it causes an issue with WebKit (Safari) based browsers, // making the text boxes no longer work. Also, it is not necessary because the newValue is saved // in the onBlur event, called by the BeeTableSelectionContext // updateValue(newValue); setEditing(false); onFeelEnterKeyDown?.({ isShiftPressed: e.shiftKey }); } } if (NavigationKeysUtils.isEsc(eventKey)) { if (feelInputRef.current?.isSuggestionWidgetOpen()) { // Do nothing. } else { // We need to restore the content on Monaco, because when // we disable it, it changes the cell content with the // last value it had. feelInputRef.current?.setMonacoValue(previousValue); updateValue(previousValue); setEditing(false); } } }, [updateValue, setEditing, onFeelTabKeyDown, onFeelEnterKeyDown, previousValue] ); useEffect(() => { setCellHeight(calculateCellHeight(value)); }, [value]); const onFeelChange = useCallback( (_e: Monaco.editor.IModelContentChangedEvent, newValue: string, newPreview: string) => { setCellHeight(calculateCellHeight(newValue)); setPreview(newPreview); }, [] ); const editableCellRef = useRef(null); useEffect(() => { if (isActive && !isEditing) { const cellElement = editableCellRef.current; if (!cellElement) { return; } // Find the boxed-expression-provider container (top-level container for this component) const activeElement = document.activeElement; const boxedExpressionProvider = cellElement.closest(".boxed-expression-provider"); // Don't steal focus if the active element is outside this boxed-expression-provider. // This prevents stealing focus from input fields in other tables/components when the table re-renders. if (activeElement && boxedExpressionProvider && !boxedExpressionProvider.contains(activeElement)) { return; } cellElement.focus(); } }, [isActive, isEditing]); const cssClass = useMemo(() => { return `editable-cell ${mode === Mode.Edit ? "editable-cell--edit-mode" : "editable-cell--read-mode"}`; }, [mode]); const onKeyDown = useCallback( (e) => { // When inside FEEL Input, all keyboard events should be kept inside it. // Exceptions to this strategy are handled on `onFeelKeyDown`. // NOTE: In macOS, we can not stopPropagation here because, otherwise, shortcuts are not handled // See https://github.com/apache/incubator-kie-issues/issues/1164 if (isEditing && !(getOperatingSystem() === OperatingSystem.MACOS && e.metaKey)) { e.stopPropagation(); } // This is used to start editing a cell without being in edit mode. if (!isReadOnly && isActive && !isEditing && isEditModeTriggeringKey(e)) { setEditingValue(""); setEditing(true); } }, [isActive, isEditing, isReadOnly, setEditing] ); return ( <>
{value}
); } function isEditModeTriggeringKey(e: React.KeyboardEvent) { if (e.altKey || e.ctrlKey || e.metaKey) { return false; } return /^[\s\S]$/.test(e.key); } function calculateCellHeight(value: string) { const numberOfValueLines = `${value}`.split("\n").length; const numberOfLines = numberOfValueLines <= 2 ? 2 : numberOfValueLines; // Always add one extra line at the end to compensate for the scrollbar. return (numberOfLines + 1) * CELL_LINE_HEIGHT; }