Skip to content

Commit

Permalink
Add breadcrumbs bar to editor widget
Browse files Browse the repository at this point in the history
This commit adds a breadcrumbs bar to the editor widget. It shows the path to the current file and outline information as breadcrumbs. A click of breadcrumbs allows to jump to other files or to code sections.

Fixes eclipse-theia#5475

Signed-off-by: Cornelius A. Ludmann <[email protected]>
  • Loading branch information
corneliusludmann authored and colin-grant-work committed Sep 1, 2021
1 parent 8929f6e commit 9ee9d91
Show file tree
Hide file tree
Showing 32 changed files with 1,248 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/********************************************************************************
* Copyright (C) 2019 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 WITH Classpath-exception-2.0
********************************************************************************/

import { Disposable, DisposableCollection } from '../../common/disposable';
import { Breadcrumbs } from './breadcrumbs';

/**
* This class creates a popup container at the given position
* so that contributions can attach their HTML elements
* as childs of `BreadcrumbPopupContainer#container`.
*
* - `dispose()` is called on blur or on hit on escape
*/
export class BreadcrumbPopupContainer implements Disposable {

protected toDispose: DisposableCollection = new DisposableCollection();

readonly container: HTMLElement;
public isOpen: boolean;

constructor(
protected readonly parent: HTMLElement,
public readonly breadcrumbId: string,
position: { x: number, y: number }
) {
this.container = this.createPopupDiv(position);
document.addEventListener('keyup', this.escFunction);
this.container.focus();
this.isOpen = true;
}

protected createPopupDiv(position: { x: number, y: number }): HTMLDivElement {
const result = window.document.createElement('div');
result.className = Breadcrumbs.Styles.BREADCRUMB_POPUP;
result.style.left = `${position.x}px`;
result.style.top = `${position.y}px`;
result.tabIndex = 0;
result.onblur = event => this.onBlur(event, this.breadcrumbId);
this.parent.appendChild(result);
return result;
}

protected onBlur = (event: FocusEvent, breadcrumbId: string) => {
if (event.relatedTarget && event.relatedTarget instanceof HTMLElement) {
// event.relatedTarget is the element that has the focus after this popup looses the focus.
// If a breadcrumb was clicked the following holds the breadcrumb ID of the clicked breadcrumb.
const clickedBreadcrumbId = event.relatedTarget.getAttribute('data-breadcrumb-id');
if (clickedBreadcrumbId && clickedBreadcrumbId === breadcrumbId) {
// This is a click on the breadcrumb that has openend this popup.
// We do not close this popup here but let the click event of the breadcrumb handle this instead
// because it needs to know that this popup is open to decide if it just closes this popup or
// also open a new popup.
return;
}
if (this.container.contains(event.relatedTarget)) {
// A child element gets focus. Set the focus to the container again.
// Otherwise the popup would not be closed when elements outside the popup get the focus.
// A popup content should not relay on getting a focus.
this.container.focus();
return;
}
}
this.dispose();
}

protected escFunction = (event: KeyboardEvent) => {
if (event.key === 'Escape' || event.key === 'Esc') {
this.dispose();
}
}

dispose(): void {
this.toDispose.dispose();
if (this.parent.contains(this.container)) {
this.parent.removeChild(this.container);
}
this.isOpen = false;
document.removeEventListener('keyup', this.escFunction);
}

addDisposable(disposable: Disposable | undefined): void {
if (disposable) { this.toDispose.push(disposable); }
}
}
42 changes: 42 additions & 0 deletions packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/********************************************************************************
* Copyright (C) 2019 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 WITH Classpath-exception-2.0
********************************************************************************/

import * as React from 'react';
import { injectable } from 'inversify';
import { Breadcrumb } from './breadcrumb';
import { Breadcrumbs } from './breadcrumbs';

export const BreadcrumbRenderer = Symbol('BreadcrumbRenderer');
export interface BreadcrumbRenderer {
/**
* Renders the given breadcrumb. If `onClick` is given, it is called on breadcrumb click.
*/
render(breadcrumb: Breadcrumb, onClick?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode;
}

@injectable()
export class DefaultBreadcrumbRenderer implements BreadcrumbRenderer {
render(breadcrumb: Breadcrumb, onClick?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode {
return <li key={breadcrumb.id} title={breadcrumb.longLabel}
className={Breadcrumbs.Styles.BREADCRUMB_ITEM + (!onClick ? '' : ' ' + Breadcrumbs.Styles.BREADCRUMB_ITEM_HAS_POPUP)}
onClick={event => onClick && onClick(breadcrumb, event)}
tabIndex={0}
data-breadcrumb-id={breadcrumb.id}
>
{breadcrumb.iconClass && <span className={breadcrumb.iconClass}></span>} <span> {breadcrumb.label}</span>
</li >;
}
}
34 changes: 34 additions & 0 deletions packages/core/src/browser/breadcrumbs/breadcrumb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/********************************************************************************
* Copyright (C) 2019 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 WITH Classpath-exception-2.0
********************************************************************************/

/** A single breadcrumb in the breadcrumbs bar. */
export interface Breadcrumb {

/** An ID of this breadcrumb that should be unique in the breadcrumbs bar. */
readonly id: string

/** The breadcrumb type. Should be the same as the contribution type `BreadcrumbsContribution#type`. */
readonly type: symbol

/** The text that will be rendered as label. */
readonly label: string

/** A longer text that will be used as tooltip text. */
readonly longLabel: string

/** A CSS class for the icon. */
readonly iconClass?: string
}
44 changes: 44 additions & 0 deletions packages/core/src/browser/breadcrumbs/breadcrumbs-contribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/********************************************************************************
* Copyright (C) 2019 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 WITH Classpath-exception-2.0
********************************************************************************/

import URI from '../../common/uri';
import { Breadcrumb } from './breadcrumb';
import { Disposable } from '../../common';

export const BreadcrumbsContribution = Symbol('BreadcrumbsContribution');
export interface BreadcrumbsContribution {

/**
* The breadcrumb type. Breadcrumbs returned by `#computeBreadcrumbs(uri)` should have this as `Breadcrumb#type`.
*/
readonly type: symbol;

/**
* The priority of this breadcrumbs contribution. Contributions with lower priority are rendered first.
*/
readonly priority: number;

/**
* Computes breadcrumbs for a given URI.
*/
computeBreadcrumbs(uri: URI): Promise<Breadcrumb[]>;

/**
* Attaches the breadcrumb popup content for the given breadcrumb as child to the given parent.
* If it returns a Disposable, it is called when the popup closes.
*/
attachPopupContent(breadcrumb: Breadcrumb, parent: HTMLElement): Promise<Disposable | undefined>;
}
Loading

0 comments on commit 9ee9d91

Please sign in to comment.