mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-14 01:25:37 -05:00
* Merge from vscode fcf3346a8e9f5ee1e00674461d9e2c2292a14ee3 * Fix test build break * Update distro * Fix build errors * Update distro * Update REH build file * Update build task names for REL * Fix product build yaml * Fix product REH task name * Fix type in task name * Update linux build step * Update windows build tasks * Turn off server publish * Disable REH * Fix typo * Bump distro * Update vscode tests * Bump distro * Fix type in disto * Bump distro * Turn off docker build * Remove docker step from release Co-authored-by: ADS Merger <andresse@microsoft.com> Co-authored-by: Karl Burtram <karlb@microsoft.com>
1056 lines
34 KiB
TypeScript
1056 lines
34 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { ITableEvent, ITableRenderer, ITableMouseEvent, ITableContextMenuEvent, ITableDataSource, IStaticTableRenderer, IStaticColumn, ITableColumn, TableError } from 'sql/base/browser/ui/table/highPerf/table';
|
|
|
|
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
|
import { memoize } from 'vs/base/common/decorators';
|
|
import { Event, Emitter, EventBufferer } from 'vs/base/common/event';
|
|
import * as DOM from 'vs/base/browser/dom';
|
|
import { TableView, ITableViewOptions } from 'sql/base/browser/ui/table/highPerf/tableView';
|
|
import { ScrollEvent } from 'vs/base/common/scrollable';
|
|
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
|
import { domEvent } from 'vs/base/browser/event';
|
|
import { KeyCode } from 'vs/base/common/keyCodes';
|
|
import * as platform from 'vs/base/common/platform';
|
|
import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget';
|
|
import { Color } from 'vs/base/common/color';
|
|
import { getOrDefault } from 'vs/base/common/objects';
|
|
import { isNumber } from 'vs/base/common/types';
|
|
import { clamp } from 'vs/base/common/numbers';
|
|
import { GlobalMouseMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor';
|
|
import { GridPosition } from 'sql/base/common/gridPosition';
|
|
import { GridRange, IGridRange } from 'sql/base/common/gridRange';
|
|
|
|
interface ITraitChangeEvent {
|
|
indexes: IGridRange[];
|
|
browserEvent?: UIEvent;
|
|
}
|
|
|
|
type ITraitTemplateData = HTMLElement;
|
|
|
|
interface IRenderedContainer {
|
|
templateData: ITraitTemplateData;
|
|
index: GridPosition;
|
|
}
|
|
|
|
class TraitRenderer<T> implements ITableRenderer<T, ITraitTemplateData>
|
|
{
|
|
private renderedElements: IRenderedContainer[] = [];
|
|
|
|
constructor(private trait: Trait<T>) { }
|
|
|
|
get templateId(): string {
|
|
return `template:${this.trait.trait}`;
|
|
}
|
|
|
|
renderTemplate(container: HTMLElement): ITraitTemplateData {
|
|
return container;
|
|
}
|
|
|
|
renderCell(element: T, row: number, cell: number, columnId: string, templateData: ITraitTemplateData): void {
|
|
const renderedElementIndex = this.renderedElements.findIndex(el => el.templateData === templateData);
|
|
|
|
if (renderedElementIndex >= 0) {
|
|
const rendered = this.renderedElements[renderedElementIndex];
|
|
this.trait.unrender(templateData);
|
|
rendered.index = new GridPosition(row, cell);
|
|
} else {
|
|
const rendered = { index: new GridPosition(row, cell), templateData };
|
|
this.renderedElements.push(rendered);
|
|
}
|
|
|
|
this.trait.renderIndex(new GridPosition(row, cell), templateData);
|
|
}
|
|
|
|
renderIndexes(indexes: IGridRange[]): void {
|
|
for (const { index, templateData } of this.renderedElements) {
|
|
if (!!indexes.find(v => GridRange.containsPosition(v, index))) {
|
|
this.trait.renderIndex(index, templateData);
|
|
}
|
|
}
|
|
}
|
|
|
|
disposeTemplate(templateData: ITraitTemplateData): void {
|
|
const index = this.renderedElements.findIndex(el => el.templateData === templateData);
|
|
|
|
if (index < 0) {
|
|
return;
|
|
}
|
|
|
|
this.renderedElements.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
class DOMFocusController<T> implements IDisposable {
|
|
|
|
private disposables: IDisposable[] = [];
|
|
|
|
constructor(
|
|
private list: Table<T>,
|
|
private view: TableView<T>
|
|
) {
|
|
this.disposables = [];
|
|
|
|
const onKeyDown = Event.chain(domEvent(view.domNode, 'keydown'))
|
|
.map(e => new StandardKeyboardEvent(e));
|
|
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey)
|
|
.on(this.onTab, this, this.disposables);
|
|
}
|
|
|
|
private onTab(e: StandardKeyboardEvent): void {
|
|
if (e.target !== this.view.domNode) {
|
|
return;
|
|
}
|
|
|
|
const focus = this.list.getFocus();
|
|
|
|
if (focus.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const focusedDomElement = this.view.domElement(focus[0].startRow, focus[0].startColumn);
|
|
|
|
if (!focusedDomElement) {
|
|
return;
|
|
}
|
|
|
|
const tabIndexElement = focusedDomElement.querySelector('[tabIndex]');
|
|
|
|
if (!tabIndexElement || !(tabIndexElement instanceof HTMLElement) || tabIndexElement.tabIndex === -1) {
|
|
return;
|
|
}
|
|
|
|
const style = window.getComputedStyle(tabIndexElement);
|
|
if (style.visibility === 'hidden' || style.display === 'none') {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
tabIndexElement.focus();
|
|
}
|
|
|
|
dispose() {
|
|
this.disposables = dispose(this.disposables);
|
|
}
|
|
}
|
|
|
|
class Trait<T> implements IDisposable {
|
|
|
|
private indexes: Array<IGridRange> = [];
|
|
private sortedIndexes: Array<IGridRange> = [];
|
|
|
|
private _onChange = new Emitter<ITraitChangeEvent>();
|
|
get onChange(): Event<ITraitChangeEvent> { return this._onChange.event; }
|
|
|
|
get trait(): string { return this._trait; }
|
|
|
|
@memoize
|
|
get renderer(): TraitRenderer<T> {
|
|
return new TraitRenderer<T>(this);
|
|
}
|
|
|
|
constructor(private _trait: string) { }
|
|
|
|
renderIndex(index: GridPosition, container: HTMLElement): void {
|
|
DOM.toggleClass(container, this._trait, this.contains(index));
|
|
}
|
|
|
|
unrender(container: HTMLElement): void {
|
|
DOM.removeClass(container, this._trait);
|
|
}
|
|
|
|
/**
|
|
* Sets the indexes which should have this trait.
|
|
*
|
|
* @param indexes Indexes which should have this trait.
|
|
* @return The old indexes which had this trait.
|
|
*/
|
|
set(indexes: Array<IGridRange>, browserEvent?: UIEvent): Array<IGridRange> {
|
|
return this._set(indexes, indexes, browserEvent);
|
|
}
|
|
|
|
private _set(indexes: Array<IGridRange>, sortedIndexes: Array<IGridRange>, browserEvent?: UIEvent): Array<IGridRange> {
|
|
const result = this.indexes;
|
|
const sortedResult = this.sortedIndexes;
|
|
|
|
this.indexes = indexes;
|
|
this.sortedIndexes = sortedIndexes;
|
|
|
|
// const toRender = disjunction(sortedResult, indexes);
|
|
this.renderer.renderIndexes(indexes.concat(sortedResult));
|
|
|
|
this._onChange.fire({ indexes, browserEvent });
|
|
return result;
|
|
}
|
|
|
|
get(): Array<IGridRange> {
|
|
return this.indexes;
|
|
}
|
|
|
|
contains(index: GridPosition): boolean {
|
|
return !!this.indexes.find(v => GridRange.containsPosition(v, index));
|
|
}
|
|
|
|
dispose() {
|
|
this._onChange = dispose(this._onChange);
|
|
}
|
|
}
|
|
|
|
class FocusTrait<T> extends Trait<T> {
|
|
|
|
constructor() {
|
|
super('focused');
|
|
}
|
|
|
|
renderIndex(index: GridPosition, container: HTMLElement): void {
|
|
super.renderIndex(index, container);
|
|
|
|
if (this.contains(index)) {
|
|
container.setAttribute('aria-selected', 'true');
|
|
} else {
|
|
container.removeAttribute('aria-selected');
|
|
}
|
|
}
|
|
}
|
|
|
|
class PipelineRenderer<T> implements ITableRenderer<T, any> {
|
|
|
|
constructor(
|
|
private renderers: ITableRenderer<any /* TODO@joao */, any>[]
|
|
) { }
|
|
|
|
renderTemplate(container: HTMLElement): any[] {
|
|
return this.renderers.map(r => r.renderTemplate(container));
|
|
}
|
|
|
|
renderCell(element: T, index: number, cell: number, columnId: string, templateData: any[], height: number | undefined): void {
|
|
let i = 0;
|
|
|
|
for (const renderer of this.renderers) {
|
|
renderer.renderCell(element, index, cell, columnId, templateData[i++], height);
|
|
}
|
|
}
|
|
|
|
disposeCell(element: T, index: number, cell: number, columnId: string, templateData: any[], height: number | undefined): void {
|
|
let i = 0;
|
|
|
|
for (const renderer of this.renderers) {
|
|
if (renderer.disposeCell) {
|
|
renderer.disposeCell(element, index, cell, columnId, templateData[i], height);
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
disposeTemplate(templateData: any[]): void {
|
|
let i = 0;
|
|
|
|
for (const renderer of this.renderers) {
|
|
renderer.disposeTemplate(templateData[i++]);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function isSelectionSingleChangeEvent(event: ITableMouseEvent<any>): boolean {
|
|
return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
|
|
}
|
|
|
|
export function isSelectionRangeChangeEvent(event: ITableMouseEvent<any>): boolean {
|
|
return event.browserEvent.shiftKey;
|
|
}
|
|
|
|
function isMouseRightClick(event: UIEvent): boolean {
|
|
return event instanceof MouseEvent && event.button === 2;
|
|
}
|
|
|
|
const DefaultMultipleSelectionController = {
|
|
isSelectionSingleChangeEvent,
|
|
isSelectionRangeChangeEvent
|
|
};
|
|
|
|
export interface IOpenController {
|
|
shouldOpen(event: UIEvent): boolean;
|
|
}
|
|
|
|
const DefaultOpenController: IOpenController = {
|
|
shouldOpen: (event: UIEvent) => {
|
|
if (event instanceof MouseEvent) {
|
|
return !isMouseRightClick(event);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
export interface IMultipleSelectionController<T> {
|
|
isSelectionSingleChangeEvent(event: ITableMouseEvent<T>): boolean;
|
|
isSelectionRangeChangeEvent(event: ITableMouseEvent<T>): boolean;
|
|
}
|
|
|
|
class RowCountRenderer implements IStaticTableRenderer<any, HTMLElement> {
|
|
renderTemplate(container: HTMLElement): HTMLElement {
|
|
return DOM.append(container, DOM.$('.row-count'));
|
|
}
|
|
|
|
renderCell(element: undefined, index: number, ccell: number, olumnId: string, templateData: HTMLElement, width: number): void {
|
|
templateData.innerText = `${index}`;
|
|
}
|
|
|
|
disposeTemplate(templateData: HTMLElement): void {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
}
|
|
|
|
const rowCountColumnDef: IStaticColumn<any, HTMLElement> = {
|
|
id: 'rowCount',
|
|
name: '',
|
|
renderer: new RowCountRenderer(),
|
|
cellClass: 'row-count-cell',
|
|
static: true,
|
|
width: 30,
|
|
resizeable: false
|
|
};
|
|
|
|
function rowCountFilter(column: ITableColumn<any, any>): boolean {
|
|
return column.id !== rowCountColumnDef.id;
|
|
}
|
|
|
|
class KeyboardController<T> implements IDisposable {
|
|
|
|
private disposables: IDisposable[];
|
|
// private openController: IOpenController;
|
|
|
|
constructor(
|
|
private table: Table<T>,
|
|
private view: TableView<T>,
|
|
options?: ITableOptions<T>
|
|
) {
|
|
// const multipleSelectionSupport = !(options.multipleSelectionSupport === false);
|
|
this.disposables = [];
|
|
|
|
// this.openController = options.openController || DefaultOpenController;
|
|
|
|
const onKeyDown = Event.chain(domEvent(view.domNode, 'keydown'))
|
|
// .filter(e => !isInputElement(e.target as HTMLElement))
|
|
.map(e => new StandardKeyboardEvent(e));
|
|
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables);
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this, this.disposables);
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this, this.disposables);
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables);
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables);
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUpArrow, this, this.disposables);
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDownArrow, this, this.disposables);
|
|
onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
|
|
|
|
// if (multipleSelectionSupport) {
|
|
onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KEY_A).on(this.onCtrlA, this, this.disposables);
|
|
// }
|
|
}
|
|
|
|
private onEnter(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.table.setSelection(this.table.getFocus(), e.browserEvent);
|
|
|
|
// if (this.openController.shouldOpen(e.browserEvent)) {
|
|
// this.list.open(this.list.getFocus(), e.browserEvent);
|
|
// }
|
|
}
|
|
|
|
private onUpArrow(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.table.focusPreviousRow(1, false, e.browserEvent);
|
|
this.table.reveal(this.table.getFocus()[0].startRow);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
private onDownArrow(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.table.focusNextRow(1, false, e.browserEvent);
|
|
this.table.reveal(this.table.getFocus()[0].startRow);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
private onRightArrow(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.table.focusNextCell(1, false, e.browserEvent);
|
|
this.table.reveal(this.table.getFocus()[0].startRow);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
private onLeftArrow(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.table.focusPreviousCell(1, false, e.browserEvent);
|
|
this.table.reveal(this.table.getFocus()[0].startRow);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
private onPageUpArrow(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
// this.table.focusPreviousPage(e.browserEvent);
|
|
// this.table.reveal(this.table.getFocus()[0]);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
private onPageDownArrow(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
// this.table.focusNextPage(e.browserEvent);
|
|
// this.table.reveal(this.table.getFocus()[0]);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
private onCtrlA(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
// this.table.setSelection(range(this.table.length), e.browserEvent);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
private onEscape(e: StandardKeyboardEvent): void {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.table.setSelection([], e.browserEvent);
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
dispose() {
|
|
this.disposables = dispose(this.disposables);
|
|
}
|
|
}
|
|
|
|
export class MouseController<T> implements IDisposable {
|
|
|
|
private multipleSelectionSupport: boolean;
|
|
readonly multipleSelectionController?: IMultipleSelectionController<T>;
|
|
private openController: IOpenController;
|
|
private disposables: IDisposable[] = [];
|
|
private readonly _mouseMoveMonitor = new GlobalMouseMoveMonitor<ITableMouseEvent<T>>();
|
|
|
|
private startMouseEvent?: ITableMouseEvent<T>;
|
|
|
|
constructor(protected table: Table<T>, protected view: TableView<T>) {
|
|
this.multipleSelectionSupport = true;
|
|
|
|
if (this.multipleSelectionSupport) {
|
|
this.multipleSelectionController = DefaultMultipleSelectionController;
|
|
}
|
|
|
|
this.openController = DefaultOpenController;
|
|
|
|
this.disposables.push(this._mouseMoveMonitor);
|
|
|
|
table.onMouseDown(this.onMouseDown, this, this.disposables);
|
|
// table.onMouseClick(this.onPointer, this, this.disposables);
|
|
table.onContextMenu(this.onContextMenu, this, this.disposables);
|
|
}
|
|
|
|
protected isSelectionSingleChangeEvent(event: ITableMouseEvent<any>): boolean {
|
|
if (this.multipleSelectionController) {
|
|
return this.multipleSelectionController.isSelectionSingleChangeEvent(event);
|
|
}
|
|
|
|
return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
|
|
}
|
|
|
|
protected isSelectionRangeChangeEvent(event: ITableMouseEvent<any>): boolean {
|
|
if (this.multipleSelectionController) {
|
|
return this.multipleSelectionController.isSelectionRangeChangeEvent(event);
|
|
}
|
|
|
|
return event.browserEvent.shiftKey;
|
|
}
|
|
|
|
private isSelectionChangeEvent(event: ITableMouseEvent<any>): boolean {
|
|
return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event);
|
|
}
|
|
|
|
private onMouseDown(e: ITableMouseEvent<T>): void {
|
|
if (document.activeElement !== e.browserEvent.target) {
|
|
this.table.domFocus();
|
|
}
|
|
const merger = (lastEvent: ITableMouseEvent<T> | null, currentEvent: MouseEvent): ITableMouseEvent<T> => {
|
|
return this.view.toMouseEvent(currentEvent);
|
|
};
|
|
this._mouseMoveMonitor.startMonitoring(e.browserEvent.target as HTMLElement, e.buttons, merger, e => this.onMouseMove(e), () => this.onMouseStop());
|
|
this.onPointer(e);
|
|
}
|
|
|
|
private onContextMenu(e: ITableContextMenuEvent<T>): void {
|
|
const focus = typeof e.index === 'undefined' ? [] : [new GridRange(e.index.row, e.index.column)];
|
|
this.table.setFocus(focus, e.browserEvent);
|
|
}
|
|
|
|
protected onMouseMove(event: ITableMouseEvent<T>): void {
|
|
if (event.index) {
|
|
this.startMouseEvent = this.startMouseEvent || event;
|
|
this.table.setSelection([new GridRange(this.startMouseEvent.index!.row, this.startMouseEvent.index!.column, event.index.row, event.index.column)]);
|
|
}
|
|
}
|
|
|
|
protected onMouseStop(): void {
|
|
this.startMouseEvent = undefined;
|
|
}
|
|
|
|
protected onPointer(e: ITableMouseEvent<T>): void {
|
|
|
|
let reference = this.table.getFocus();
|
|
const selection = this.table.getSelection();
|
|
reference = reference === undefined ? selection : reference;
|
|
|
|
const focus = e.index;
|
|
|
|
if (typeof focus === 'undefined') {
|
|
this.table.setFocus([], e.browserEvent);
|
|
this.table.setSelection([], e.browserEvent);
|
|
return;
|
|
}
|
|
|
|
if (this.multipleSelectionSupport && this.isSelectionRangeChangeEvent(e)) {
|
|
return this.changeSelection(e, reference);
|
|
}
|
|
|
|
if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) {
|
|
return this.changeSelection(e, reference);
|
|
}
|
|
|
|
this.table.setFocus([new GridRange(focus.row, focus.column)], e.browserEvent);
|
|
|
|
if (!isMouseRightClick(e.browserEvent)) {
|
|
this.table.setSelection([new GridRange(focus.row, focus.column)], e.browserEvent);
|
|
|
|
if (this.openController.shouldOpen(e.browserEvent)) {
|
|
// this.table.open([focus], e.browserEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
private changeSelection(e: ITableMouseEvent<T>, reference: IGridRange[] | undefined): void {
|
|
const focus = e.index!;
|
|
|
|
if (this.isSelectionRangeChangeEvent(e) && reference !== undefined) {
|
|
const selection = this.table.getSelection();
|
|
const lastSelection = selection.pop();
|
|
if (lastSelection) {
|
|
this.table.setSelection([...selection, GridRange.plusRange(lastSelection, new GridRange(focus.row, focus.column))]);
|
|
} else {
|
|
this.table.setSelection([...selection, new GridRange(focus.row, focus.column)]);
|
|
}
|
|
} else if (this.isSelectionSingleChangeEvent(e)) {
|
|
const selection = this.table.getSelection();
|
|
selection.push(new GridRange(focus.row, focus.column));
|
|
this.table.setSelection(selection);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
this.disposables = dispose(this.disposables);
|
|
}
|
|
}
|
|
|
|
export interface ITableOptions<T> extends ITableViewOptions<T> {
|
|
keyboardSupport?: boolean;
|
|
dnd?: boolean;
|
|
}
|
|
|
|
export interface ITableStyles extends IListStyles {
|
|
cellOutlineColor?: Color;
|
|
tableHeaderAndRowCountColor?: Color;
|
|
}
|
|
|
|
export class DefaultStyleController implements IStyleController {
|
|
|
|
constructor(private styleElement: HTMLStyleElement, private selectorSuffix?: string) { }
|
|
|
|
style(styles: ITableStyles): void {
|
|
const suffix = this.selectorSuffix ? `.${this.selectorSuffix}` : '';
|
|
const content: string[] = [];
|
|
|
|
if (styles.listFocusBackground) {
|
|
content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.focused { background-color: ${styles.listFocusBackground}; }`);
|
|
content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case!
|
|
}
|
|
|
|
if (styles.listFocusForeground) {
|
|
content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.focused { color: ${styles.listFocusForeground}; }`);
|
|
}
|
|
|
|
if (styles.listActiveSelectionBackground) {
|
|
content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected { background-color: ${styles.listActiveSelectionBackground}; }`);
|
|
content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!
|
|
}
|
|
|
|
if (styles.listActiveSelectionForeground) {
|
|
content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected { color: ${styles.listActiveSelectionForeground}; }`);
|
|
}
|
|
|
|
if (styles.listFocusAndSelectionBackground) {
|
|
content.push(`
|
|
.monaco-drag-image,
|
|
.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }
|
|
`);
|
|
}
|
|
|
|
if (styles.listFocusAndSelectionForeground) {
|
|
content.push(`
|
|
.monaco-drag-image,
|
|
.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }
|
|
`);
|
|
}
|
|
|
|
if (styles.listInactiveFocusBackground) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.focused { background-color: ${styles.listInactiveFocusBackground}; }`);
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!
|
|
}
|
|
|
|
if (styles.listInactiveSelectionBackground) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected { background-color: ${styles.listInactiveSelectionBackground}; }`);
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!
|
|
}
|
|
|
|
if (styles.listInactiveSelectionForeground) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected { color: ${styles.listInactiveSelectionForeground}; }`);
|
|
}
|
|
|
|
if (styles.listHoverBackground) {
|
|
content.push(`.monaco-perftable${suffix}:not(.drop-target) .monaco-perftable-cell:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);
|
|
}
|
|
|
|
if (styles.listHoverForeground) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`);
|
|
}
|
|
|
|
if (styles.listSelectionOutline) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);
|
|
}
|
|
|
|
if (styles.listFocusOutline) {
|
|
content.push(`
|
|
.monaco-drag-image,
|
|
.monaco-perftable${suffix}:focus .monaco-perftable-cell.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }
|
|
`);
|
|
}
|
|
|
|
if (styles.listInactiveFocusOutline) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);
|
|
}
|
|
|
|
if (styles.listHoverOutline) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);
|
|
}
|
|
|
|
if (styles.listDropBackground) {
|
|
content.push(`
|
|
.monaco-perftable${suffix}.drop-target,
|
|
.monaco-perftable${suffix} .monaco-perftable-cell.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; }
|
|
`);
|
|
}
|
|
|
|
if (styles.listFilterWidgetBackground) {
|
|
content.push(`.monaco-perftable-type-filter { background-color: ${styles.listFilterWidgetBackground} }`);
|
|
}
|
|
|
|
if (styles.listFilterWidgetOutline) {
|
|
content.push(`.monaco-perftable-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`);
|
|
}
|
|
|
|
if (styles.listFilterWidgetNoMatchesOutline) {
|
|
content.push(`.monaco-perftable-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`);
|
|
}
|
|
|
|
if (styles.listMatchesShadow) {
|
|
content.push(`.monaco-perftable-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`);
|
|
}
|
|
|
|
if (styles.cellOutlineColor) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell { border: 1px solid ${styles.cellOutlineColor}; }`);
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-header-cell { border: 1px solid ${styles.cellOutlineColor}; }`);
|
|
}
|
|
|
|
if (styles.tableHeaderAndRowCountColor) {
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-header-cell { background-color: ${styles.tableHeaderAndRowCountColor}; }`);
|
|
content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.row-count-cell { background-color: ${styles.tableHeaderAndRowCountColor}; }`);
|
|
}
|
|
|
|
const newStyles = content.join('\n');
|
|
if (newStyles !== this.styleElement.innerHTML) {
|
|
this.styleElement.innerHTML = newStyles;
|
|
}
|
|
}
|
|
}
|
|
|
|
const DefaultOptions = {
|
|
rowCountColumn: true,
|
|
};
|
|
|
|
export class Table<T> implements IDisposable {
|
|
|
|
private focus: Trait<T>;
|
|
private selection: Trait<T>;
|
|
private eventBufferer = new EventBufferer();
|
|
private view: TableView<T>;
|
|
private styleElement: HTMLStyleElement;
|
|
private styleController: IStyleController;
|
|
|
|
public inDrag = false;
|
|
|
|
protected readonly disposables = new DisposableStore();
|
|
|
|
@memoize get onFocusChange(): Event<ITableEvent<T>> {
|
|
return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toTableEvent(e));
|
|
}
|
|
|
|
@memoize get onSelectionChange(): Event<ITableEvent<T>> {
|
|
return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toTableEvent(e));
|
|
}
|
|
|
|
private toTableEvent({ indexes, browserEvent }: ITraitChangeEvent) {
|
|
return { indexes, elements: indexes.map(i => this.view.element(i.startRow)!), browserEvent };
|
|
}
|
|
|
|
get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }
|
|
get onMouseClick(): Event<ITableMouseEvent<T>> { return this.view.onMouseClick; }
|
|
get onMouseDblClick(): Event<ITableMouseEvent<T>> { return this.view.onMouseDblClick; }
|
|
get onMouseMiddleClick(): Event<ITableMouseEvent<T>> { return this.view.onMouseMiddleClick; }
|
|
get onMouseUp(): Event<ITableMouseEvent<T>> { return this.view.onMouseUp; }
|
|
get onMouseDown(): Event<ITableMouseEvent<T>> { return this.view.onMouseDown; }
|
|
get onMouseOver(): Event<ITableMouseEvent<T>> { return this.view.onMouseOver; }
|
|
get onMouseMove(): Event<ITableMouseEvent<T>> { return this.view.onMouseMove; }
|
|
get onMouseOut(): Event<ITableMouseEvent<T>> { return this.view.onMouseOut; }
|
|
|
|
private didJustPressContextMenuKey: boolean = false;
|
|
@memoize get onContextMenu(): Event<ITableContextMenuEvent<T>> {
|
|
const fromKeydown = Event.chain(domEvent(this.view.domNode, 'keydown'))
|
|
.map(e => new StandardKeyboardEvent(e))
|
|
.filter(e => this.didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
|
|
.filter(e => { e.preventDefault(); e.stopPropagation(); return false; })
|
|
.event as Event<any>;
|
|
|
|
const fromKeyup = Event.chain(domEvent(this.view.domNode, 'keyup'))
|
|
.filter(() => {
|
|
const didJustPressContextMenuKey = this.didJustPressContextMenuKey;
|
|
this.didJustPressContextMenuKey = false;
|
|
return didJustPressContextMenuKey;
|
|
})
|
|
.filter(() => this.getFocus().length > 0 && !!this.view.domElement(this.getFocus()[0].startRow, this.getFocus()[0].startColumn))
|
|
.map(browserEvent => {
|
|
const index = this.getFocus()[0];
|
|
const element = this.view.element(index.startRow);
|
|
const anchor = this.view.domElement(index.startRow, index.startColumn) as HTMLElement;
|
|
return { index: GridRange.lift(index).getStartPosition(), element, anchor, browserEvent };
|
|
})
|
|
.event;
|
|
|
|
const fromMouse = Event.chain(this.view.onContextMenu)
|
|
.filter(() => !this.didJustPressContextMenuKey)
|
|
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent }))
|
|
.event;
|
|
|
|
return Event.any<ITableContextMenuEvent<T>>(fromKeydown, fromKeyup, fromMouse);
|
|
}
|
|
|
|
get onKeyDown(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keydown'); }
|
|
get onKeyUp(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keyup'); }
|
|
get onKeyPress(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keypress'); }
|
|
|
|
readonly onDidFocus: Event<void>;
|
|
readonly onDidBlur: Event<void>;
|
|
|
|
private readonly _onDidDispose = new Emitter<void>();
|
|
readonly onDidDispose: Event<void> = this._onDidDispose.event;
|
|
|
|
constructor(
|
|
private readonly user: string,
|
|
container: HTMLElement,
|
|
columns: ITableColumn<T, any>[],
|
|
dataSource: ITableDataSource<T>,
|
|
options: ITableOptions<T> = DefaultOptions
|
|
) {
|
|
this.focus = new FocusTrait();
|
|
this.selection = new Trait('selected');
|
|
|
|
const baseRenderers: ITableRenderer<T, ITraitTemplateData>[] = [this.focus.renderer, this.selection.renderer];
|
|
|
|
columns = columns.map(r => {
|
|
r.renderer = new PipelineRenderer([...baseRenderers, r.renderer]);
|
|
return r;
|
|
});
|
|
|
|
options.rowCountColumn = getOrDefault(options, o => o.rowCountColumn, DefaultOptions.rowCountColumn);
|
|
|
|
if (options.rowCountColumn) {
|
|
columns.unshift(rowCountColumnDef);
|
|
}
|
|
|
|
this.view = new TableView(container, columns, dataSource, options);
|
|
|
|
this.view.domNode.setAttribute('aria-multiselectable', 'true');
|
|
|
|
this.styleElement = DOM.createStyleSheet(this.view.domNode);
|
|
|
|
this.styleController = new DefaultStyleController(this.styleElement, this.view.domId);
|
|
|
|
this.disposables.add(new DOMFocusController(this, this.view));
|
|
|
|
if (!options || typeof options.keyboardSupport !== 'boolean' || options.keyboardSupport) {
|
|
const controller = new KeyboardController(this, this.view, options);
|
|
this.disposables.add(controller);
|
|
}
|
|
|
|
this.onDidFocus = Event.map(domEvent(this.view.domNode, 'focus', true), () => null!);
|
|
this.onDidBlur = Event.map(domEvent(this.view.domNode, 'blur', true), () => null!);
|
|
|
|
this.disposables.add(this.createMouseController());
|
|
|
|
this.onFocusChange(this._onFocusChange, this, this.disposables);
|
|
this.onSelectionChange(this._onSelectionChange, this, this.disposables);
|
|
}
|
|
|
|
protected createMouseController(): MouseController<T> {
|
|
return new MouseController(this, this.view);
|
|
}
|
|
|
|
get length(): number {
|
|
return this.view.length;
|
|
}
|
|
|
|
set length(length: number) {
|
|
this.view.length = length;
|
|
}
|
|
|
|
get columnLength(): number {
|
|
return this.view.columnLength;
|
|
}
|
|
|
|
layout(height?: number, width?: number): void {
|
|
this.view.layout(height, width);
|
|
}
|
|
|
|
domFocus(): void {
|
|
this.view.domNode.focus();
|
|
}
|
|
|
|
setSelection(indexes: IGridRange[], browserEvent?: UIEvent): void {
|
|
// for (const index of indexes) {
|
|
// if (index < 0 || index >= this.length) {
|
|
// throw new Error(`Invalid index ${index}`);
|
|
// }
|
|
// }
|
|
|
|
this.selection.set(indexes, browserEvent);
|
|
}
|
|
|
|
getSelection(): IGridRange[] {
|
|
return this.selection.get();
|
|
}
|
|
|
|
setFocus(indexes: IGridRange[], browserEvent?: UIEvent): void {
|
|
// for (const index of indexes) {
|
|
// if (index < 0 || index >= this.length) {
|
|
// throw new Error(`Invalid index ${index}`);
|
|
// }
|
|
// }
|
|
|
|
this.focus.set(indexes, browserEvent);
|
|
this.selection.set(indexes, browserEvent);
|
|
}
|
|
|
|
reveal(index: number, relativeTop?: number): void {
|
|
if (index < 0 || index >= this.length) {
|
|
throw new TableError(this.user, `Invalid index ${index}`);
|
|
}
|
|
|
|
const scrollTop = this.view.getScrollTop();
|
|
const elementTop = this.view.elementTop(index);
|
|
const elementHeight = this.view.rowHeight;
|
|
|
|
if (isNumber(relativeTop)) {
|
|
// y = mx + b
|
|
const m = elementHeight - this.view.renderHeight;
|
|
this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop);
|
|
} else {
|
|
const viewItemBottom = elementTop + elementHeight;
|
|
const wrapperBottom = scrollTop + this.view.renderHeight;
|
|
|
|
if (elementTop < scrollTop) {
|
|
this.view.setScrollTop(elementTop);
|
|
} else if (viewItemBottom >= wrapperBottom) {
|
|
this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
focusNextCell(n = 1, loop = false, browserEvent?: UIEvent, filter: (column: ITableColumn<T, any>) => boolean = rowCountFilter): void {
|
|
if (this.length === 0) { return; }
|
|
|
|
const focus = this.focus.get();
|
|
const cellIndex = focus.length > 0 ? focus[0].startColumn! : 0;
|
|
const targetColumn = this.findNextColumn(cellIndex + n, loop, filter);
|
|
const targetRow = focus.length > 0 ? focus[0].startRow : 0;
|
|
|
|
if (targetColumn > -1) {
|
|
this.setFocus([new GridRange(targetRow, targetColumn)], browserEvent);
|
|
}
|
|
}
|
|
|
|
focusPreviousCell(n = 1, loop = false, browserEvent?: UIEvent, filter: (column: ITableColumn<T, any>) => boolean = rowCountFilter): void {
|
|
if (this.length === 0) { return; }
|
|
|
|
const focus = this.focus.get();
|
|
const cellIndex = focus.length > 0 ? focus[0].startColumn! : 0;
|
|
const targetColumn = this.findPreviousColumn(cellIndex - n, loop, filter);
|
|
const targetRow = focus.length > 0 ? focus[0].startRow : 0;
|
|
|
|
if (targetColumn > -1) {
|
|
this.setFocus([new GridRange(targetRow, targetColumn)], browserEvent);
|
|
}
|
|
}
|
|
|
|
focusNextRow(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
|
|
if (this.length === 0) { return; }
|
|
|
|
const focus = this.focus.get();
|
|
const index = this.findNextRowIndex(focus.length > 0 ? focus[0].startRow + n : 0, loop, filter);
|
|
|
|
const targetColumn = focus.length > 0 ? focus[0].startColumn : 0;
|
|
|
|
if (index > -1) {
|
|
this.setFocus([new GridRange(index, targetColumn)], browserEvent);
|
|
}
|
|
}
|
|
|
|
focusPreviousRow(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
|
|
if (this.length === 0) { return; }
|
|
|
|
const focus = this.focus.get();
|
|
const index = this.findPreviousRowIndex(focus.length > 0 ? focus[0].startRow - n : 0, loop, filter);
|
|
|
|
const targetColumn = focus.length > 0 ? focus[0].startColumn : 0;
|
|
|
|
if (index > -1) {
|
|
this.setFocus([new GridRange(index, targetColumn)], browserEvent);
|
|
}
|
|
}
|
|
|
|
private findNextColumn(index: number, loop = false, filter?: (column: ITableColumn<T, any>) => boolean): number {
|
|
for (let i = 0; i < this.columnLength; i++) {
|
|
if (index >= this.columnLength && !loop) {
|
|
return -1;
|
|
}
|
|
|
|
index = index % this.columnLength;
|
|
|
|
if (!filter || filter(this.view.column(index)!)) {
|
|
return index;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private findPreviousColumn(index: number, loop = false, filter?: (column: ITableColumn<T, any>) => boolean): number {
|
|
for (let i = 0; i < this.columnLength; i++) {
|
|
if (index < 0 && !loop) {
|
|
return -1;
|
|
}
|
|
|
|
index = (this.columnLength + (index % this.columnLength)) % this.columnLength;
|
|
|
|
if (!filter || filter(this.view.column(index)!)) {
|
|
return index;
|
|
}
|
|
|
|
index--;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private findNextRowIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
|
|
for (let i = 0; i < this.length; i++) {
|
|
if (index >= this.length && !loop) {
|
|
return -1;
|
|
}
|
|
|
|
index = index % this.length;
|
|
|
|
// if (!filter || filter(this.view.element(index))) {
|
|
return index;
|
|
// }
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private findPreviousRowIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
|
|
for (let i = 0; i < this.length; i++) {
|
|
if (index < 0 && !loop) {
|
|
return -1;
|
|
}
|
|
|
|
index = (this.length + (index % this.length)) % this.length;
|
|
|
|
// if (!filter || filter(this.view.element(index))) {
|
|
return index;
|
|
//
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
getFocus(): Array<IGridRange> {
|
|
return this.focus.get();
|
|
}
|
|
|
|
style(styles: ITableStyles): void {
|
|
this.styleController.style(styles);
|
|
}
|
|
|
|
getHTMLElement(): HTMLElement {
|
|
return this.view.domNode;
|
|
}
|
|
|
|
private _onFocusChange(): void {
|
|
const focus = this.focus.get();
|
|
|
|
if (focus.length > 0) {
|
|
this.view.domNode.setAttribute('aria-activedescendant', this.view.getElementDomId(focus[0].startRow, focus[0].startColumn));
|
|
} else {
|
|
this.view.domNode.removeAttribute('aria-activedescendant');
|
|
}
|
|
|
|
this.view.domNode.setAttribute('role', 'tree');
|
|
DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0);
|
|
}
|
|
|
|
private _onSelectionChange(): void {
|
|
const selection = this.selection.get();
|
|
|
|
DOM.toggleClass(this.view.domNode, 'selection-none', selection.length === 0);
|
|
DOM.toggleClass(this.view.domNode, 'selection-single', selection.length === 1);
|
|
DOM.toggleClass(this.view.domNode, 'selection-multiple', selection.length > 1);
|
|
}
|
|
|
|
dispose(): void {
|
|
this._onDidDispose.fire();
|
|
this.disposables.dispose();
|
|
|
|
this._onDidDispose.dispose();
|
|
}
|
|
}
|