deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
320
packages/notebook/src/browser/view/notebook-cell-list-view.tsx
Normal file
320
packages/notebook/src/browser/view/notebook-cell-list-view.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { CellEditType, CellKind, NotebookCellsChangeType } from '../../common';
|
||||
import { NotebookCellModel } from '../view-model/notebook-cell-model';
|
||||
import { NotebookModel } from '../view-model/notebook-model';
|
||||
import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory';
|
||||
import { animationFrame, onDomEvent } from '@theia/core/lib/browser';
|
||||
import { CommandMenu, CommandRegistry, DisposableCollection, MenuModelRegistry, nls } from '@theia/core';
|
||||
import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution';
|
||||
import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution';
|
||||
import { NotebookContextManager } from '../service/notebook-context-manager';
|
||||
import { NotebookViewModel } from '../view-model/notebook-view-model';
|
||||
|
||||
export interface CellRenderer {
|
||||
render(notebookData: NotebookModel, cell: NotebookCellModel, index: number): React.ReactNode
|
||||
renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode
|
||||
renderDragImage(cell: NotebookCellModel): HTMLElement
|
||||
}
|
||||
|
||||
export function observeCellHeight(ref: HTMLDivElement | null, cell: NotebookCellModel): void {
|
||||
if (ref) {
|
||||
cell.cellHeight = ref?.getBoundingClientRect().height ?? 0;
|
||||
new ResizeObserver(entries =>
|
||||
cell.cellHeight = ref?.getBoundingClientRect().height ?? 0
|
||||
).observe(ref);
|
||||
}
|
||||
}
|
||||
|
||||
interface CellListProps {
|
||||
renderers: Map<CellKind, CellRenderer>;
|
||||
notebookModel: NotebookModel;
|
||||
notebookViewModel: NotebookViewModel;
|
||||
notebookContext: NotebookContextManager;
|
||||
toolbarRenderer: NotebookCellToolbarFactory;
|
||||
commandRegistry: CommandRegistry;
|
||||
menuRegistry: MenuModelRegistry;
|
||||
}
|
||||
|
||||
interface NotebookCellListState {
|
||||
selectedCell?: NotebookCellModel;
|
||||
scrollIntoView: boolean;
|
||||
dragOverIndicator: { cell: NotebookCellModel, position: 'top' | 'bottom' } | undefined;
|
||||
}
|
||||
|
||||
export class NotebookCellListView extends React.Component<CellListProps, NotebookCellListState> {
|
||||
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
protected static dragGhost: HTMLElement | undefined;
|
||||
protected cellListRef: React.RefObject<HTMLUListElement> = React.createRef();
|
||||
|
||||
constructor(props: CellListProps) {
|
||||
super(props);
|
||||
this.state = { selectedCell: props.notebookViewModel.selectedCell, dragOverIndicator: undefined, scrollIntoView: true };
|
||||
this.toDispose.push(props.notebookModel.onDidAddOrRemoveCell(e => {
|
||||
if (e.newCellIds && e.newCellIds.length > 0) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
selectedCell: props.notebookViewModel.selectedCell,
|
||||
scrollIntoView: true
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
...this.state,
|
||||
selectedCell: props.notebookViewModel.selectedCell,
|
||||
scrollIntoView: false
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.toDispose.push(props.notebookModel.onDidChangeContent(events => {
|
||||
if (events.some(e => e.kind === NotebookCellsChangeType.Move)) {
|
||||
// When a cell has been moved, we need to rerender the whole component
|
||||
this.forceUpdate();
|
||||
}
|
||||
}));
|
||||
|
||||
this.toDispose.push(props.notebookViewModel.onDidChangeSelectedCell(e => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
selectedCell: e.cell,
|
||||
scrollIntoView: e.scrollIntoView
|
||||
});
|
||||
}));
|
||||
|
||||
this.toDispose.push(onDomEvent(document, 'focusin', () => {
|
||||
animationFrame().then(() => {
|
||||
if (!this.cellListRef.current) {
|
||||
return;
|
||||
}
|
||||
let hasCellFocus = false;
|
||||
let hasFocus = false;
|
||||
if (this.cellListRef.current.contains(document.activeElement)) {
|
||||
if (this.props.notebookViewModel.selectedCell) {
|
||||
hasCellFocus = true;
|
||||
}
|
||||
hasFocus = true;
|
||||
}
|
||||
this.props.notebookContext.changeCellFocus(hasCellFocus);
|
||||
this.props.notebookContext.changeCellListFocus(hasFocus);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return <ul className='theia-notebook-cell-list' ref={this.cellListRef} onDragStart={e => this.onDragStart(e)}>
|
||||
{this.props.notebookModel.getVisibleCells()
|
||||
.map((cell, index) => {
|
||||
const cellViewModel = this.props.notebookViewModel.cellViewModels.get(cell.handle);
|
||||
return <React.Fragment key={'cell-' + cell.handle}>
|
||||
<NotebookCellDivider
|
||||
menuRegistry={this.props.menuRegistry}
|
||||
isVisible={() => this.isEnabled()}
|
||||
onAddNewCell={handler => this.onAddNewCell(handler, index)}
|
||||
onDrop={e => this.onDrop(e, index)}
|
||||
onDragOver={e => this.onDragOver(e, cell, 'top')} />
|
||||
<CellDropIndicator visible={this.shouldRenderDragOverIndicator(cell, 'top')} />
|
||||
<li className={'theia-notebook-cell' + (this.state.selectedCell === cell ? ' focused' : '') + (this.isEnabled() ? ' draggable' : '')}
|
||||
onDragEnd={e => {
|
||||
NotebookCellListView.dragGhost?.remove();
|
||||
this.setState({ ...this.state, dragOverIndicator: undefined });
|
||||
}}
|
||||
onDragOver={e => this.onDragOver(e, cell)}
|
||||
onDrop={e => this.onDrop(e, index)}
|
||||
draggable={true}
|
||||
tabIndex={-1}
|
||||
data-cell-handle={cell.handle}
|
||||
ref={ref => {
|
||||
if (ref && cell === this.state.selectedCell && this.state.scrollIntoView) {
|
||||
ref.scrollIntoView({ block: 'nearest' });
|
||||
if (cell.cellKind === CellKind.Markup && !cellViewModel?.editing) {
|
||||
ref.focus();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={e => {
|
||||
this.setState({ ...this.state, selectedCell: cell });
|
||||
this.props.notebookViewModel.setSelectedCell(cell, false);
|
||||
}}
|
||||
>
|
||||
<div className='theia-notebook-cell-sidebar'>
|
||||
<div className={'theia-notebook-cell-marker' + (this.state.selectedCell === cell ? ' theia-notebook-cell-marker-selected' : '')}></div>
|
||||
{this.renderCellSidebar(cell)}
|
||||
</div>
|
||||
<div className='theia-notebook-cell-content'>
|
||||
{this.renderCellContent(cell, index)}
|
||||
</div>
|
||||
{this.state.selectedCell === cell &&
|
||||
this.props.toolbarRenderer.renderCellToolbar(NotebookCellActionContribution.ACTION_MENU, cell, {
|
||||
contextMenuArgs: () => [cell], commandArgs: () => [this.props.notebookModel, cell]
|
||||
})
|
||||
}
|
||||
</li>
|
||||
<CellDropIndicator visible={this.shouldRenderDragOverIndicator(cell, 'bottom')} />
|
||||
</React.Fragment>;
|
||||
})
|
||||
}
|
||||
<NotebookCellDivider
|
||||
menuRegistry={this.props.menuRegistry}
|
||||
isVisible={() => this.isEnabled()}
|
||||
onAddNewCell={handler => this.onAddNewCell(handler, this.props.notebookModel.cells.length)}
|
||||
onDrop={e => this.onDrop(e, this.props.notebookModel.cells.length - 1)}
|
||||
onDragOver={e => this.onDragOver(e, this.props.notebookModel.cells[this.props.notebookModel.cells.length - 1], 'bottom')} />
|
||||
</ul>;
|
||||
}
|
||||
|
||||
renderCellContent(cell: NotebookCellModel, index: number): React.ReactNode {
|
||||
const renderer = this.props.renderers.get(cell.cellKind);
|
||||
if (!renderer) {
|
||||
throw new Error(`No renderer found for cell type ${cell.cellKind}`);
|
||||
}
|
||||
return renderer.render(this.props.notebookModel, cell, index);
|
||||
}
|
||||
|
||||
renderCellSidebar(cell: NotebookCellModel): React.ReactNode {
|
||||
const renderer = this.props.renderers.get(cell.cellKind);
|
||||
if (!renderer) {
|
||||
throw new Error(`No renderer found for cell type ${cell.cellKind}`);
|
||||
}
|
||||
return renderer.renderSidebar(this.props.notebookModel, cell);
|
||||
}
|
||||
|
||||
protected onDragStart(event: React.DragEvent<HTMLElement>): void {
|
||||
event.stopPropagation();
|
||||
if (!this.isEnabled()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const cellHandle = (event.target as HTMLLIElement).getAttribute('data-cell-handle');
|
||||
|
||||
if (!cellHandle) {
|
||||
throw new Error('Cell handle not found in element for cell drag event');
|
||||
}
|
||||
|
||||
const index = this.props.notebookModel.getCellIndexByHandle(parseInt(cellHandle));
|
||||
const cell = this.props.notebookModel.cells[index];
|
||||
|
||||
NotebookCellListView.dragGhost = document.createElement('div');
|
||||
NotebookCellListView.dragGhost.classList.add('theia-notebook-drag-ghost-image');
|
||||
NotebookCellListView.dragGhost.appendChild(this.props.renderers.get(cell.cellKind)?.renderDragImage(cell) ?? document.createElement('div'));
|
||||
document.body.appendChild(NotebookCellListView.dragGhost);
|
||||
event.dataTransfer.setDragImage(NotebookCellListView.dragGhost, -10, 0);
|
||||
|
||||
event.dataTransfer.setData('text/theia-notebook-cell-index', index.toString());
|
||||
event.dataTransfer.setData('text/plain', this.props.notebookModel.cells[index].source);
|
||||
}
|
||||
|
||||
protected onDragOver(event: React.DragEvent<HTMLLIElement>, cell: NotebookCellModel, position?: 'top' | 'bottom'): void {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// show indicator
|
||||
this.setState({ ...this.state, dragOverIndicator: { cell, position: position ?? event.nativeEvent.offsetY < event.currentTarget.clientHeight / 2 ? 'top' : 'bottom' } });
|
||||
}
|
||||
|
||||
protected isEnabled(): boolean {
|
||||
return !Boolean(this.props.notebookModel.readOnly);
|
||||
}
|
||||
|
||||
protected onDrop(event: React.DragEvent<HTMLLIElement>, dropElementIndex: number): void {
|
||||
if (!this.isEnabled()) {
|
||||
this.setState({ dragOverIndicator: undefined });
|
||||
return;
|
||||
}
|
||||
const index = parseInt(event.dataTransfer.getData('text/theia-notebook-cell-index'));
|
||||
const isTargetBelow = index < dropElementIndex;
|
||||
let newIdx = this.state.dragOverIndicator?.position === 'top' ? dropElementIndex : dropElementIndex + 1;
|
||||
newIdx = isTargetBelow ? newIdx - 1 : newIdx;
|
||||
if (index !== undefined && index !== dropElementIndex) {
|
||||
this.props.notebookModel.applyEdits([{
|
||||
editType: CellEditType.Move,
|
||||
length: 1,
|
||||
index,
|
||||
newIdx
|
||||
}], true);
|
||||
}
|
||||
this.setState({ ...this.state, dragOverIndicator: undefined });
|
||||
}
|
||||
|
||||
protected onAddNewCell(handler: (...args: unknown[]) => void, index: number): void {
|
||||
if (this.isEnabled()) {
|
||||
this.props.commandRegistry.executeCommand(NotebookCommands.CHANGE_SELECTED_CELL.id, index - 1);
|
||||
handler(
|
||||
this.props.notebookModel,
|
||||
index
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldRenderDragOverIndicator(cell: NotebookCellModel, position: 'top' | 'bottom'): boolean {
|
||||
return this.isEnabled() &&
|
||||
this.state.dragOverIndicator !== undefined &&
|
||||
this.state.dragOverIndicator.cell === cell &&
|
||||
this.state.dragOverIndicator.position === position;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface NotebookCellDividerProps {
|
||||
isVisible: () => boolean;
|
||||
onAddNewCell: (createCommand: (...args: unknown[]) => void) => void;
|
||||
onDrop: (event: React.DragEvent<HTMLLIElement>) => void;
|
||||
onDragOver: (event: React.DragEvent<HTMLLIElement>) => void;
|
||||
menuRegistry: MenuModelRegistry;
|
||||
}
|
||||
|
||||
export function NotebookCellDivider({ isVisible, onAddNewCell, onDrop, onDragOver, menuRegistry }: NotebookCellDividerProps): React.JSX.Element {
|
||||
const [hover, setHover] = React.useState(false);
|
||||
|
||||
const menuPath = NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP; // we contribute into this menu, so it will exist
|
||||
const menuItems: CommandMenu[] = menuRegistry.getMenu(menuPath)!.children.filter(item => CommandMenu.is(item)).map(item => item as CommandMenu);
|
||||
|
||||
const renderItem = (item: CommandMenu): React.ReactNode => {
|
||||
const execute = (...args: unknown[]) => {
|
||||
if (CommandMenu.is(item)) {
|
||||
item.run([...menuPath, item.id], ...args);
|
||||
}
|
||||
};
|
||||
return <button
|
||||
key={item.id}
|
||||
className='theia-notebook-add-cell-button'
|
||||
onClick={() => onAddNewCell(execute)}
|
||||
title={nls.localizeByDefault(`Add ${item.label} Cell`)}
|
||||
>
|
||||
<div className={item.icon + ' theia-notebook-add-cell-button-icon'} />
|
||||
<div className='theia-notebook-add-cell-button-text'>{item.label}</div>
|
||||
</button>;
|
||||
};
|
||||
|
||||
return <li className='theia-notebook-cell-divider' onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onDrop={onDrop} onDragOver={onDragOver}>
|
||||
{hover && isVisible() && <div className='theia-notebook-add-cell-buttons'>
|
||||
{menuItems.map((item: CommandMenu) => renderItem(item))}
|
||||
</div>}
|
||||
</li>;
|
||||
}
|
||||
|
||||
function CellDropIndicator(props: { visible: boolean }): React.JSX.Element {
|
||||
return <div className='theia-notebook-cell-drop-indicator' style={{ visibility: props.visible ? 'visible' : 'hidden' }} />;
|
||||
}
|
||||
Reference in New Issue
Block a user