From 22642333ca798f58610f8bb9edfc8f965872614c Mon Sep 17 00:00:00 2001 From: Paul Cody Johnston Date: Mon, 21 Jan 2019 09:51:15 -0700 Subject: [PATCH] Improve failure component rendering Assorted other changes --- js/ui/BUILD | 1 + js/ui/app.js | 7 ++- js/ui/component.js | 36 ++++++++--- js/ui/eventtype.js | 33 ++++++++++ js/ui/history.js | 12 ++-- js/ui/keyboard.js | 17 +++++- js/ui/menushield.js | 30 ++++++---- js/ui/route.js | 37 ++++++++++++ js/ui/route/event.js | 4 +- js/ui/router.js | 12 ++-- js/ui/select.js | 140 +++++++++++++++++++++++++++++++++++++++---- js/ui/tabs.js | 8 +-- 12 files changed, 287 insertions(+), 50 deletions(-) create mode 100644 js/ui/eventtype.js diff --git a/js/ui/BUILD b/js/ui/BUILD index 15a9835..5165bff 100644 --- a/js/ui/BUILD +++ b/js/ui/BUILD @@ -6,6 +6,7 @@ closure_js_library( name = "ui", srcs = [ "app.js", + "eventtype.js", "history.js", "history/event.js", "select.js", diff --git a/js/ui/app.js b/js/ui/app.js index bd918c7..0dbf7d9 100644 --- a/js/ui/app.js +++ b/js/ui/app.js @@ -1,5 +1,7 @@ goog.module('stack.ui.App'); +const AppEvent = goog.require('stack.ui.app.Event'); +const AppEventType = goog.require('stack.ui.app.EventType'); const BzlHistory = goog.require('stack.ui.History'); const Component = goog.require('stack.ui.Component'); const HEventType = goog.require('goog.history.EventType'); @@ -65,7 +67,7 @@ class App extends Component { * @param {!Component} c * @param {!boolean} b */ - setLoading(c, b) { + setComponentLoading(c, b) { if (b) { console.warn("Starting Loading " + c.getPathUrl()); } else { @@ -142,7 +144,8 @@ class App extends Component { /** @param {!stack.ui.Route} route */ handle404(route) { - this.notifyError("404 (Not Found): " + route.getPath()); + this.dispatchEvent(new AppEvent(AppEventType.NOT_FOUND, route)); + this.notifyError("Not Found: " + route.getPath()); } /** diff --git a/js/ui/component.js b/js/ui/component.js index cd95295..bc67081 100644 --- a/js/ui/component.js +++ b/js/ui/component.js @@ -5,6 +5,7 @@ goog.module('stack.ui.Component'); const BgColorTransform = goog.require('goog.fx.dom.BgColorTransform'); +const EventType = goog.require('goog.ui.Component.EventType'); const GoogUiComponent = goog.require('goog.ui.Component'); const asserts = goog.require('goog.asserts'); const easing = goog.require('goog.fx.easing'); @@ -171,7 +172,25 @@ class Component extends GoogUiComponent { return /** @type {!stack.ui.App} */ (this.getRoot()); } - + /** + * Get a list of menu items that should be displayed for this component. + * + * @return {?Array} + */ + getMenuItems() { + return null; + } + + /** + * Get the list of keyboard shortcuts this component responds to. + * Default is null, no shortcuts. + * + * @return {?Array} + */ + getKeyboardShortcuts() { + return null; + } + /** * Default is no-op. */ @@ -183,22 +202,25 @@ class Component extends GoogUiComponent { */ show() { style.setElementShown(this.getShowHideElement(), true); + this.dispatchEvent(EventType.SHOW); } /** - * Flash this component. + * Hide this component. */ - flash() { - style.setElementShown(this.getShowHideElement(), true); + hide() { + style.setElementShown(this.getShowHideElement(), false); + this.dispatchEvent(EventType.HIDE); } /** - * Hide this component. + * Set the progress state of the component. + * @param {boolean} b */ - hide() { - style.setElementShown(this.getShowHideElement(), false); + setLoading(b) { } + /** * @return {!Element} */ diff --git a/js/ui/eventtype.js b/js/ui/eventtype.js new file mode 100644 index 0000000..85fc2b8 --- /dev/null +++ b/js/ui/eventtype.js @@ -0,0 +1,33 @@ +goog.provide('stack.ui.app.Event'); +goog.provide('stack.ui.app.EventType'); + +goog.require('goog.events.Event'); + +/** + * @public + * @enum {string} + */ +stack.ui.app.EventType = { + NOT_FOUND: 'not-found', +}; + + +/** + * application routing event. + * + * @param {string|!goog.events.EventId} type Event Type. + * @param {!stack.ui.Route} route Reference to the route object that is the target of + * this event. + * @constructor + * @extends {goog.events.Event} + */ +stack.ui.app.Event = function(type, route) { + stack.ui.app.Event.base(this, 'constructor', type, route); + + /** + * @public + * @type {!stack.ui.Route} + */ + this.route = route; +}; +goog.inherits(stack.ui.app.Event, goog.events.Event); \ No newline at end of file diff --git a/js/ui/history.js b/js/ui/history.js index 418e001..9daf296 100644 --- a/js/ui/history.js +++ b/js/ui/history.js @@ -49,14 +49,18 @@ class History extends EventTarget { * @suppress {reportUnknownTypes} */ handleDocumentClick(e) { - //console.log("history; click!"); + console.log("history; click!"); // Element that was clicked could be a child of of the , so // look up through the ancestry chain. let anchor = /** @type {?HTMLAnchorElement} */ ( - dom.getAncestor(asserts.assertElement(e.target), - /** !Element */el => el.tagName === TagName.A.toString(), - true) + dom.getAncestor(asserts.assertElement(e.target), node => { + if (!(node instanceof HTMLElement)) { + return false; + } + const el = /** @type {!HTMLElement} */(node); + return el.tagName === TagName.A.toString(); + }, true) ); if (!anchor) { return; diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js index 4e0110d..69b69ce 100644 --- a/js/ui/keyboard.js +++ b/js/ui/keyboard.js @@ -21,6 +21,14 @@ class Keyboard extends Disposable { this.handleKeyboardShortcut, false, this); } + /** + * Set the enabled status. + * @param {boolean} b + */ + setEnabled(b) { + this.shortcuts_.setAllShortcutsAreGlobal(b); + } + /** * @param {!function(!KeyboardShortcutEvent)} callback * @param {string} identifier Identifier for the task performed by the keyboard @@ -49,6 +57,13 @@ class Keyboard extends Disposable { } } + /** + * Unregisters all shortcuts + */ + removeAll() { + this.shortcuts_.unregisterAll(); + } + /** @override */ disposeInternal() { super.disposeInternal(); @@ -60,4 +75,4 @@ class Keyboard extends Disposable { } -exports = Keyboard; +exports = Keyboard; \ No newline at end of file diff --git a/js/ui/menushield.js b/js/ui/menushield.js index 1af4792..daacf0c 100644 --- a/js/ui/menushield.js +++ b/js/ui/menushield.js @@ -13,10 +13,9 @@ class MenuShield extends Component { /** * @param {!goog.ui.Container} menuBar - * @param {!Map} menuButtons * @param {?dom.DomHelper=} opt_domHelper */ - constructor(menuBar, menuButtons, opt_domHelper) { + constructor(menuBar, opt_domHelper) { super(opt_domHelper); /** @@ -25,16 +24,22 @@ class MenuShield extends Component { */ this.menuBar_ = menuBar; - /** - * A mapping from menu to the parent menubutton. - * - * @const @private - * @type {!Map} - */ - this.menuButtons_ = menuButtons; + // /** + // * A mapping from menu to the parent menubutton. + // * + // * @private + // * @type {?Map} + // */ + // this.menuButtons_ = null; } - + // /** + // * @param {!Map} map + // */ + // setMenuButtonMap(map) { + // this.menuButtons_ = map; + // } + /** * @override */ @@ -78,8 +83,9 @@ class MenuShield extends Component { */ move(menu) { const shield = this.getElementStrict(); - const button = this.menuButtons_.get(menu); - asserts.assert(button); + //const button = this.menuButtons_.get(menu); // JUST get the parent!!! + const button = /** @type {!MenuButton} */(menu.getParent()); + asserts.assert(button, "button not found"); const e = button.getElement(); diff --git a/js/ui/route.js b/js/ui/route.js index f7f8750..c4ccdb6 100644 --- a/js/ui/route.js +++ b/js/ui/route.js @@ -3,6 +3,7 @@ goog.module('stack.ui.Route'); const Event = goog.require('stack.ui.route.Event'); const EventTarget = goog.require('goog.events.EventTarget'); const Promise_ = goog.require('goog.Promise'); +const arrays = goog.require('goog.array'); const asserts = goog.require('goog.asserts'); /** @@ -96,6 +97,42 @@ class Route extends EventTarget { this.path_.push(segment); return this; } + + + /** + * Splice out a path segment into the current position of the route. + * + * @param {number=} opt_index + * @return {!Route} + */ + remove(opt_index) { + arrays.removeAt(this.path_, goog.isNumber(opt_index) ? opt_index : this.index_); + return this; + } + + /** + * Splice a path segment into the current position of the route. + * + * @param {string} segment + * @param {number=} opt_index + * @return {!Route} + */ + insert(segment, opt_index) { + arrays.insertAt(this.path_, segment, goog.isNumber(opt_index) ? opt_index : this.index_); + return this; + } + + /** + * Set the path segment at the given position. + * + * @param {string} segment + * @param {number=} opt_index + * @return {!Route} + */ + set(segment, opt_index) { + this.path_[goog.isNumber(opt_index) ? opt_index : this.index_] = segment; + return this; + } /** * Get the indexed progress component or the last one if not set. diff --git a/js/ui/route/event.js b/js/ui/route/event.js index b32088c..1eae279 100644 --- a/js/ui/route/event.js +++ b/js/ui/route/event.js @@ -15,12 +15,12 @@ class Event extends GEvent { super(type, target); /** - * @type {!stack.ui.Route} + * @public @const @type {!stack.ui.Route} */ this.route = target; /** - * @type {?stack.ui.Component|undefined} + * @public @const @type {?stack.ui.Component|undefined} */ this.component = component; } diff --git a/js/ui/router.js b/js/ui/router.js index d5b9542..ed817c3 100644 --- a/js/ui/router.js +++ b/js/ui/router.js @@ -117,10 +117,10 @@ class Router extends EventTarget { //console.log('go: ' + path); asserts.assertString(path, 'Routing path must be a string'); if (this.route_) { - console.warn(`cannot route to ${path} due to existing route`, this.route_); - return Promise_.reject( - 'Already routing to ' + this.route_.getPath() - ); + console.warn(`override ${path} due to existing route: ` + this.route_.getPath()); + // return Promise_.reject( + // 'Already routing to ' + this.route_.getPath() + // ); } // Remove empty path segments @@ -150,11 +150,13 @@ class Router extends EventTarget { /** @param {!RouteEvent} e */ handleProgress(e) { //console.log(e.target.index() + '. Progress ' + e.target.pathMatched(), e.component); + this.dispatchEvent(e); } /** @param {!RouteEvent} e */ handleDone(e) { //console.log('Done! ' + e.target.matchedPath(), e.component); + this.dispatchEvent(e); this.unlistenRoute(); } @@ -162,12 +164,14 @@ class Router extends EventTarget { handleFail(e) { const target = /** @type {!Route} */(e.target); console.warn('Route Failed: ' + target.getFailReason()); + this.dispatchEvent(e); this.unlistenRoute(); } /** @param {!RouteEvent} e */ handleTimeout(e) { console.warn('Route Timeout', e); + this.dispatchEvent(e); this.unlistenRoute(); } diff --git a/js/ui/select.js b/js/ui/select.js index 33bd69f..6e16ef3 100644 --- a/js/ui/select.js +++ b/js/ui/select.js @@ -5,7 +5,16 @@ goog.module('stack.ui.Select'); const Component = goog.require('stack.ui.Component'); +const ItemEvent = goog.require('goog.ui.ItemEvent'); const asserts = goog.require('goog.asserts'); +const dom = goog.require('goog.dom'); +const soy = goog.require('goog.soy'); + +/** + * @private + * @type {?Function} + */ +let defaultFailTemplate_ = null; /** * Component with routing capability such that only one named child @@ -14,7 +23,7 @@ const asserts = goog.require('goog.asserts'); class Select extends Component { /** - * @param {?goog.dom.DomHelper=} opt_domHelper + * @param {?dom.DomHelper=} opt_domHelper */ constructor(opt_domHelper) { super(opt_domHelper); @@ -63,10 +72,10 @@ class Select extends Component { c.hide(); this.registerDisposable(c); this.prev_ = name; + this.dispatchEvent(new ItemEvent('tab-added', c, null)); return c; } - /** * @param {string} name * @return {?Component} @@ -74,15 +83,22 @@ class Select extends Component { showTab(name) { var tab = this.getTab(name); if (tab) { - this.hideCurrent(); - this.current_ = name; - //var path = this.getPath(); - //path.push(name); - tab.show(); + this.showTabInternal(name, tab); } return tab; } + /** + * @private + * @param {string} name + * @param {!Component} tab + */ + showTabInternal(name, tab) { + this.hideCurrent(); + this.current_ = name; + tab.show(); + } + /** * @param {string} name * @return {?Component} @@ -95,6 +111,14 @@ class Select extends Component { return tab; } + /** + * @param {string} name + * @return {boolean} + */ + hasTab(name) { + return goog.isDefAndNotNull(this.getTab(name)); + } + /** * Get a tab that we don't ever expect to be null. * @param {string} name @@ -117,7 +141,7 @@ class Select extends Component { */ goDown(route) { var name = route.peek(); - //console.log('select.goDown("' + name + '")', this); + //console.log('select.goDown("' + name + '")' + this.getTabNames(), this.name2id_); if (name) { this.select(name, route); } else { @@ -128,7 +152,6 @@ class Select extends Component { } } } - /** * @param {string} name @@ -150,10 +173,64 @@ class Select extends Component { * @param {!stack.ui.Route} route */ selectFail(name, route) { - route.fail(this, 'No tab for ' + name + ' in ' + JSON.stringify(this.name2id_)); - this.getApp().handle404(route); + const message = `No route to "${name}" from /${route.matchedPath().join("/")}`; + if (defaultFailTemplate_) { + this.fail(name, route, defaultFailTemplate_, { + name: name, + matched: route.matchedPath(), + code: 404, + message: message, + }); + return; + } + route.fail(this, message); + const fail = this.getFailTab(); + fail.renderText(message); + this.showTabInternal("__fail__", fail); // TODO: use 'name' here? + } + + /** + * Render a failed component, using the given soy template and data. + * @param {string} name + * @param {!stack.ui.Route} route + * @param {?function(ARG_TYPES, ?Object=):*|?function(ARG_TYPES, null=, ?Object=):*} template The Soy template defining the element's content. + * @param {ARG_TYPES=} opt_templateData The data for the template. + * @param {?Object=} opt_injectedData The injected data for the template. + * @template ARG_TYPES + */ + fail(name, route, template, opt_templateData, opt_injectedData) { + const message = `No route to "${name}" from /${route.matchedPath()}`; + route.fail(this, message); + this.showError(template, opt_templateData, opt_injectedData); } + /** + * Render a failed component, using the given soy template and data. + * @param {?function(ARG_TYPES, ?Object=):*|?function(ARG_TYPES, null=, ?Object=):*} template The Soy template defining the element's content. + * @param {ARG_TYPES=} opt_templateData The data for the template. + * @param {?Object=} opt_injectedData The injected data for the template. + * @template ARG_TYPES + */ + showError(template, opt_templateData, opt_injectedData) { + const fail = this.getFailTab(); + fail.renderTemplate(template, opt_templateData, opt_injectedData); + this.showTabInternal("__fail__", fail); + } + + /** + * Get the special tab for routing failure. + * @private + * @return {!FailComponent} + */ + getFailTab() { + let tab = this.getTab("__fail__"); + if (!tab) { + tab = this.addTab("__fail__", new FailComponent()); + } + return /** @type {!FailComponent} */(tab); + } + + /** * Get the current tab. @@ -188,4 +265,45 @@ class Select extends Component { } +class FailComponent extends Component { + + /** + * @param {?dom.DomHelper=} opt_domHelper + */ + constructor(opt_domHelper) { + super(opt_domHelper); + } + + /** + * Render the given fail message. + * + * @param {?function(ARG_TYPES, ?Object=):*|?function(ARG_TYPES, null=, ?Object=):*} template The Soy template defining the element's content. + * @param {ARG_TYPES=} opt_templateData The data for the template. + * @param {?Object=} opt_injectedData The injected data for the template. + * @template ARG_TYPES + * */ + renderTemplate(template, opt_templateData, opt_injectedData) { + soy.renderElement(this.getElement(), template, opt_templateData, opt_injectedData); + } + + /** + * Render the given fail message + * + * @param {string} message + * */ + renderText(message) { + const el = this.getElementStrict(); + dom.removeChildren(el); + dom.append(el, dom.createTextNode(message)); + } + +} + +/** + * @param {?Function} t + */ +Select.setDefaultFailTemplate = function(t) { + defaultFailTemplate_ = t; +}; + exports = Select; diff --git a/js/ui/tabs.js b/js/ui/tabs.js index a949439..24d1b9d 100644 --- a/js/ui/tabs.js +++ b/js/ui/tabs.js @@ -35,7 +35,6 @@ class Tabs extends Select { this.defaultTabName_ = opt_defaultTabName || ""; } - /** * @override */ @@ -45,7 +44,6 @@ class Tabs extends Select { this.menuElement_ = this.insertMenuElement(); } } - /** * Add the menu element if one was not provided to the constructor. @@ -58,7 +56,6 @@ class Tabs extends Select { return menu; } - /** * @return {!HTMLElement} */ @@ -66,15 +63,13 @@ class Tabs extends Select { return /** @type{!HTMLElement} */(this.getDomHelper().createDom(TagName.DIV, this.getMenuElementClass())); } - /** * @return {string} */ getMenuElementClass() { return 'ui menu'; } - - + /** * @return {?HTMLElement} */ @@ -82,7 +77,6 @@ class Tabs extends Select { return this.menuElement_; } - /** * @override */