Add ability to edit tab name (#67)

This commit is contained in:
Red J Adaya 2024-06-22 01:23:04 +08:00 committed by GitHub
parent 2c6f6d917f
commit 9cc5d9d3ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 16 deletions

View File

@ -12,7 +12,7 @@ class BlockServiceType {
} }
// send command to block // send command to block
SendCommand(blockid: string, cmd: BlockCommand): Promise<void> { SendCommand(cmd: string, arg3: BlockCommand): Promise<void> {
return WOS.callBackendService("block", "SendCommand", Array.from(arguments)) return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
} }
} }
@ -109,6 +109,11 @@ class ObjectServiceType {
return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments)) return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments))
} }
// @returns object updates
UpdateTabName(tabId: string, name: string): Promise<void> {
return WOS.callBackendService("object", "UpdateTabName", Array.from(arguments))
}
// @returns object updates // @returns object updates
UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise<void> { UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
return WOS.callBackendService("object", "UpdateWorkspaceTabIds", Array.from(arguments)) return WOS.callBackendService("object", "UpdateWorkspaceTabIds", Array.from(arguments))

View File

@ -32,6 +32,13 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
-webkit-user-select: none; -webkit-user-select: none;
z-index: 3; z-index: 3;
&.focused {
outline: none;
border: 1px solid var(--border-color);
padding: 2px 6px;
border-radius: 2px;
}
} }
.vertical-line { .vertical-line {
@ -95,7 +102,7 @@
rgba(0, 0, 0, 0.8) 60%, rgba(0, 0, 0, 0.8) 60%,
rgba(0, 0, 0, 0.7) 100% rgba(0, 0, 0, 0.7) 100%
); );
pointer-events: none; /* Prevents the background from capturing mouse events */ pointer-events: none;
} }
} }

View File

@ -1,10 +1,8 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Button } from "@/element/button"; import { Button } from "@/element/button";
import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import { clsx } from "clsx"; import { clsx } from "clsx";
import React, { useEffect, useRef } from "react"; import { forwardRef, useEffect, useRef, useState } from "react";
import "./tab.less"; import "./tab.less";
@ -14,18 +12,84 @@ interface TabProps {
isBeforeActive: boolean; isBeforeActive: boolean;
isDragging: boolean; isDragging: boolean;
onSelect: () => void; onSelect: () => void;
onClose: () => void; onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onDragStart: (event: MouseEvent) => void; onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onLoaded: () => void; onLoaded: () => void;
} }
const Tab = React.forwardRef<HTMLDivElement, TabProps>( const Tab = forwardRef<HTMLDivElement, TabProps>(
({ id, active, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => { ({ id, active, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id)); const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
const name = tabData?.name ?? "..."; const [originalName, setOriginalName] = useState("");
const [isEditable, setIsEditable] = useState(false);
const editableRef = useRef<HTMLDivElement>(null);
const editableTimeoutRef = useRef<NodeJS.Timeout>();
const loadedRef = useRef(false); const loadedRef = useRef(false);
useEffect(() => {
if (tabData?.name) {
setOriginalName(tabData.name);
}
}, [tabData]);
useEffect(() => {
return () => {
if (editableTimeoutRef.current) {
clearTimeout(editableTimeoutRef.current);
}
};
}, []);
const handleDoubleClick = (event) => {
event.stopPropagation();
setIsEditable(true);
editableTimeoutRef.current = setTimeout(() => {
if (editableRef.current) {
editableRef.current.focus();
document.execCommand("selectAll", false);
}
}, 0);
};
const handleBlur = () => {
let newText = editableRef.current.innerText.trim();
newText = newText || originalName;
editableRef.current.innerText = newText;
setIsEditable(false);
services.ObjectService.UpdateTabName(id, newText);
};
const handleKeyDown = (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
event.preventDefault();
if (editableRef.current) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(editableRef.current);
selection.removeAllRanges();
selection.addRange(range);
}
return;
}
if (event.key === "Enter") {
event.preventDefault();
if (editableRef.current.innerText.trim() === "") {
editableRef.current.innerText = originalName;
}
editableRef.current.blur();
} else if (event.key === "Escape") {
editableRef.current.innerText = originalName;
editableRef.current.blur();
} else if (
editableRef.current.innerText.length >= 8 &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(event.key)
) {
event.preventDefault();
}
};
useEffect(() => { useEffect(() => {
if (!loadedRef.current) { if (!loadedRef.current) {
onLoaded(); onLoaded();
@ -33,6 +97,11 @@ const Tab = React.forwardRef<HTMLDivElement, TabProps>(
} }
}, [onLoaded]); }, [onLoaded]);
// Prevent drag from being triggered on mousedown
const handleMouseDownOnClose = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
};
return ( return (
<div <div
ref={ref} ref={ref}
@ -41,10 +110,20 @@ const Tab = React.forwardRef<HTMLDivElement, TabProps>(
onClick={onSelect} onClick={onSelect}
data-tab-id={id} data-tab-id={id}
> >
<div className="name">{name}</div> <div
ref={editableRef}
className={clsx("name", { focused: isEditable })}
contentEditable={isEditable}
onDoubleClick={handleDoubleClick}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
suppressContentEditableWarning={true}
>
{tabData?.name}
</div>
{!isDragging && <div className="vertical-line" />} {!isDragging && <div className="vertical-line" />}
{active && <div className="mask" />} {active && <div className="mask" />}
<Button className="secondary ghost close" onClick={onClose}> <Button className="secondary ghost close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
<i className="fa fa-solid fa-xmark" /> <i className="fa fa-solid fa-xmark" />
</Button> </Button>
</div> </div>

View File

@ -425,7 +425,8 @@ const TabBar = ({ workspace }: TabBarProps) => {
}, 30); }, 30);
}; };
const handleCloseTab = (tabId: string) => { const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, tabId: string) => {
event.stopPropagation();
services.WindowService.CloseTab(tabId); services.WindowService.CloseTab(tabId);
deleteLayoutStateAtomForTab(tabId); deleteLayoutStateAtomForTab(tabId);
}; };
@ -458,7 +459,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
onSelect={() => handleSelectTab(tabId)} onSelect={() => handleSelectTab(tabId)}
active={activetabid === tabId} active={activetabid === tabId}
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])} onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
onClose={() => handleCloseTab(tabId)} onClose={(event) => handleCloseTab(event, tabId)}
onLoaded={() => handleTabLoaded(tabId)} onLoaded={() => handleTabLoaded(tabId)}
isBeforeActive={isBeforeActive(tabId)} isBeforeActive={isBeforeActive(tabId)}
isDragging={draggingTab === tabId} isDragging={draggingTab === tabId}

View File

@ -31,7 +31,7 @@ declare global {
type BlockCommand = { type BlockCommand = {
command: string; command: string;
} & ( ResolveIdsCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockInputCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | CreateBlockCommand ); } & ( CreateBlockCommand | BlockInputCommand | BlockAppendFileCommand | ResolveIdsCommand | BlockMessageCommand | BlockAppendIJsonCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand );
// wstore.BlockDef // wstore.BlockDef
type BlockDef = { type BlockDef = {
@ -49,6 +49,7 @@ declare global {
// wshutil.BlockInputCommand // wshutil.BlockInputCommand
type BlockInputCommand = { type BlockInputCommand = {
blockid: string;
command: "controller:input"; command: "controller:input";
inputdata64?: string; inputdata64?: string;
signame?: string; signame?: string;
@ -91,6 +92,7 @@ declare global {
// wshutil.CreateBlockCommand // wshutil.CreateBlockCommand
type CreateBlockCommand = { type CreateBlockCommand = {
command: "createblock"; command: "createblock";
tabid: string;
blockdef: BlockDef; blockdef: BlockDef;
rtopts?: RuntimeOpts; rtopts?: RuntimeOpts;
}; };
@ -325,4 +327,4 @@ declare global {
} }
export {} export {}

View File

@ -155,6 +155,23 @@ func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string)
return updates, nil return updates, nil
} }
func (svc *ObjectService) UpdateTabName_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{
ArgNames: []string{"uiContext", "tabId", "name"},
}
}
func (svc *ObjectService) UpdateTabName(uiContext wstore.UIContext, tabId, name string) (wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx)
err := wstore.UpdateTabName(ctx, tabId, name)
if err != nil {
return nil, fmt.Errorf("error updating tab name: %w", err)
}
return wstore.ContextGetUpdatesRtn(ctx), nil
}
func (svc *ObjectService) CreateBlock_Meta() tsgenmeta.MethodMeta { func (svc *ObjectService) CreateBlock_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{ return tsgenmeta.MethodMeta{
ArgNames: []string{"uiContext", "blockDef", "rtOpts"}, ArgNames: []string{"uiContext", "blockDef", "rtOpts"},

View File

@ -211,6 +211,20 @@ func SetActiveTab(ctx context.Context, windowId string, tabId string) error {
}) })
} }
func UpdateTabName(ctx context.Context, tabId, name string) error {
return WithTx(ctx, func(tx *TxWrap) error {
tab, _ := DBGet[*Tab](tx.Context(), tabId)
if tab == nil {
return fmt.Errorf("tab not found: %q", tabId)
}
if tabId != "" {
tab.Name = name
DBUpdate(tx.Context(), tab)
}
return nil
})
}
func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts *RuntimeOpts) (*Block, error) { func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts *RuntimeOpts) (*Block, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) {
tab, _ := DBGet[*Tab](tx.Context(), tabId) tab, _ := DBGet[*Tab](tx.Context(), tabId)