Files
catalog/ts_web/views/eco-view-browser/eco-view-browser.ts

960 lines
26 KiB
TypeScript
Raw Normal View History

import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import { DeesIcon } from '@design.estate/dees-catalog';
import { demo } from './eco-view-browser.demo.js';
// Ensure components are registered
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-view-browser': EcoViewBrowser;
}
}
// Types
export interface IBookmark {
id: string;
title: string;
url: string;
favicon?: string;
createdAt: Date;
}
export interface IBrowserState {
url: string;
title: string;
canGoBack: boolean;
canGoForward: boolean;
isLoading: boolean;
}
export type TBrowserPanel = 'browser' | 'bookmarks';
@customElement('eco-view-browser')
export class EcoViewBrowser extends DeesElement {
public static demo = demo;
public static demoGroup = 'Views';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.browser-container {
display: flex;
flex-direction: column;
height: 100%;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
}
.nav-buttons {
display: flex;
gap: 4px;
}
.nav-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.nav-button:hover:not(:disabled) {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.nav-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.nav-button.loading dees-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.url-container {
flex: 1;
display: flex;
align-items: center;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
padding: 0 12px;
height: 36px;
transition: border-color 0.15s ease;
}
.url-container:focus-within {
border-color: hsl(217 91% 60%);
}
.url-icon {
margin-right: 8px;
opacity: 0.5;
}
.url-input {
flex: 1;
border: none;
background: transparent;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
outline: none;
}
.url-input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
}
.bookmark-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.bookmark-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
}
.bookmark-button.bookmarked {
color: hsl(45 93% 47%);
}
.bookmark-button.bookmarked:hover {
color: hsl(45 93% 40%);
}
.menu-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.menu-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
}
.webview-container {
flex: 1;
position: relative;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 5%)')};
overflow: hidden;
}
.webview-wrapper {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
webview {
width: 100%;
height: 100%;
border: none;
display: flex;
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
gap: 16px;
}
.placeholder dees-icon {
opacity: 0.3;
}
.placeholder-text {
font-size: 14px;
}
.placeholder-hint {
font-size: 12px;
opacity: 0.7;
}
.bookmarks-bar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
background: ${cssManager.bdTheme('#fafafa', 'hsl(240 6% 9%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 13%)')};
overflow-x: auto;
}
.bookmarks-bar::-webkit-scrollbar {
height: 4px;
}
.bookmarks-bar::-webkit-scrollbar-track {
background: transparent;
}
.bookmarks-bar::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')};
border-radius: 2px;
}
.bookmark-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
border-radius: 6px;
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
}
.bookmark-chip:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 18%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')};
}
.bookmark-chip .favicon {
width: 14px;
height: 14px;
border-radius: 2px;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
box-shadow: 0 4px 16px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
min-width: 180px;
z-index: 100;
overflow: hidden;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
cursor: pointer;
transition: background 0.1s ease;
}
.dropdown-item:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')};
}
.dropdown-item.destructive {
color: hsl(0 72% 50%);
}
.dropdown-item.destructive:hover {
background: hsl(0 72% 95%);
}
.dropdown-divider {
height: 1px;
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
margin: 4px 0;
}
.menu-container {
position: relative;
}
/* Bookmarks panel styles */
.bookmarks-panel {
padding: 32px 48px;
overflow-y: auto;
height: 100%;
}
.panel-header {
margin-bottom: 24px;
}
.panel-title {
font-size: 28px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
margin: 0 0 8px 0;
}
.panel-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
margin: 0;
}
.bookmarks-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.bookmark-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
}
.bookmark-item:hover {
border-color: hsl(217 91% 60%);
transform: translateX(2px);
}
.bookmark-item .favicon {
width: 20px;
height: 20px;
border-radius: 4px;
flex-shrink: 0;
}
.bookmark-info {
flex: 1;
min-width: 0;
}
.bookmark-title {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bookmark-url {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bookmark-delete {
opacity: 0;
transition: opacity 0.15s ease;
}
.bookmark-item:hover .bookmark-delete {
opacity: 1;
}
.bookmark-delete-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
cursor: pointer;
}
.bookmark-delete-btn:hover {
background: hsl(0 72% 95%);
color: hsl(0 72% 50%);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.empty-state dees-icon {
margin-bottom: 16px;
opacity: 0.4;
}
.empty-state p {
font-size: 14px;
margin: 0;
}
.loading-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg,
hsl(217 91% 60%) 0%,
hsl(217 91% 70%) 50%,
hsl(217 91% 60%) 100%
);
background-size: 200% 100%;
animation: loading 1.5s linear infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`,
];
@property({ type: Array })
accessor bookmarks: IBookmark[] = [];
@property({ type: Boolean })
accessor showBookmarksBar = true;
@property({ type: String })
accessor defaultUrl = '';
@state()
accessor activePanel: TBrowserPanel = 'browser';
@state()
accessor currentUrl = '';
@state()
accessor inputUrl = '';
@state()
accessor pageTitle = '';
@state()
accessor canGoBack = false;
@state()
accessor canGoForward = false;
@state()
accessor isLoading = false;
@state()
accessor showMenu = false;
private isElectron = false;
private webviewEventsBound = false;
async connectedCallback(): Promise<void> {
await super.connectedCallback();
// Check if we're in Electron - webview tag only works in Electron
// Since nodeIntegration is disabled, we check for the existence of electronAPI
// or if we're in a file:// protocol (Electron loads from file://)
this.isElectron = typeof window !== 'undefined' && (
typeof (window as any).electronAPI !== 'undefined' ||
window.location.protocol === 'file:' ||
navigator.userAgent.includes('Electron')
);
if (this.defaultUrl) {
this.inputUrl = this.defaultUrl;
}
}
protected updated(changedProperties: Map<string, any>): void {
super.updated(changedProperties);
// Bind webview events after it's rendered
this.bindWebviewEvents();
}
private bindWebviewEvents(): void {
if (!this.isElectron || this.webviewEventsBound) return;
const webview = this.shadowRoot?.querySelector('webview') as any;
if (!webview) return;
this.webviewEventsBound = true;
webview.addEventListener('did-start-loading', () => {
this.isLoading = true;
});
webview.addEventListener('did-stop-loading', () => {
this.isLoading = false;
this.updateNavigationState();
});
webview.addEventListener('did-navigate', (e: any) => {
this.currentUrl = e.url || this.currentUrl;
this.inputUrl = this.currentUrl;
this.updateNavigationState();
});
webview.addEventListener('did-navigate-in-page', (e: any) => {
if (e.isMainFrame) {
this.currentUrl = e.url || this.currentUrl;
this.inputUrl = this.currentUrl;
}
});
webview.addEventListener('page-title-updated', (e: any) => {
this.pageTitle = e.title || '';
});
webview.addEventListener('page-favicon-updated', (e: any) => {
// Could store favicon for bookmarks
});
webview.addEventListener('dom-ready', () => {
this.updateNavigationState();
});
console.log('Webview events bound');
}
public render(): TemplateResult {
return html`
<div class="browser-container">
${this.renderToolbar()}
${this.showBookmarksBar && this.bookmarks.length > 0 ? this.renderBookmarksBar() : ''}
${this.activePanel === 'browser' ? this.renderBrowserContent() : this.renderBookmarksPanel()}
</div>
`;
}
private renderToolbar(): TemplateResult {
return html`
<div class="toolbar">
<div class="nav-buttons">
<button
class="nav-button"
title="Back"
?disabled=${!this.canGoBack}
@click=${this.handleBack}
>
<dees-icon .icon=${'lucide:arrowLeft'} .iconSize=${18}></dees-icon>
</button>
<button
class="nav-button"
title="Forward"
?disabled=${!this.canGoForward}
@click=${this.handleForward}
>
<dees-icon .icon=${'lucide:arrowRight'} .iconSize=${18}></dees-icon>
</button>
<button
class="nav-button ${this.isLoading ? 'loading' : ''}"
title=${this.isLoading ? 'Stop' : 'Refresh'}
@click=${this.isLoading ? this.handleStop : this.handleRefresh}
>
<dees-icon
.icon=${this.isLoading ? 'lucide:loader' : 'lucide:refreshCw'}
.iconSize=${18}
></dees-icon>
</button>
</div>
<div class="url-container">
<dees-icon class="url-icon" .icon=${'lucide:globe'} .iconSize=${16}></dees-icon>
<input
class="url-input"
type="text"
placeholder="Enter URL or search..."
.value=${this.inputUrl}
@input=${(e: InputEvent) => this.inputUrl = (e.target as HTMLInputElement).value}
@keydown=${this.handleUrlKeydown}
/>
</div>
<button
class="bookmark-button ${this.isCurrentPageBookmarked() ? 'bookmarked' : ''}"
title=${this.isCurrentPageBookmarked() ? 'Remove bookmark' : 'Add bookmark'}
@click=${this.toggleBookmark}
?disabled=${!this.currentUrl}
>
<dees-icon
.icon=${this.isCurrentPageBookmarked() ? 'lucide:star' : 'lucide:star'}
.iconSize=${18}
></dees-icon>
</button>
<div class="menu-container">
<button
class="menu-button"
title="Menu"
@click=${() => this.showMenu = !this.showMenu}
>
<dees-icon .icon=${'lucide:moreVertical'} .iconSize=${18}></dees-icon>
</button>
${this.showMenu ? this.renderMenu() : ''}
</div>
</div>
`;
}
private renderMenu(): TemplateResult {
return html`
<div class="dropdown-menu" @mouseleave=${() => this.showMenu = false}>
<div class="dropdown-item" @click=${() => { this.activePanel = 'bookmarks'; this.showMenu = false; }}>
<dees-icon .icon=${'lucide:bookmark'} .iconSize=${18}></dees-icon>
Bookmarks
</div>
<div class="dropdown-item" @click=${this.handleNewTab}>
<dees-icon .icon=${'lucide:plus'} .iconSize=${18}></dees-icon>
New Tab
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item" @click=${this.handleOpenDevTools}>
<dees-icon .icon=${'lucide:code'} .iconSize=${18}></dees-icon>
Developer Tools
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item" @click=${this.handleCopyUrl}>
<dees-icon .icon=${'lucide:copy'} .iconSize=${18}></dees-icon>
Copy URL
</div>
</div>
`;
}
private renderBookmarksBar(): TemplateResult {
return html`
<div class="bookmarks-bar">
${this.bookmarks.slice(0, 10).map(bookmark => html`
<div
class="bookmark-chip"
title=${bookmark.url}
@click=${() => this.navigate(bookmark.url)}
>
${bookmark.favicon
? html`<img class="favicon" src=${bookmark.favicon} alt="" />`
: html`<dees-icon .icon=${'lucide:globe'} .iconSize=${14}></dees-icon>`
}
${bookmark.title || new URL(bookmark.url).hostname}
</div>
`)}
</div>
`;
}
private renderBrowserContent(): TemplateResult {
return html`
<div class="webview-container">
${this.isLoading ? html`<div class="loading-bar"></div>` : ''}
<div class="webview-wrapper">
${this.currentUrl ? this.renderWebview() : this.renderPlaceholder()}
</div>
</div>
`;
}
private renderWebview(): TemplateResult {
// In Electron, we use webview tag. In browser, we show a notice.
if (this.isElectron) {
// Reset event binding flag when URL changes so events get rebound
// Note: webview events are bound in updated() lifecycle
return html`
<webview
src=${this.currentUrl}
style="width: 100%; height: 100%;"
allowpopups
></webview>
`;
} else {
// Fallback for non-Electron environment (demo/testing)
return html`
<div class="placeholder">
<dees-icon .icon=${'lucide:monitor'} .iconSize=${48}></dees-icon>
<span class="placeholder-text">Webview requires Electron environment</span>
<span class="placeholder-hint">URL: ${this.currentUrl}</span>
</div>
`;
}
}
private renderPlaceholder(): TemplateResult {
return html`
<div class="placeholder">
<dees-icon .icon=${'lucide:globe'} .iconSize=${64}></dees-icon>
<span class="placeholder-text">Enter a URL to start browsing</span>
<span class="placeholder-hint">or select a bookmark</span>
</div>
`;
}
private renderBookmarksPanel(): TemplateResult {
return html`
<div class="bookmarks-panel">
<div class="panel-header">
<h2 class="panel-title">Bookmarks</h2>
<p class="panel-description">Your saved websites</p>
</div>
${this.bookmarks.length > 0
? html`
<div class="bookmarks-list">
${this.bookmarks.map(bookmark => html`
<div class="bookmark-item" @click=${() => this.handleBookmarkClick(bookmark)}>
${bookmark.favicon
? html`<img class="favicon" src=${bookmark.favicon} alt="" />`
: html`<dees-icon .icon=${'lucide:globe'} .iconSize=${20}></dees-icon>`
}
<div class="bookmark-info">
<div class="bookmark-title">${bookmark.title || bookmark.url}</div>
<div class="bookmark-url">${bookmark.url}</div>
</div>
<div class="bookmark-delete">
<button
class="bookmark-delete-btn"
title="Delete bookmark"
@click=${(e: Event) => { e.stopPropagation(); this.removeBookmark(bookmark.id); }}
>
<dees-icon .icon=${'lucide:trash2'} .iconSize=${16}></dees-icon>
</button>
</div>
</div>
`)}
</div>
`
: html`
<div class="empty-state">
<dees-icon .icon=${'lucide:bookmark'} .iconSize=${48}></dees-icon>
<p>No bookmarks yet. Star a page to save it here.</p>
</div>
`
}
</div>
`;
}
// Navigation methods
private navigate(url: string): void {
if (!url) return;
// Add protocol if missing
if (!/^https?:\/\//i.test(url)) {
// Check if it looks like a domain
if (/^[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}/.test(url)) {
url = 'https://' + url;
} else {
// Treat as search query
url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
}
this.currentUrl = url;
this.inputUrl = url;
this.isLoading = true;
this.dispatchEvent(new CustomEvent('navigate', {
detail: { url },
bubbles: true,
composed: true,
}));
}
private handleUrlKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter') {
this.navigate(this.inputUrl);
}
}
private get webview(): any {
return this.shadowRoot?.querySelector('webview');
}
private handleBack(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.goBack();
}
this.dispatchEvent(new CustomEvent('browser-back', { bubbles: true, composed: true }));
}
private handleForward(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.goForward();
}
this.dispatchEvent(new CustomEvent('browser-forward', { bubbles: true, composed: true }));
}
private handleRefresh(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.reload();
}
this.dispatchEvent(new CustomEvent('browser-refresh', { bubbles: true, composed: true }));
}
private handleStop(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.stop();
}
this.isLoading = false;
this.dispatchEvent(new CustomEvent('browser-stop', { bubbles: true, composed: true }));
}
private handleFaviconUpdate(e: any): void {
// Could update current page favicon for bookmarks
}
private updateNavigationState(): void {
if (this.webview && this.isElectron) {
this.canGoBack = (this.webview as any).canGoBack?.() || false;
this.canGoForward = (this.webview as any).canGoForward?.() || false;
}
}
// Bookmark methods
private isCurrentPageBookmarked(): boolean {
return this.bookmarks.some(b => b.url === this.currentUrl);
}
private toggleBookmark(): void {
if (!this.currentUrl) return;
if (this.isCurrentPageBookmarked()) {
const bookmark = this.bookmarks.find(b => b.url === this.currentUrl);
if (bookmark) {
this.removeBookmark(bookmark.id);
}
} else {
this.addBookmark();
}
}
private addBookmark(): void {
const bookmark: IBookmark = {
id: crypto.randomUUID(),
title: this.pageTitle || this.currentUrl,
url: this.currentUrl,
createdAt: new Date(),
};
this.dispatchEvent(new CustomEvent('bookmark-add', {
detail: { bookmark },
bubbles: true,
composed: true,
}));
}
private removeBookmark(id: string): void {
this.dispatchEvent(new CustomEvent('bookmark-remove', {
detail: { id },
bubbles: true,
composed: true,
}));
}
private handleBookmarkClick(bookmark: IBookmark): void {
this.navigate(bookmark.url);
this.activePanel = 'browser';
}
// Menu actions
private handleNewTab(): void {
this.showMenu = false;
this.dispatchEvent(new CustomEvent('new-tab', { bubbles: true, composed: true }));
}
private handleOpenDevTools(): void {
this.showMenu = false;
if (this.webview && this.isElectron) {
(this.webview as any).openDevTools();
}
this.dispatchEvent(new CustomEvent('open-devtools', { bubbles: true, composed: true }));
}
private handleCopyUrl(): void {
this.showMenu = false;
if (this.currentUrl) {
navigator.clipboard.writeText(this.currentUrl);
}
}
// Public API
public goTo(url: string): void {
this.navigate(url);
}
public setBookmarks(bookmarks: IBookmark[]): void {
this.bookmarks = bookmarks;
}
public getState(): IBrowserState {
return {
url: this.currentUrl,
title: this.pageTitle,
canGoBack: this.canGoBack,
canGoForward: this.canGoForward,
isLoading: this.isLoading,
};
}
}