diff --git a/cmd/heapview/client/main.ts b/cmd/heapview/client/main.ts index 045e0692..fe3985c1 100644 --- a/cmd/heapview/client/main.ts +++ b/cmd/heapview/client/main.ts @@ -3,47 +3,193 @@ // license that can be found in the LICENSE file. /** - * A hamburger menu element. + * An enum of types of actions that might be requested + * by the app. */ -class HamburgerElement extends HTMLElement { - attachedCallback() { - this.innerHTML = '☰'; // Unicode character for hamburger menu. +enum Action { + TOGGLE_SIDEBAR, // Toggle the sidebar. + NAVIGATE_ABOUT, // Go to the about page. +} + +const TITLE = 'Go Heap Viewer'; + +/** + * A type of event that signals to the AppElement controller + * that something shoud be done. For the most part, the structure + * of the app will be that elements' state will mostly be controlled + * by parent elements. Elements will issue actions that the AppElement + * will handle, and the app will be re-rendered down the DOM + * hierarchy. + */ +class ActionEvent extends Event { + static readonly EVENT_TYPE = 'action-event' + constructor(public readonly action: Action) { super(ActionEvent.EVENT_TYPE); } +} + +/** + * A hamburger menu element. Triggers a TOGGLE_SIDE action to toggle the + * sidebar. + */ +export class HamburgerElement extends HTMLElement { + static readonly NAME = 'heap-hamburger'; + + createdCallback() { + this.appendChild(document.createTextNode('☰')); + this.onclick = + () => { this.dispatchEvent(new ActionEvent(Action.TOGGLE_SIDEBAR)) }; } } -document.registerElement('heap-hamburger', HamburgerElement); +document.registerElement(HamburgerElement.NAME, HamburgerElement); /** * A heading for the page with a hamburger menu and a title. */ export class HeadingElement extends HTMLElement { - attachedCallback() { + static readonly NAME = 'heap-heading'; + + createdCallback() { this.style.display = 'block'; this.style.backgroundColor = '#2196F3'; this.style.webkitUserSelect = 'none'; this.style.cursor = 'default'; this.style.color = '#FFFFFF'; this.style.padding = '10px'; - this.innerHTML = ` -
Go Heap Viewer
- `; + + const div = document.createElement('div'); + div.style.margin = '0px'; + div.style.fontSize = '2em'; + div.appendChild(document.createElement(HamburgerElement.NAME)); + div.appendChild(document.createTextNode(' ' + TITLE)); + this.appendChild(div); } } -document.registerElement('heap-heading', HeadingElement); +document.registerElement(HeadingElement.NAME, HeadingElement); /** - * Reset body's margin and padding, and set font. + * A sidebar that has navigation for the app. */ -function clearStyle() { - document.head.innerHTML += ` - - `; +export class SidebarElement extends HTMLElement { + static readonly NAME = 'heap-sidebar'; + + createdCallback() { + this.style.display = 'none'; + this.style.backgroundColor = '#9E9E9E'; + this.style.width = '15em'; + + const aboutButton = document.createElement('button'); + aboutButton.innerText = 'about'; + aboutButton.onclick = + () => { this.dispatchEvent(new ActionEvent(Action.NAVIGATE_ABOUT)) }; + this.appendChild(aboutButton); + } + + toggle() { + this.style.display = this.style.display === 'none' ? 'block' : 'none'; + } +} +document.registerElement(SidebarElement.NAME, SidebarElement); + +/** + * A Container for the main content in the app. + * TODO(matloob): Implement main content. + */ +export class MainContentElement extends HTMLElement { + static readonly NAME = 'heap-container'; + + attachedCallback() { + this.style.backgroundColor = '#E0E0E0'; + this.style.height = '100%'; + this.style.flex = '1'; + } +} +document.registerElement(MainContentElement.NAME, MainContentElement); + +/** + * A container and controller for the whole app. + * Contains the heading, side drawer and main panel. + */ +class AppElement extends HTMLElement { + static readonly NAME = 'heap-app'; + private sidebar: SidebarElement; + private mainContent: MainContentElement; + + attachedCallback() { + document.title = TITLE; + + this.addEventListener( + ActionEvent.EVENT_TYPE, e => this.handleAction(e as ActionEvent), + /* capture */ true); + + this.render(); + } + + render() { + this.style.display = 'block'; + this.style.height = '100vh'; + this.style.width = '100vw'; + this.appendChild(document.createElement(HeadingElement.NAME)); + + const bodyDiv = document.createElement('div'); + bodyDiv.style.height = '100%'; + bodyDiv.style.display = 'flex'; + this.sidebar = + document.createElement(SidebarElement.NAME) as SidebarElement; + bodyDiv.appendChild(this.sidebar); + this.mainContent = + document.createElement(MainContentElement.NAME) as MainContentElement; + bodyDiv.appendChild(this.mainContent); + this.appendChild(bodyDiv); + + this.renderRoute(); + } + + renderRoute() { + this.mainContent.innerHTML = '' + switch (window.location.pathname) { + case '/about': + this.mainContent.appendChild( + document.createElement(AboutPageElement.NAME)); + break; + } + } + + handleAction(event: ActionEvent) { + switch (event.action) { + case Action.TOGGLE_SIDEBAR: + this.sidebar.toggle(); + break; + case Action.NAVIGATE_ABOUT: + window.history.pushState({}, '', '/about'); + this.renderRoute(); + break; + } + } +} +document.registerElement(AppElement.NAME, AppElement); + +/** + * An about page. + */ +class AboutPageElement extends HTMLElement { + static readonly NAME = 'heap-about'; + + createdCallback() { this.textContent = TITLE; } +} +document.registerElement(AboutPageElement.NAME, AboutPageElement); + +/** + * Resets body's margin and padding, and sets font. + */ +function clearStyle(document: Document) { + const styleElement = document.createElement('style') as HTMLStyleElement; + document.head.appendChild(styleElement); + const styleSheet = styleElement.sheet as CSSStyleSheet; + styleSheet.insertRule( + '* {font-family: Roboto,Helvetica; box-sizing: border-box}', 0); + styleSheet.insertRule('body {margin: 0px; padding:0px}', 0); } export function main() { - document.title = 'Go Heap Viewer'; - clearStyle(); - document.body.appendChild(document.createElement("heap-heading")); + clearStyle(document); + document.body.appendChild(document.createElement(AppElement.NAME)); } diff --git a/cmd/heapview/client/main_test.ts b/cmd/heapview/client/main_test.ts index 93da4e5c..a2086538 100644 --- a/cmd/heapview/client/main_test.ts +++ b/cmd/heapview/client/main_test.ts @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -import {main} from './main'; +import {HamburgerElement, HeadingElement, SidebarElement, main} from './main'; describe('main', () => { it('sets the document\'s title', () => { @@ -12,6 +12,18 @@ describe('main', () => { it('has a heading', () => { main(); - expect(document.querySelector('heap-heading')).toBeDefined(); + expect(document.querySelector(HeadingElement.NAME)).toBeDefined(); }); + + it('has a sidebar', () => { + main(); + const hamburger = document.querySelector(HamburgerElement.NAME); + const sidebar = + document.querySelector(SidebarElement.NAME) as SidebarElement; + expect(sidebar.style.display).toBe('none'); + + // Click on the hamburger. Sidebar should then be visible. + hamburger.dispatchEvent(new Event('click')); + expect(sidebar.style.display).toBe('block'); + }) }); \ No newline at end of file diff --git a/cmd/heapview/client/tsconfig.json b/cmd/heapview/client/tsconfig.json index 5ce449bf..14c38a1b 100644 --- a/cmd/heapview/client/tsconfig.json +++ b/cmd/heapview/client/tsconfig.json @@ -10,6 +10,7 @@ { "compilerOptions": { "noEmit": true, - "strictNullChecks": true + "strictNullChecks": true, + "target": "es2015" } } diff --git a/cmd/heapview/main.go b/cmd/heapview/main.go index 8d519c2f..5fab790a 100644 --- a/cmd/heapview/main.go +++ b/cmd/heapview/main.go @@ -15,6 +15,7 @@ import ( "path/filepath" ) +var host = flag.String("host", "", "host addr to listen on") var port = flag.Int("port", 8080, "service port") var index = ` @@ -68,12 +69,12 @@ var addHandlers = func() { }) } -var listenAndServe = func() { - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)) +var listenAndServe = func() error { + return http.ListenAndServe(fmt.Sprintf("%s:%d", *host, *port), nil) } func main() { parseFlags() addHandlers() - listenAndServe() + log.Fatal(listenAndServe()) }