[gtv-resources] r49 committed - Maintenance update.

1 view
Skip to first unread message

gtv-re...@googlecode.com

unread,
Apr 26, 2011, 5:45:24 AM4/26/11
to google-tv-w...@googlegroups.com
Revision: 49
Author: wiktor...@google.com
Date: Tue Apr 26 02:44:02 2011
Log: Maintenance update.


http://code.google.com/p/gtv-resources/source/detail?r=49

Added:
/trunk/gtv-closure-demo/static/tv-ui/scroll_pane.css
/trunk/gtv-closure-demo/static/tv-ui/scroll_pane.js
/trunk/gtv-closure-demo/static/tv-ui/sub_menu.js
Modified:
/trunk/gtv-closure-demo/main.py
/trunk/gtv-closure-demo/static/tv-ui/button.js
/trunk/gtv-closure-demo/static/tv-ui/component.js
/trunk/gtv-closure-demo/static/tv-ui/container.js
/trunk/gtv-closure-demo/static/tv-ui/decorate_handler.js
/trunk/gtv-closure-demo/static/tv-ui/deps.js
/trunk/gtv-closure-demo/static/tv-ui/document.js
/trunk/gtv-closure-demo/static/tv-ui/grid.js
/trunk/gtv-closure-demo/static/tv-ui/input.js
/trunk/gtv-closure-demo/static/tv-ui/lightbox.css
/trunk/gtv-closure-demo/static/tv-ui/lightbox.js
/trunk/gtv-closure-demo/static/tv-ui/link.js
/trunk/gtv-closure-demo/static/tv-ui/menu.js
/trunk/gtv-closure-demo/static/tv-ui/tab_container.js
/trunk/gtv-closure-demo/static/tv-ui/toggle_button.js
/trunk/gtv-closure-demo/static/tv-ui/ui.js
/trunk/gtv-closure-demo/templates/ajax.html
/trunk/gtv-closure-demo/templates/lightbox.html
/trunk/gtv-closure-demo/templates/main.html
/trunk/gtv-closure-demo/templates/menu.html

=======================================
--- /dev/null
+++ /trunk/gtv-closure-demo/static/tv-ui/scroll_pane.css Tue Apr 26
02:44:02 2011
@@ -0,0 +1,11 @@
+/* Required styles for tv.ui.ScrollPane in order to work correctly. */
+
+.tv-scroll-pane-viewport,
+.tv-scroll-pane-scrollbar {
+ position: relative;
+}
+
+.tv-scroll-pane-content,
+.tv-scroll-pane-scrollbar-thumb {
+ position: absolute;
+}
=======================================
--- /dev/null
+++ /trunk/gtv-closure-demo/static/tv-ui/scroll_pane.js Tue Apr 26 02:44:02
2011
@@ -0,0 +1,355 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Scroll pane.
+ *
+ * Component that enables scrolling of an given element with provided
next/prev
+ * buttons.
+ *
+ * Vertical scroll pane means the top property of content element will
+ * change for scrolling. Horizontal scroll changes left property.
+ *
+ * Expected HTML structure:
+ * <div class="tv-scroll-pane-vertical tv-container-vertical">
+ * <div class="tv-button tv-scroll-pane-prev">Prev</div>
+ * <div class="tv-button tv-scroll-pane-next">Next</div>
+ *
+ * <div class="tv-scroll-pane-viewport">
+ * <div class="tv-scroll-pane-content">
+ * Lots of text to be scrolled inside limited width or height.
+ * </div>
+ * </div>
+ *
+ * <div class="tv-scroll-pane-scrollbar">
+ * <div class="tv-scroll-pane-scrollbar-thumb"></div>
+ * </div>
+ * </div>
+ *
+ */
+goog.provide('tv.ui.ScrollPane');
+
+goog.require('goog.asserts');
+goog.require('goog.dom.classes');
+goog.require('goog.style');
+goog.require('tv.ui');
+goog.require('tv.ui.Button')
+goog.require('tv.ui.Container');
+
+/**
+ * Constructs scroll pane.
+ * @constructor
+ * @extends {tv.ui.Container}
+ */
+tv.ui.ScrollPane = function() {
+ goog.base(this);
+};
+goog.inherits(tv.ui.ScrollPane, tv.ui.Container);
+
+/**
+ * @type {string} Main CSS class that triggers decoration.
+ */
+tv.ui.ScrollPane.CLASS = 'tv-scroll-pane';
+
+/**
+ * CSS classes that controls the scroll pane.
+ * @enum {string}
+ */
+tv.ui.ScrollPane.Class = {
+ /**
+ * Applied to root element if scroll pane has horizontal orientation.
+ * NEXT and PREV buttons will control left property of content element.
+ * @see #hasHorizontalScroll
+ */
+ HORIZONTAL: 'tv-scroll-pane-horizontal',
+
+ /**
+ * Applied to root element if scroll pane has vertical orientation.
+ * NEXT and PREV buttons will control top property of content element.
+ */
+ VERTICAL: 'tv-scroll-pane-vertical',
+
+ /**
+ * Applied to root element if content element fits into viewport and no
+ * scrolling is needed.
+ */
+ CONTENT_FITS_VIEWPORT: 'tv-scroll-pane-content-fits-viewport',
+
+ /**
+ * Denotes next button component. It's expected that this is
tv.ui.Button.
+ */
+ NEXT: 'tv-scroll-pane-next',
+
+ /**
+ * Denotes prev button component. It's expected that this is
tv.ui.Button.
+ */
+ PREV: 'tv-scroll-pane-prev',
+
+ /**
+ * Denotes scroll window. It's expected that CONTENT element is child
+ * of viewport.
+ */
+ VIEWPORT: 'tv-scroll-pane-viewport',
+
+ /**
+ * Denotes content element which will be scrolled.
+ */
+ CONTENT: 'tv-scroll-pane-content',
+
+ /**
+ * Denotes scrollbar element.
+ */
+ SCROLLBAR: 'tv-scroll-pane-scrollbar',
+
+ /**
+ * Denotes scroll thumb. It's expected to be child of SCROLLBAR.
+ */
+ SCROLL_THUMB: 'tv-scroll-pane-scrollbar-thumb'
+};
+
+tv.ui.registerDecorator(
+ tv.ui.ScrollPane,
+ [
+ tv.ui.ScrollPane.CLASS,
+ tv.ui.ScrollPane.Class.HORIZONTAL,
+ tv.ui.ScrollPane.Class.VERTICAL
+ ]);
+
+/**
+ * Viewport of content.
+ * @type {Element}
+ */
+tv.ui.ScrollPane.prototype.viewportElement_;
+
+/**
+ * Content element which is being scrolled.
+ * @type {Element}
+ */
+tv.ui.ScrollPane.prototype.contentElement_;
+
+/**
+ * Scrollbar track element.
+ * @type {Element}
+ */
+tv.ui.ScrollPane.prototype.scrollbarElement_;
+
+/**
+ * Scrollbar thumb element. Height (for horizontal scroll) or width
+ * (for vertical) will be adjusted to reflect content / viewport size
ratio.
+ * @type {Element}
+ */
+tv.ui.ScrollPane.prototype.scrollThumbElement_;
+
+/**
+ * Current scroll offset of content inside viewport.
+ * @type {number}
+ */
+tv.ui.ScrollPane.prototype.scrollOffset_ = 0;
+
+/**
+ * How many pixels is content element going to be scrolled by.
+ * @type {number}
+ */
+tv.ui.ScrollPane.prototype.scrollDelta_ = 40;
+
+/**
+ * Size of viewport (width for horizontal scroll, height for vertical).
+ * @type {number}
+ */
+tv.ui.ScrollPane.prototype.viewportSize_;
+
+/**
+ * Size of content (width for horizontal scroll, height for vertical).
+ * @type {number}
+ */
+tv.ui.ScrollPane.prototype.contentSize_;
+
+/**
+ * Ratio between viewport size and content size.
+ * @type {number}
+ */
+tv.ui.ScrollPane.prototype.scrollbarRatio_;
+
+/**
+ * Ratio between content size and scrollbar size.
+ * @type {number}
+ */
+tv.ui.ScrollPane.prototype.contentRatio_;
+
+
+/**
+ * @inheritDoc
+ */
+tv.ui.Container.prototype.disposeInternal = function() {
+ delete this.contentElement_;
+ delete this.viewportElement_;
+ delete this.scrollElement_;
+ delete this.scrollThumbElement_;
+
+ goog.base(this, 'disposeInternal');
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.ScrollPane.prototype.getClass = function() {
+ return tv.ui.ScrollPane.CLASS;
+};
+
+/**
+ * Sets new scroll delta.
+ * @param {number} newDelta New scroll delta.
+ */
+tv.ui.ScrollPane.prototype.setScrollDelta = function(newDelta) {
+ this.scrollDelta_ = newDelta;
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.ScrollPane.prototype.decorate = function(element) {
+ goog.base(this, 'decorate', element);
+
+ for (var childNode = element.firstChild; childNode;
+ childNode = childNode.nextSibling) {
+ if (childNode.nodeType != goog.dom.NodeType.ELEMENT) {
+ continue;
+ }
+
+ var childEl = /** @type {Element} */(childNode);
+ if (goog.dom.classes.has(childEl, tv.ui.ScrollPane.Class.VIEWPORT)) {
+ this.viewportElement_ = childEl;
+ this.contentElement_ = goog.dom.getElementByClass(
+ tv.ui.ScrollPane.Class.CONTENT, childEl);
+ } else if (goog.dom.classes.has(childEl,
+ tv.ui.ScrollPane.Class.SCROLLBAR)) {
+ this.scrollbarElement_ = childEl;
+ this.scrollThumbElement_ = goog.dom.getElementByClass(
+ tv.ui.ScrollPane.Class.SCROLL_THUMB, childEl);
+ }
+ }
+
+ goog.asserts.assert(!!this.viewportElement_, 'No viewport element.');
+ goog.asserts.assert(!!this.contentElement_, 'No content element.')
+ goog.asserts.assert(!!this.scrollbarElement_, 'No scrollbar element.');
+ goog.asserts.assert(!!this.scrollThumbElement_, 'No scroll thumb
element');
+
+ this.scheduleRender();
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.ScrollPane.prototype.addChild = function(child) {
+ goog.base(this, 'addChild', child);
+
+ goog.asserts.assert(child instanceof tv.ui.Button,
+ 'Child components of tv.ui.ScrollPane must be tv.ui.Buttons');
+
+ if (goog.dom.classes.has(child.getElement(),
+ tv.ui.ScrollPane.Class.NEXT)) {
+ this.getEventHandler().listen(
+ child,
+ tv.ui.Button.EventType.ACTION,
+ goog.bind(this.scrollBy_, this, this.scrollDelta_));
+ } else if (goog.dom.classes.has(child.getElement(),
+ tv.ui.ScrollPane.Class.PREV)) {
+ this.getEventHandler().listen(
+ child,
+ tv.ui.Button.EventType.ACTION,
+ goog.bind(this.scrollBy_, this, -this.scrollDelta_));
+ }
+};
+
+/**
+ * @return {boolean} Whether the scroll pane is horizontal.
+ */
+tv.ui.ScrollPane.prototype.hasHorizontalScroll = function() {
+ return goog.dom.classes.has(
+ this.getElement(), tv.ui.ScrollPane.Class.HORIZONTAL);
+};
+
+/**
+ * Scrolls content element by given element of pixels.
+ * @param {number} scrollBy Number of pixels to scroll content element.
+ * @private
+ */
+tv.ui.ScrollPane.prototype.scrollBy_ = function(scrollBy) {
+ // Nothing to scroll.
+ if (this.contentRatio_ >= 1) {
+ return;
+ }
+
+ this.scrollOffset_ = Math.min(
+ this.contentSize_ - this.viewportSize_,
+ Math.max(this.scrollOffset_ + scrollBy, 0));
+
+ var contentCoords = new goog.math.Coordinate(
+ this.hasHorizontalScroll() ? -this.scrollOffset_ : 0,
+ this.hasHorizontalScroll() ? 0 : -this.scrollOffset_)
+ goog.style.setPosition(this.contentElement_, contentCoords);
+
+ var thumbCoords = new goog.math.Coordinate(
+ this.hasHorizontalScroll() ?
+ this.scrollOffset_ / this.scrollbarRatio_ : 0,
+ this.hasHorizontalScroll() ?
+ 0 : this.scrollOffset_ / this.scrollbarRatio_);
+ goog.style.setPosition(this.scrollThumbElement_, thumbCoords);
+};
+
+/**
+ * Computes all values needed for scroll:
+ * - viewportSize_ - scroll window size,
+ * - contentSize_ - content size,
+ * - contentRatio_ and scrollbarRatio_.
+ *
+ * Sets size of scroll thumb. Hides scrollbar if no scroll is needed.
+ *
+ * @inheritDoc
+ */
+tv.ui.ScrollPane.prototype.render = function() {
+ goog.base(this, 'render');
+
+ this.viewportSize_ = this.hasHorizontalScroll() ?
+ goog.style.getSize(this.viewportElement_).width :
+ goog.style.getSize(this.viewportElement_).height;
+ this.contentSize_ = this.hasHorizontalScroll() ?
+ this.contentElement_.scrollWidth :
+ this.contentElement_.scrollHeight;
+ var scrollbarSize = this.hasHorizontalScroll() ?
+ goog.style.getSize(this.scrollbarElement_).width :
+ goog.style.getSize(this.scrollbarElement_).height;
+
+ this.contentRatio_ = this.viewportSize_ / this.contentSize_;
+ this.scrollbarRatio_ = this.contentSize_ / scrollbarSize;
+
+ // Nothing to scroll?
+ var nothingToScroll = (this.contentRatio_ >= 1);
+ goog.dom.classes.enable(this.getElement(),
+ tv.ui.ScrollPane.Class.CONTENT_FITS_VIEWPORT,
+ nothingToScroll);
+ goog.array.forEach(this.getChildren(), function(child) {
+ child.setVisible(!nothingToScroll);
+ });
+
+ var thumbSize = Math.min(
+ scrollbarSize, Math.max(0, scrollbarSize * this.contentRatio_));
+ if (this.hasHorizontalScroll()) {
+ goog.style.setWidth(this.scrollThumbElement_, thumbSize);
+ } else {
+ goog.style.setHeight(this.scrollThumbElement_, thumbSize);
+ }
+
+ this.scrollBy_(0);
+};
=======================================
--- /dev/null
+++ /trunk/gtv-closure-demo/static/tv-ui/sub_menu.js Tue Apr 26 02:44:02
2011
@@ -0,0 +1,151 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Sub-menu is a container which has exactly one button and
one
+ * menu. If button is pressed, menu gets focused. If Esc key is pressed
while
+ * menu is focused, focus is brought back to button. Menu could also
contain
+ * special back button which is marked with CSS class. Pressing back
button in
+ * menu is analogous to pressing Esc.
+ */
+goog.provide('tv.ui.SubMenu');
+
+goog.require('goog.functions');
+goog.require('tv.ui.Container');
+
+/**
+ * Constructs menu.
+ * @constructor
+ * @extends {tv.ui.Container}
+ */
+tv.ui.SubMenu = function() {
+ goog.base(this);
+};
+goog.inherits(tv.ui.SubMenu, tv.ui.Container);
+
+/**
+ * @type {string} Main CSS class that triggers decoration.
+ */
+tv.ui.SubMenu.CLASS = 'tv-sub-menu';
+tv.ui.registerDecorator(tv.ui.SubMenu, tv.ui.SubMenu.CLASS);
+
+/**
+ * CSS classes that control look and feel of sub-menu.
+ * @enum {string}
+ */
+tv.ui.SubMenu.Class = {
+ /**
+ * Applied to special button within menu which, when pressed, will focus
this
+ * sub-menu's button.
+ */
+ BACK_BUTTON: 'tv-sub-menu-back-button'
+};
+
+/**
+ * @type {tv.ui.Button}
+ * @private
+ */
+tv.ui.SubMenu.prototype.button_;
+
+/**
+ * @type {tv.ui.Menu}
+ * @private
+ */
+tv.ui.SubMenu.prototype.menu_;
+
+/**
+ * @inheritDoc
+ */
+tv.ui.SubMenu.prototype.decorate = function(element) {
+ goog.base(this, 'decorate', element);
+
+ this.getEventHandler().listen(
+ this, tv.ui.Button.EventType.ACTION, this.onAction);
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.SubMenu.prototype.getClass = function() {
+ return tv.ui.SubMenu.CLASS;
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.SubMenu.prototype.addChild = function(child) {
+ goog.base(this, 'addChild', child);
+
+ if (child instanceof tv.ui.Button) {
+ goog.asserts.assert(
+ !this.button_, 'Sub-menu should have exactly one button.');
+ this.button_ = /** @type {tv.ui.Button} */(child);
+ } else if (child instanceof tv.ui.Menu) {
+ goog.asserts.assert(
+ !this.menu_, 'Sub-menu should have exactly one menu.');
+ this.menu_ = /** @type {tv.ui.Menu} */(child);
+ }
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.SubMenu.prototype.onKey = function(event) {
+ if (event.keyCode == goog.events.KeyCodes.ESC &&
+ !event.ctrlKey &&
+ !event.altKey &&
+ !event.shiftKey &&
+ !event.metaKey &&
+ this.getSelectedChild() != this.button_ &&
+ this.button_ &&
+ this.button_.tryFocus()) {
+ event.stopPropagation();
+ return;
+ }
+
+ goog.base(this, 'onKey', event);
+};
+
+/**
+ * Handles action event.
+ * @param {goog.events.Event} event Action event.
+ * @protected
+ */
+tv.ui.SubMenu.prototype.onAction = function(event) {
+ if (event.target == this.button_) {
+ if (this.menu_ && this.menu_.tryFocus()) {
+ event.stopPropagation();
+ }
+ } else if (goog.dom.classes.has(
+ event.target.getElement(), tv.ui.SubMenu.Class.BACK_BUTTON)) {
+ if (this.button_ && this.button_.tryFocus()) {
+ event.stopPropagation();
+ }
+ }
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.Menu.prototype.adjustSelectionFromKey = function(keyCode) {
+ var selectedChild = this.getSelectedChild();
+ if (!this.isSelectable() || !selectedChild) {
+ return false;
+ }
+ if (selectedChild.adjustSelectionFromKey) {
+ return selectedChild.adjustSelectionFromKey(keyCode);
+ } else {
+ return selectedChild.isSelectable();
+ }
+};
=======================================
--- /trunk/gtv-closure-demo/main.py Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/main.py Tue Apr 26 02:44:02 2011
@@ -20,6 +20,7 @@
{'title': 'Grid', 'template': 'grid.html'},
{'title': 'Lightbox', 'template': 'lightbox.html'},
{'title': 'Tab Container', 'template': 'tabcontainer.html'},
+ {'title': 'Menu', 'template': 'menu.html'},
# TODO(maksym): Input demo.
{'title': 'AJAX', 'template': 'ajax.html'}]
self.render_template('main.html', {'demos': demos})
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/button.js Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/button.js Tue Apr 26 02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Push button.
*/
@@ -90,6 +89,7 @@
tv.ui.Button.prototype.onMouseDown = function(event) {
goog.base(this, 'onMouseDown', event);

+ // TODO(maksym): Disabled button shouldn't trigger event.
if (this.isEager() &&
event.isButton(goog.events.BrowserEvent.MouseButton.LEFT)) {
this.dispatchAction();
@@ -97,6 +97,20 @@
}
};

+/**
+ * @inheritDoc
+ */
+tv.ui.Button.prototype.onTouchEnd = function(event) {
+ goog.base(this, 'onTouchEnd', event);
+
+ // TODO(maksym): Should we have eager buttons in touch mode?
+ if (!this.isEager() &&
+ !this.wasTouchMoved() &&
+ event.getBrowserEvent().touches.length == 0) {
+ this.dispatchAction();
+ }
+};
+
/**
* @return {boolean} Whether ACTION event is dispatched on mouse down.
*/
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/component.js Wed Feb 2 12:42:08
2011
+++ /trunk/gtv-closure-demo/static/tv-ui/component.js Tue Apr 26 02:44:02
2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Base class for TV UI components.
*/
@@ -56,7 +55,7 @@
/**
* Applied when component receives focus.
* @see tv.ui.Component.EventType#FOCUS
- * @see tv.ui.Document#setFocusedComponent
+ * @see #tryFocus
*/
FOCUSED: 'tv-component-focused',

@@ -119,10 +118,18 @@
*/
tv.ui.Component.prototype.renderScheduled_;

+/**
+ * Whether finger was moved during touch,
+ * @type {boolean}
+ * @private
+ */
+tv.ui.Component.prototype.touchMoved_;
+
/**
* @inheritDoc
*/
tv.ui.Component.prototype.disposeInternal = function() {
+ tv.ui.unregisterComponent(this);
this.eventHandler_.dispose();
delete this.element_;

@@ -171,13 +178,23 @@
this.element_ = element;
goog.dom.classes.add(this.element_, this.getClass());

+ tv.ui.registerComponent(this);
+
+ // TODO(maksym): Is this needed?
this.setVisible(!goog.dom.classes.has(
this.element_, tv.ui.Component.Class.HIDDEN));

+ if (goog.isDef(window.ontouchstart)) {
+ this.getEventHandler().listen(
+ element, goog.events.EventType.TOUCHSTART, this.onTouchStart);
+ this.getEventHandler().listen(
+ element, goog.events.EventType.TOUCHMOVE, this.onTouchMove);
+ this.getEventHandler().listen(
+ element, goog.events.EventType.TOUCHEND, this.onTouchEnd);
+ }
this.eventHandler_.listen(
- element,
- goog.events.EventType.MOUSEDOWN,
- this.onMouseDown);
+ element, goog.events.EventType.MOUSEDOWN, this.onMouseDown);
+
this.eventHandler_.listen(
this, tv.ui.Component.EventType.FOCUS, this.onFocus);
this.eventHandler_.listen(
@@ -261,7 +278,9 @@
*/
tv.ui.Component.prototype.dispatchKey_ = function(event) {
event.type = tv.ui.Component.EventType.KEY;
- event.target = this;
+ // TODO(maksym): Remove this cast magic.
+ var thisAsObject = (/** @type {Object} */ this);
+ event.target = (/** @type {Node} */ thisAsObject);
this.dispatchEvent(event);
};

@@ -286,21 +305,16 @@
}
}

- // Request focus to component or in case of containers to one of its
- // descendants.
- var focusedComponent = this.getSelectedDescendantOrSelf();
- if (!focusedComponent) {
- return;
- }
-
- this.getDocument().setFocusedComponent(focusedComponent);
- event.stopPropagation();
+ if (this.tryFocus()) {
+ event.stopPropagation();
+ }
};

/**
+ * @param {number} opt_keyCode Code of key that triggered selection change.
* @return {tv.ui.Component} Self, or null if component cannot accept
focus.
*/
-tv.ui.Component.prototype.getSelectedDescendantOrSelf = function() {
+tv.ui.Component.prototype.getSelectedDescendantOrSelf =
function(opt_keyCode) {
return this.isEnabled() && this.isVisible() ? this : null;
};

@@ -310,10 +324,36 @@
* @return {boolean} True if component can be selected.
* @protected
*/
-tv.ui.Component.prototype.selectFirstChild = function() {
+tv.ui.Component.prototype.selectFirstDescendant = function() {
return this.isEnabled() && this.isVisible();
};

+/**
+ * Can this component accept focus?
+ * @return {boolean} True if component can be selected.
+ * @protected
+ */
+tv.ui.Component.prototype.isSelectable = function() {
+ return this.isEnabled() && this.isVisible();
+};
+
+/**
+ * Requests focus to component or in case of containers to one of its
+ * descendants. Does nothing if component is not focusable.
+ * @param {boolean} opt_noScroll Don't scroll focused component into
viewport.
+ * @return {boolean} Whether component is focusable.
+ * @see tv.ui.Document#setFocusedComponent
+ */
+tv.ui.Component.prototype.tryFocus = function(opt_noScroll) {
+ var focusedComponent = this.getSelectedDescendantOrSelf();
+ if (!focusedComponent) {
+ return false;
+ }
+
+ this.getDocument().setFocusedComponent(focusedComponent, opt_noScroll);
+ return true;
+};
+
/**
* @return {boolean} Whether component is visible.
*/
@@ -326,6 +366,9 @@
* @param {boolean} visible Sets whether component is visible.
*/
tv.ui.Component.prototype.setVisible = function(visible) {
+ if (visible == this.isVisible()) {
+ return;
+ }
goog.dom.classes.enable(
this.element_, tv.ui.Component.Class.HIDDEN, !visible);

@@ -364,3 +407,43 @@
tv.ui.scheduleRender(this);
}
};
+
+/**
+ * Handles touch start event.
+ * @param {goog.events.Event} event Touch event.
+ * @protected
+ */
+tv.ui.Component.prototype.onTouchStart = function(event) {
+ this.touchMoved_ = false;
+ event.preventDefault();
+};
+
+/**
+ * Handles touch move event.
+ * @param {goog.events.Event} event Touch event.
+ * @protected
+ */
+tv.ui.Component.prototype.onTouchMove = function(event) {
+ this.touchMoved_ = true;
+ event.preventDefault();
+};
+
+/**
+ * Handles touch end event.
+ * @param {goog.events.Event} event Touch event.
+ * @protected
+ */
+tv.ui.Component.prototype.onTouchEnd = function(event) {
+ if (!this.touchMoved_ && event.getBrowserEvent().touches.length == 0) {
+ this.tryFocus(true);
+ }
+ event.preventDefault();
+};
+
+/**
+ * @return {boolean} Whether finger was moved during touch,
+ * @protected
+ */
+tv.ui.Component.prototype.wasTouchMoved = function() {
+ return this.touchMoved_;
+};
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/container.js Wed Feb 2 12:42:08
2011
+++ /trunk/gtv-closure-demo/static/tv-ui/container.js Tue Apr 26 02:44:02
2011
@@ -12,16 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Component container that features highlighting and
scrolling.
*/
goog.provide('tv.ui.Container');

+goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
+goog.require('goog.userAgent.product');
goog.require('tv.ui');
goog.require('tv.ui.Component');

@@ -157,7 +158,13 @@
* Mock child is an element that has dimensions of typical child.
* It should be used when scroll element or child elements have
animations.
*/
- MOCK_SCROLL: 'tv-container-mock-scroll'
+ MOCK_SCROLL: 'tv-container-mock-scroll',
+
+ /**
+ * Applied to root element if container resets its selection on blur.
+ * @see #hasTransientSelection
+ */
+ TRANSIENT_SELECTION: 'tv-container-transient-selection'
};

tv.ui.registerDecorator(
@@ -187,7 +194,19 @@
UPDATE_HIGHLIGHT: goog.events.getUniqueId('update_highlight')
};

-// TODO(maksym): Comment member variables.
+// TODO(maksym): Comment member and static variables.
+/**
+ * @type {number}
+ * @private
+ */
+tv.ui.Container.AVERAGE_VELOCITY_CALCULATION_INTERVAL = 50;
+
+/**
+ * @type {number}
+ * @private
+ */
+tv.ui.Container.DECELERATION_ANIMATION_INTERVAL = 20;
+
/**
* @type {Element}
* @private
@@ -236,6 +255,50 @@
*/
tv.ui.Container.prototype.controllingSharedHighlight_;

+/**
+ * @type {number}
+ * @private
+ */
+tv.ui.Container.prototype.scrollElementCoordinate_;
+
+/**
+ * @type {number}
+ * @private
+ */
+tv.ui.Container.prototype.minScrollElementCoordinate_;
+
+/**
+ * @type {number}
+ * @private
+ */
+tv.ui.Container.prototype.touchIdentifier_;
+
+/**
+ * @type {Array.<{time: number, coordinate: number}>}
+ * @private
+ */
+tv.ui.Container.prototype.touchMoves_;
+
+/**
+ * @type {goog.Timer}
+ * @private
+ */
+tv.ui.Container.prototype.decelerationTimer_;
+
+/**
+ * @type {number}
+ * @private
+ */
+tv.ui.Container.prototype.decelerationVelocity_;
+
+// TODO(maksym): Flag below is dangerous because it assumes that render()
will
+// be called immediately from scheduleRender(), which may not always be
true.
+/**
+ * @type {boolean}
+ * @private
+ */
+tv.ui.Container.prototype.skipNextScroll_;
+
/**
* @inheritDoc
*/
@@ -286,6 +349,23 @@
childElement.removeChild(childElement.firstChild));
}
}
+
+ if (this.scrollElement_ && goog.isDef(window.ontouchstart)) {
+ this.scrollTo_(0);
+
+ this.decelerationTimer_ = new goog.Timer(
+ tv.ui.Container.DECELERATION_ANIMATION_INTERVAL);
+ this.getEventHandler().listen(
+ this.decelerationTimer_, goog.Timer.TICK, function() {
+ this.scrollTo_(
+ this.scrollElementCoordinate_ + this.decelerationVelocity_,
true);
+
+ this.decelerationVelocity_ *= 0.95;
+ if (Math.abs(this.decelerationVelocity_) < 0.05) {
+ this.decelerationTimer_.stop();
+ }
+ });
+ }
};

/**
@@ -421,6 +501,14 @@
this.scrollElement_, tv.ui.Container.Class.START_SCROLL);
};

+/**
+ * @return {boolean} Whether container resets its selection on blur.
+ */
+tv.ui.Container.prototype.hasTransientSelection = function() {
+ return goog.dom.classes.has(
+ this.getElement(), tv.ui.Container.Class.TRANSIENT_SELECTION);
+};
+
/**
* Handles key event.
* Controls child component focus.
@@ -433,53 +521,57 @@
return;
}

- var selectionChanged;
- if (event.keyCode == this.getPreviousKey_()) {
- selectionChanged = this.selectPreviousChild();
- } else if (event.keyCode == this.getNextKey_()) {
- selectionChanged = this.selectNextChild();
+
+ var selectedChild;
+ var keyCode = event.keyCode;
+ if (keyCode == this.getPreviousKey_()) {
+ selectedChild = this.findPreviousSelectableChild(keyCode);
+ } else if (keyCode == this.getNextKey_()) {
+ selectedChild = this.findNextSelectableChild(keyCode);
}

- if (selectionChanged) {
+ if (selectedChild) {
+ this.setSelectedChild(selectedChild);
+ this.tryFocus();
+
event.stopPropagation();
event.preventDefault();
-
- this.getDocument().setFocusedComponent(
- this.selectedChild_.getSelectedDescendantOrSelf());
}
};

/**
- * @return {number} Code for key that moves selection towards start of the
- * container.
+ * @return {number} Code for the key that moves the selection towards
start of
+ * the container, or 0 if there is no such key.
* @private
*/
tv.ui.Container.prototype.getPreviousKey_ = function() {
- return this.isHorizontal() ?
- goog.events.KeyCodes.LEFT : goog.events.KeyCodes.UP;
+ return this.isHorizontal() ? goog.events.KeyCodes.LEFT :
+ this.isVertical() ? goog.events.KeyCodes.UP :
+ 0;
};

/**
- * @return {number} Code for key that moves selection towards end of the
- * container.
+ * @return {number} Code for the key that moves the selection towards end
of the
+ * container, or 0 if there is no such key.
* @private
*/
tv.ui.Container.prototype.getNextKey_ = function() {
- return this.isHorizontal() ?
- goog.events.KeyCodes.RIGHT : goog.events.KeyCodes.DOWN;
+ return this.isHorizontal() ? goog.events.KeyCodes.RIGHT :
+ this.isVertical() ? goog.events.KeyCodes.DOWN :
+ 0;
};

+// TODO(maksym): Change to findFirstSelectableChild().
/**
- * Tries to select one of the child components starting from first one.
- * Only components that can receive focus (or have children that can
receive
- * focus) are qualified.
- * @return {boolean} Whether focusable child has been found.
+ * Looks for first selectable descendant and updates selection chain up to
this
+ * container. Does nothing if container doesn't have selectable
descendants.
+ * @return {boolean} Whether selectable descendant has been found.
* @protected
*/
-tv.ui.Container.prototype.selectFirstChild = function() {
- return goog.base(this, 'selectFirstChild') &&
+tv.ui.Container.prototype.selectFirstDescendant = function() {
+ return goog.base(this, 'selectFirstDescendant') &&
goog.array.some(this.children_, function(child) {
- if (child.selectFirstChild()) {
+ if (child.selectFirstDescendant()) {
this.setSelectedChild(child);
return true;
}
@@ -488,41 +580,81 @@
};

/**
- * Tries to select one of the child components before currently selected
one.
- * Only components that can receive focus (or have children that can
receive
- * focus) are qualified.
- * @return {boolean} Whether selection changed.
+ * Walks down the container tree, and sometimes changes which child is
selected.
+ * If the key that was used to change selection matches the container's
next/
+ * previous key, we change the selection to the first/last child
respectively.
+ * Does nothing if container has no selectable descendants.
+ * @param {number} opt_keyCode Code of key that triggered selection change.
+ * @return {boolean} Whether selectable descendant has been found.
* @protected
*/
-tv.ui.Container.prototype.selectPreviousChild = function() {
- return this.changeSelectedChildIndexBy_(-1, 0);
+tv.ui.Container.prototype.adjustSelectionFromKey = function(opt_keyCode) {
+ if (!this.isSelectable()) {
+ return false;
+ }
+ var indexBegin, indexEnd;
+ if (opt_keyCode === this.getNextKey_()) {
+ // We entered the container by pressing 'right' or 'down'.
+ indexBegin = 0;
+ indexEnd = this.children_.length;
+ } else if (opt_keyCode === this.getPreviousKey_()) {
+ // We entered the container by pressing 'left' or 'up'.
+ indexBegin = this.children_.length - 1;
+ indexEnd = -1;
+ } else {
+ // Don't change selection in this container.
+ return this.selectedChild_ &&
+ (this.selectedChild_.adjustSelectionFromKey &&
+ this.selectedChild_.adjustSelectionFromKey(opt_keyCode) ||
+ this.selectedChild_.isSelectable());
+ }
+ var delta = indexBegin < indexEnd ? 1 : -1;
+ for (var i = indexBegin; i != indexEnd; i += delta) {
+ var child = this.children_[i];
+ if (child.adjustSelectionFromKey &&
+ child.adjustSelectionFromKey(opt_keyCode) ||
+ child.isSelectable()) {
+ this.setSelectedChild(child);
+ return true;
+ }
+ }
+ return false;
};

/**
- * Tries to select one of the child components after currently selected
one.
- * Only components that can receive focus (or have children that can
receive
- * focus) are qualified.
- * @return {boolean} Whether selection changed.
- * @protected
+ * Looks for selectable child components before currently selected one.
+ * @param {number} opt_keyCode Code of key that triggered selection change.
+ * @return {tv.ui.Component} Selectable child component or null if there
are
+ * none before currently selected one.
*/
-tv.ui.Container.prototype.selectNextChild = function() {
- return this.changeSelectedChildIndexBy_(1, this.children_.length - 1);
+tv.ui.Container.prototype.findPreviousSelectableChild =
function(opt_keyCode) {
+ return this.findSelectableChild_(-1, 0, opt_keyCode);
};

/**
- * Tries to select one of the child components in given direction
relatively to
+ * Looks for selectable child components after currently selected one.
+ * @param {number} opt_keyCode Code of key that triggered selection change.
+ * @return {tv.ui.Component} Selectable child component or null if there
are
+ * none after currently selected one.
+ */
+tv.ui.Container.prototype.findNextSelectableChild = function(opt_keyCode) {
+ return this.findSelectableChild_(1, this.children_.length - 1,
opt_keyCode);
+};
+
+/**
+ * Looks for selectable child components in given direction relatively to
* currently selected one.
- * Only components that can receive focus (or have children that can
receive
- * focus) are qualified.
* @param {number} delta +/-1 for direction.
* @param {number} lastIndex Last child index.
- * @return {boolean} Whether selection changed.
+ * @param {number} opt_keyCode Code of key that triggered selection change.
+ * @return {tv.ui.Component} Selectable child component or null if there
are
+ * none in given direction.
* @private
*/
-tv.ui.Container.prototype.changeSelectedChildIndexBy_ = function(
- delta, lastIndex) {
+tv.ui.Container.prototype.findSelectableChild_ = function(
+ delta, lastIndex, opt_keyCode) {
if (!this.selectedChild_) {
- return false;
+ return null;
}

var selectedChildIndex =
@@ -531,23 +663,30 @@
while (selectedChildIndex != lastIndex) {
selectedChildIndex += delta;
var selectedChild = this.children_[selectedChildIndex];
- if (selectedChild.getSelectedDescendantOrSelf()) {
- this.setSelectedChild(selectedChild);
- return true;
+ if (selectedChild.getSelectedDescendantOrSelf(opt_keyCode)) {
+ return selectedChild;
}
}

- return false;
+ return null;
};

/**
+ * @param {number} opt_keyCode Code of key that triggered selection change.
* @return {tv.ui.Component} Selected grand-...-grandchild, or null if no
* child is selected.
*/
-tv.ui.Container.prototype.getSelectedDescendantOrSelf = function() {
- return goog.base(this, 'getSelectedDescendantOrSelf') &&
- this.selectedChild_ &&
- this.selectedChild_.getSelectedDescendantOrSelf();
+tv.ui.Container.prototype.getSelectedDescendantOrSelf =
function(opt_keyCode) {
+ if (!goog.base(this, 'getSelectedDescendantOrSelf', opt_keyCode)) {
+ return null;
+ }
+ if (!this.isFocused()) {
+ // We are not focused, so we can adjust selected child if necessary.
+ this.adjustSelectionFromKey(opt_keyCode);
+ }
+ // Now check whether we have a selected child.
+ return (this.selectedChild_ &&
+ this.selectedChild_.getSelectedDescendantOrSelf(opt_keyCode));
};

/**
@@ -559,9 +698,12 @@
};

/**
- * @param {tv.ui.Component} selectedChild Sets currently selected child.
+ * Sets currently selected child.
+ * @param {tv.ui.Component} selectedChild Child to select.
+ * @param {boolean} opt_noScroll Don't scroll focused component into
viewport.
*/
-tv.ui.Container.prototype.setSelectedChild = function(selectedChild) {
+tv.ui.Container.prototype.setSelectedChild = function(
+ selectedChild, opt_noScroll) {
if (this.selectedChild_ == selectedChild) {
return;
}
@@ -576,6 +718,7 @@
if (this.selectedChild_) {
goog.dom.classes.add(
this.selectedChild_.getElement(),
tv.ui.Container.Class.SELECTED_CHILD);
+ this.skipNextScroll_ = opt_noScroll || false;
this.scheduleRender();
}

@@ -606,11 +749,21 @@
if (!this.scrollElement_) {
return;
}
+
+ // TODO(maksym): Minimum coordinate can contradict scrolling policy.
+ this.minScrollElementCoordinate_ = Math.min(0,
+ this.getOffsetSize_(this.element_) -
+ this.getScrollSize_(this.mockScrollElement_ || this.scrollElement_));
+
+ if (this.skipNextScroll_) {
+ this.skipNextScroll_ = false;
+ return;
+ }

// No children or all children are non-focusable?
if (!this.selectedChild_) {
// Scroll to start.
- this.setScrollElementPosition_(this.createCoordinate_(0));
+ this.scrollTo_(0);

// Hide slits.
this.startSlitElement_ && goog.dom.classes.remove(
@@ -627,8 +780,7 @@

// Policy requires to keep selected child at start of scrolling window.
if (this.isStartScroll_()) {
- this.setScrollElementPosition_(this.createCoordinate_(
- -this.getOffsetCoordinate_(selectedChildElement)));
+ this.scrollTo_(-this.getOffsetCoordinate_(selectedChildElement));

// TODO(maksym): Update slit visibility.

@@ -688,8 +840,7 @@
(allChildrenSize - firstVisibleChildCoordinate) > scrollWindowSize;

// Scroll, finally!
- this.setScrollElementPosition_(
- this.createCoordinate_(-firstVisibleChildCoordinate));
+ this.scrollTo_(-firstVisibleChildCoordinate);

// Show slits if necessary.
if (this.startSlitElement_) {
@@ -721,17 +872,43 @@

/**
* Sets position of real and mock scroll elements.
- * @param {goog.math.Coordinate} scrollElementPosition Position to set.
+ * @param {number} scrollElementCoordinate Position to set.
+ * @param {boolean=} opt_touchConstraints Whether to constrain scrolling as
+ * expected during touch interaction.
* @private
*/
-tv.ui.Container.prototype.setScrollElementPosition_ = function(
- scrollElementPosition) {
- goog.style.setPosition(this.scrollElement_, scrollElementPosition);
+tv.ui.Container.prototype.scrollTo_ = function(
+ scrollElementCoordinate, opt_touchConstraints) {
+ // TODO(maksym): Implement bouncing in touch mode instead.
+ this.scrollElementCoordinate_ = opt_touchConstraints ?
+ Math.max(
+ Math.min(0, scrollElementCoordinate),
+ this.minScrollElementCoordinate_) :
+ scrollElementCoordinate;
+
+ var scrollElementPosition =
+ this.createCoordinate_(this.scrollElementCoordinate_);
+ tv.ui.Container.setElementPosition_(
+ this.scrollElement_, scrollElementPosition);
if (this.mockScrollElement_) {
- goog.style.setPosition(this.mockScrollElement_, scrollElementPosition)
+ goog.style.setPosition(this.mockScrollElement_, scrollElementPosition);
}
};

+/**
+ * Moves element to given position.
+ * @param {Element} element Element to move.
+ * @param {goog.math.Coordinate} position Position to set.
+ */
+tv.ui.Container.setElementPosition_ =
+ goog.userAgent.product.IPHONE || goog.userAgent.product.IPAD ?
+ function(element, position) {
+ // 3D translation is powered by OpenGL on iOS.
+ element.style.webkitTransform =
+ 'translate3d(' + position.x + 'px, ' + position.y + 'px,
0)';
+ } :
+ goog.style.setPosition;
+
/**
* Moves highlight element at position of selected child.
* Does nothing if highlight is shared and container isn't in focus chain.
@@ -834,11 +1011,10 @@
// Child became selectable, set it as selected if container has none.
this.setSelectedChild(child);
} else if (this.selectedChild_ == child &&
- !child.getSelectedDescendantOrSelf() &&
- !this.selectNextChild() &&
- !this.selectPreviousChild()) {
- // Child stopped being selectable, no other selectable children found.
- this.setSelectedChild(null);
+ !child.getSelectedDescendantOrSelf()) {
+ // Child stopped being selectable, try to find other selectable
children.
+ this.setSelectedChild(
+ this.findNextSelectableChild() ||
this.findPreviousSelectableChild());
}
};

@@ -858,8 +1034,8 @@
if (this.hasSharedHighlight_()) {
this.controllingSharedHighlight_ = true;

- // Overkill, we only need to position highlight element. We might
have done
- // it by calling updateHighlight_() method. However we can trigger
+ // Overkill, we only need to position highlight element. We might
have
+ // done it by calling updateHighlight_() method. However we can
trigger
// unnecessary repaint in browser, so it's better this way.
this.scheduleRender();
}
@@ -887,4 +1063,132 @@

this.dispatchEvent(tv.ui.Container.EventType.UPDATE_HIGHLIGHT);
}
+
+ if (this.hasTransientSelection()) {
+ this.selectFirstDescendant();
+ }
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.Container.prototype.onTouchStart = function(event) {
+ goog.base(this, 'onTouchStart', event);
+
+ var touches = event.getBrowserEvent().changedTouches;
+ if (!this.scrollElement_ || this.touchIdentifier_ || touches.length !=
1) {
+ return;
+ }
+
+ this.decelerationTimer_.stop();
+ this.touchIdentifier_ = touches[0].identifier;
+ this.touchMoves_ = [];
+ this.addTouchMove_(this.getTouch_(event));
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.Container.prototype.onTouchMove = function(event) {
+ goog.base(this, 'onTouchMove', event);
+
+ var touch = this.getTouch_(event);
+ if (!this.scrollElement_ || !touch) {
+ return;
+ }
+
+ // TODO(maksym): Update slits in touch mode.
+ this.scrollTo_(
+ this.scrollElementCoordinate_ +
+ this.getPageCoordinate_(touch) -
+ this.getLastTouchMove_().coordinate,
+ true);
+ this.addTouchMove_(touch);
+
+ // TODO(maksym): It would be nice to stop propagation here but it's hard
+ // to decide when do so. Figure it out eventually.
+};
+
+/**
+ * @inheritDoc
+ */
+tv.ui.Container.prototype.onTouchEnd = function(event) {
+ // Intentionally not calling base class implementation, as containers
+ // shouldn't try focus themselves in touch mode.
+ event.preventDefault();
+
+ if (!this.scrollElement_ || !this.touchIdentifier_ ||
this.getTouch_(event)) {
+ return;
+ }
+
+ if (this.touchMoves_.length >= 2) {
+ var endTime = goog.now();
+ var endCoordinate = this.getLastTouchMove_().coordinate;
+
+ var interval = 0;
+ var distance = 0;
+ for (var i = this.touchMoves_.length - 1; i >= 0; i--) {
+ var possibleInterval = endTime - this.touchMoves_[i].time;
+ if (possibleInterval >
+ tv.ui.Container.AVERAGE_VELOCITY_CALCULATION_INTERVAL) {
+ break;
+ }
+ interval = possibleInterval;
+ distance = endCoordinate - this.touchMoves_[i].coordinate;
+ }
+
+ if (interval > 0) {
+ // Initial deceleration velocity should be a velocity of swipe.
+ // Multiplying it by animation interval gets number of pixels to
+ // scroll during single animation frame.
+ this.decelerationVelocity_ = distance / interval *
+ tv.ui.Container.DECELERATION_ANIMATION_INTERVAL;
+ this.decelerationTimer_.start();
+ }
+ }
+
+ delete this.touchIdentifier_;
+ delete this.touchMoves_;
+};
+
+/**
+ * @param {goog.events.Event} event Touch event.
+ * @return {Touch} Touch which initiated swipe, null if event doesn't have
it.
+ * @private
+ */
+tv.ui.Container.prototype.getTouch_ = function(event) {
+ return /** @type {Touch} */
(goog.array.find(event.getBrowserEvent().touches,
+ function(touch) {
+ return touch.identifier == this.touchIdentifier_;
+ }, this));
+};
+
+/**
+ * Records touch move for calculation of average speed of swipe.
+ * @param {Touch} touch Touch move.
+ * @private
+ */
+tv.ui.Container.prototype.addTouchMove_ = function(touch) {
+ this.touchMoves_.push({
+ time: goog.now(),
+ coordinate: this.getPageCoordinate_(touch)
+ });
+};
+
+/**
+ * @return {{time: number, coordinate: number}} Last recorded touch move.
+ * @private
+ */
+tv.ui.Container.prototype.getLastTouchMove_ = function() {
+ return this.touchMoves_[this.touchMoves_.length - 1];
+};
+
+/**
+ * Abstracts page coordinate of event for horizontal and vertical
container.
+ * @param {Touch} touch Touch move.
+ * @return {number} Page x or y, depending on container orientation.
+ * @private
+ */
+tv.ui.Container.prototype.getPageCoordinate_ = function(touch) {
+ return this.isHorizontal() ? touch.pageX : touch.pageY;
};
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/decorate_handler.js Wed Feb 2
12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/decorate_handler.js Tue Apr 26
02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Facilitates post-decoration initialization of components.
* Components can be matched by element id and CSS class.
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/deps.js Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/deps.js Tue Apr 26 02:44:02 2011
@@ -2,7 +2,7 @@
// Please do not edit.
goog.addDependency('../../../tv-ui/button.js', ['tv.ui.Button'],
['tv.ui', 'tv.ui.Component']);
goog.addDependency('../../../tv-ui/component.js', ['tv.ui.Component'],
['goog.events', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.style', 'tv.ui', 'tv.ui.Document']);
-goog.addDependency('../../../tv-ui/container.js', ['tv.ui.Container'],
['goog.dom', 'goog.dom.classes', 'goog.math.Coordinate', 'goog.style', 'tv.ui', 'tv.ui.Component']);
+goog.addDependency('../../../tv-ui/container.js', ['tv.ui.Container'],
['goog.Timer', 'goog.dom', 'goog.dom.classes', 'goog.math.Coordinate', 'goog.style', 'goog.userAgent.product', 'tv.ui', 'tv.ui.Component']);
goog.addDependency('../../../tv-ui/decorate_handler.js',
['tv.ui.DecorateHandler'], ['goog.array']);
goog.addDependency('../../../tv-ui/deps.js', [], []);
goog.addDependency('../../../tv-ui/document.js', ['tv.ui.Document'],
['goog.events.EventHandler', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.events.KeyHandler']);
@@ -10,7 +10,9 @@
goog.addDependency('../../../tv-ui/input.js', ['tv.ui.Input'],
['goog.dom', 'goog.dom.selection', 'goog.events.InputHandler', 'tv.ui', 'tv.ui.Component']);
goog.addDependency('../../../tv-ui/lightbox.js', ['tv.ui.Lightbox'],
['tv.ui.Container']);
goog.addDependency('../../../tv-ui/link.js', ['tv.ui.Link'],
['tv.ui', 'tv.ui.Button']);
-goog.addDependency('../../../tv-ui/menu.js', ['tv.ui.Menu'],
['tv.ui.Button', 'tv.ui.TabContainer']);
+goog.addDependency('../../../tv-ui/menu.js', ['tv.ui.Menu'],
['goog.functions', 'tv.ui.Container']);
+goog.addDependency('../../../tv-ui/scroll_pane.js', ['tv.ui.ScrollPane'],
['goog.asserts', 'goog.dom.classes', 'goog.style', 'tv.ui', 'tv.ui.Button', 'tv.ui.Container']);
+goog.addDependency('../../../tv-ui/sub_menu.js', ['tv.ui.SubMenu'],
['goog.functions', 'tv.ui.Container']);
goog.addDependency('../../../tv-ui/tab_container.js',
['tv.ui.TabContainer'], ['tv.ui.Container']);
goog.addDependency('../../../tv-ui/toggle_button.js',
['tv.ui.ToggleButton'], ['tv.ui', 'tv.ui.Button']);
goog.addDependency('../../../tv-ui/ui.js', ['tv.ui'],
['goog.array', 'goog.dom']);
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/document.js Wed Feb 2 12:42:08
2011
+++ /trunk/gtv-closure-demo/static/tv-ui/document.js Tue Apr 26 02:44:02
2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Document controls focus and key event flow.
*/
@@ -95,14 +94,17 @@
* Note that it is not guaranteed that component will be focused
immediately
* after exiting this method or even at all.
* @param {tv.ui.Component} componentPendingFocus Component to focus.
+ * @param {boolean} opt_noScroll Don't scroll focused component into
viewport.
*/
-tv.ui.Document.prototype.setFocusedComponent =
function(componentPendingFocus) {
+tv.ui.Document.prototype.setFocusedComponent = function(
+ componentPendingFocus, opt_noScroll) {
// Detect recursive call from blur or focus handler.
var recursive = goog.isDef(this.componentPendingFocus_);

// Remember component to focus.
// If called recursively from handler, last call wins.
this.componentPendingFocus_ = componentPendingFocus;
+ this.noScroll_ = opt_noScroll || false;

// Request will proceed in loop below.
if (recursive) {
@@ -143,7 +145,7 @@
}
for (; selectIndex >= 0; selectIndex--) {
focusCandidates[selectIndex + 1].setSelectedChild(
- focusCandidates[selectIndex]);
+ focusCandidates[selectIndex], this.noScroll_);
}

// Focus components down from the common ancestor.
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/grid.js Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/grid.js Tue Apr 26 02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Two-dimensional grid. It's a container that consists
* of other containers, whose selections are synchronized.
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/input.js Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/input.js Tue Apr 26 02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Single-line text input that is able to display hint when
empty.
*/
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/lightbox.css Wed Feb 2 12:42:08
2011
+++ /trunk/gtv-closure-demo/static/tv-ui/lightbox.css Tue Apr 26 02:44:02
2011
@@ -1,3 +1,7 @@
+/*
+ * tv.ui.Lightbox required styles.
+ */
+
.tv-lightbox-background {
background-color: #000;
}
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/lightbox.js Wed Feb 2 12:42:08
2011
+++ /trunk/gtv-closure-demo/static/tv-ui/lightbox.js Tue Apr 26 02:44:02
2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Photo lightbox. Simple variation of lightbox that displays
* only photos. Designed to take whole page space.
@@ -67,16 +66,16 @@
event.stopPropagation();
break;
case goog.events.KeyCodes.SPACE:
- if (this.selectNextChild()) {
- this.getDocument().setFocusedComponent(
- this.getSelectedDescendantOrSelf());
+ var selectedChild = this.findNextSelectableChild();
+ if (selectedChild) {
+ selectedChild.tryFocus();
event.stopPropagation();
}
break;
case goog.events.KeyCodes.BACKSPACE:
- if (this.selectPreviousChild()) {
- this.getDocument().setFocusedComponent(
- this.getSelectedDescendantOrSelf());
+ var selectedChild = this.findPreviousSelectableChild();
+ if (selectedChild) {
+ selectedChild.tryFocus();
event.stopPropagation();
}
break;
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/link.js Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/link.js Tue Apr 26 02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview A hyperlink.
*/
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/menu.js Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/menu.js Tue Apr 26 02:44:02 2011
@@ -12,27 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
- * @fileoverview Menu with submenus.
- * Based on tab container. Tab bar should consist of buttons which
represent
- * menu items, tab content - of containers which represent submenus. When
button
- * in tab bar is pressed, menu will focus appropriate container in tab
content.
+ * @fileoverview Menu is a container with transient selection which is
aware of
+ * items with sub-menus.
*/
goog.provide('tv.ui.Menu');

-goog.require('tv.ui.Button');
-goog.require('tv.ui.TabContainer');
+goog.require('goog.functions');
+goog.require('tv.ui.Container');

/**
* Constructs menu.
* @constructor
- * @extends {tv.ui.TabContainer}
+ * @extends {tv.ui.Container}
*/
tv.ui.Menu = function() {
goog.base(this);
};
-goog.inherits(tv.ui.Menu, tv.ui.TabContainer);
+goog.inherits(tv.ui.Menu, tv.ui.Container);

/**
* @type {string} Main CSS class that triggers decoration.
@@ -41,15 +38,15 @@
tv.ui.registerDecorator(tv.ui.Menu, tv.ui.Menu.CLASS);

/**
- * CSS classes that control look and feel of menu.
+ * CSS classes that reflect look of menu.
* @enum {string}
*/
tv.ui.Menu.Class = {
/**
- * A button inside tab content which when pressed will bring focus back
to
- * corresponding menu item in tab bar.
+ * Applied to root element if currently selected menu item is expanded.
+ * @see #hasOpenedSubMenu
*/
- BACK_BUTTON: 'tv-menu-back-button'
+ HAS_OPENED_SUB_MENU: 'tv-menu-has-opened-sub-menu'
};

/**
@@ -65,93 +62,67 @@
tv.ui.Menu.prototype.addChild = function(child) {
goog.base(this, 'addChild', child);

- if (goog.dom.classes.has(
- child.getElement(), tv.ui.TabContainer.Class.BAR)) {
+ if (child instanceof tv.ui.SubMenu) {
this.getEventHandler().listen(
- /** @type {tv.ui.Container} */(child),
- tv.ui.Button.EventType.ACTION,
- this.onBarAction);
- } else if (goog.dom.classes.has(
- child.getElement(), tv.ui.TabContainer.Class.CONTENT)) {
- this.getEventHandler().listen(
- /** @type {tv.ui.Container} */(child),
- tv.ui.Button.EventType.ACTION,
- this.onContentAction);
- this.getEventHandler().listen(
- /** @type {tv.ui.Container} */(child),
- tv.ui.Component.EventType.KEY,
- this.onContentKey);
+ child,
+ tv.ui.Container.EventType.SELECT_CHILD,
+ this.onSubMenuSelectChild_);
}
};

/**
- * @inheritDoc
+ * Handles selection change in one of sub-menus.
+ * @param {goog.events.Event} event Selection change event.
+ * @private
*/
-tv.ui.Menu.prototype.onBarSelectChild = function(event) {
- goog.base(this, 'onBarSelectChild', event);
- this.resetSubMenuSelection_();
+tv.ui.Menu.prototype.onSubMenuSelectChild_ = function(event) {
+ var selectedChild = this.getSelectedChild();
+ if (event.target == selectedChild) {
+ goog.dom.classes.enable(
+ this.getElement(),
+ tv.ui.Menu.Class.HAS_OPENED_SUB_MENU,
+ selectedChild.getSelectedChild() instanceof tv.ui.Menu);
+ // TODO(maksym): Dispatch tv.ui.Menu.EventType.TOGGLE_SUB_MENU.
+ }
};

/**
* @inheritDoc
*/
-tv.ui.Menu.prototype.onBarFocus = function(event) {
- this.resetSubMenuSelection_();
- goog.base(this, 'onBarFocus', event);
-};
-
-/**
- * Handles action event on tab bar.
- * Focuses sub-menu that corresponds to selected menu item.
- * @param {goog.events.Event} event Action event.
- * @protected
- */
-tv.ui.Menu.prototype.onBarAction = function(event) {
- if (!goog.dom.classes.has(
- event.target.getElement(), tv.ui.Menu.Class.BACK_BUTTON)) {
- this.resetSubMenuSelection_();
- this.tryFocusSelectedDescendant(this.getContentContainer());
- event.stopPropagation();
- }
-};
-
-/**
- * Handles action event on tab content.
- * Focuses menu item that corresponds to selected sub-menu if 'Back'
button was
- * pressed.
- * @param {goog.events.Event} event Action event.
- * @protected
- */
-tv.ui.Menu.prototype.onContentAction = function(event) {
- if (goog.dom.classes.has(
- event.target.getElement(), tv.ui.Menu.Class.BACK_BUTTON)) {
- this.tryFocusSelectedDescendant(this.getBarContainer());
- event.stopPropagation();
- }
+tv.ui.Menu.prototype.onKey = function(event) {
+ // Consider following component structure:
+ //
+ // - 1
+ // - 1.1
+ // - 1.2
+ // - 2
+ //
+ // If component 1.2 is focused and user presses Down key, focus will
traverse
+ // to component 2. However if components 1.1 and 1.2 are placed inside
+ // sub-menu, such behavior is unwanted. Instead, we pass this key
through.
+
+ if (this.hasOpenedSubMenu()) {
+ return;
+ }
+
+ goog.base(this, 'onKey', event);
};

/**
- * Handles key event on tab content.
- * Focuses menu item that corresponds to selected sub-menu if Esc is
pressed.
- * @param {goog.events.KeyEvent} event Key event.
- * @protected
+ * @return {boolean} Whether currently selected menu item is expanded.
*/
-tv.ui.Menu.prototype.onContentKey = function(event) {
- if (event.keyCode == goog.events.KeyCodes.ESC) {
- this.tryFocusSelectedDescendant(this.getBarContainer());
- event.stopPropagation();
- }
+tv.ui.Menu.prototype.hasOpenedSubMenu = function() {
+ return goog.dom.classes.has(
+ this.getElement(), tv.ui.Menu.Class.HAS_OPENED_SUB_MENU);
};

/**
- * Selects first child in sub-menu.
- * @private
+ * @inheritDoc
*/
-tv.ui.Menu.prototype.resetSubMenuSelection_ = function() {
- if (this.getContentContainer()) {
- var subMenu = this.getContentContainer().getSelectedChild();
- if (subMenu instanceof tv.ui.Container) {
- subMenu.selectFirstChild();
- }
- }
+tv.ui.Menu.prototype.adjustSelectionFromKey = function(keyCode) {
+ if (this.hasOpenedSubMenu()) {
+ // Don't alter the selection if we're not the final selected menu.
+ return false;
+ }
+ goog.base(this, 'adjustSelectionFromKey', keyCode);
};
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/tab_container.js Wed Feb 2
12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/tab_container.js Tue Apr 26
02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Container that displays child components in tabs.
* Container consists of two child containers - tab bar and tab content,
whose
@@ -94,7 +93,7 @@
* @return {boolean} Whether tab content container is focus attractor.
*/
tv.ui.TabContainer.prototype.hasFocusAttractor = function() {
- return this.contentContainer_ && goog.dom.classes.has(
+ return !!this.contentContainer_ && goog.dom.classes.has(
this.contentContainer_.getElement(),
tv.ui.TabContainer.Class.FOCUS_ATTRACTOR);
};
@@ -140,15 +139,17 @@
/**
* @inheritDoc
*/
-tv.ui.TabContainer.prototype.selectPreviousChild = function() {
- return !this.hasFocusAttractor() &&
goog.base(this, 'selectPreviousChild');
+tv.ui.TabContainer.prototype.findPreviousSelectableChild = function() {
+ return this.hasFocusAttractor() ?
+ null : goog.base(this, 'findPreviousSelectableChild');
};

/**
* @inheritDoc
*/
-tv.ui.TabContainer.prototype.selectNextChild = function() {
- return !this.hasFocusAttractor() && goog.base(this, 'selectNextChild');
+tv.ui.TabContainer.prototype.findNextSelectableChild = function() {
+ return this.hasFocusAttractor() ?
+ null : goog.base(this, 'findNextSelectableChild');
};

/**
@@ -196,19 +197,6 @@
*/
tv.ui.TabContainer.prototype.onBarFocus = function(event) {
if (this.hasFocusAttractor()) {
- this.tryFocusSelectedDescendant(this.contentContainer_);
- }
-};
-
-/**
- * Focuses given component if it is able to receive focus.
- * Does nothing otherwise.
- * @param {goog.ui.Component} component Component to focus.
- * @protected
- */
-tv.ui.TabContainer.prototype.tryFocusSelectedDescendant =
function(component) {
- var selectedDescendant = component.getSelectedDescendantOrSelf();
- if (selectedDescendant) {
- this.getDocument().setFocusedComponent(selectedDescendant);
+ this.contentContainer_.tryFocus();
}
};
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/toggle_button.js Wed Feb 2
12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/toggle_button.js Tue Apr 26
02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Button with on/off state.
*/
=======================================
--- /trunk/gtv-closure-demo/static/tv-ui/ui.js Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/static/tv-ui/ui.js Tue Apr 26 02:44:02 2011
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

-
/**
* @fileoverview Core utilities for TV components.
*/
@@ -159,3 +158,46 @@
tv.ui.componentsScheduledRender_.push(component);
}
};
+
+/**
+ * Identifier of last registered component.
+ * @type {number}
+ * @private
+ */
+tv.ui.lastUniqueComponentId_ = 0;
+
+/**
+ * Map from numeric identifier to decorated component.
+ * @type {Object.<number, tv.ui.Component>}
+ * @private
+ */
+tv.ui.idToComponentMap_ = {};
+
+/**
+ * Links DOM element and corresponding TV UI component.
+ * Components register themselves during decoration.
+ * @param {tv.ui.Component} component Component to register.
+ */
+tv.ui.registerComponent = function(component) {
+ var componentId = ++tv.ui.lastUniqueComponentId_;
+ component.getElement().componentId = componentId;
+ tv.ui.idToComponentMap_[componentId] = component;
+};
+
+/**
+ * Unlinks DOM element and corresponding TV UI component.
+ * Components unregister themselves during disposal.
+ * @param {tv.ui.Component} component Component to unregister.
+ */
+tv.ui.unregisterComponent = function(component) {
+ delete tv.ui.idToComponentMap_[component.getElement().componentId];
+};
+
+/**
+ * @param {Element} element Decorated DOM element.
+ * @return {tv.ui.Component} TV UI component that was created as the
result of
+ * decoration of given DOM element.
+ */
+tv.ui.getComponentByElement = function(element) {
+ return tv.ui.idToComponentMap_[element.componentId];
+};
=======================================
--- /trunk/gtv-closure-demo/templates/ajax.html Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/templates/ajax.html Tue Apr 26 02:44:02 2011
@@ -164,14 +164,13 @@
});
}

-var previewsContainer;
-
-function onPreviewsDecorate(container) {
- previewsContainer = container;
+function onPreviewsDecorate() {
loadPreviews('movie trailers');
}

function loadPreviews(query, opt_focusPreviews) {
+ var previewsContainer = tv.ui.getComponentByElement(
+ goog.dom.getElementByClass('ajax-previews'));
previewsContainer.removeChildren();
var scrollElement = goog.dom.getElementByClass(
'tv-container-middle-scroll', previewsContainer.getElement());
@@ -201,8 +200,7 @@

var firstPreviewButton = previewsContainer.getChildren()[0];
if (opt_focusPreviews && firstPreviewButton) {
- firstPreviewButton.getDocument().setFocusedComponent(
- firstPreviewButton);
+ firstPreviewButton.tryFocus();
}
});
}
=======================================
--- /trunk/gtv-closure-demo/templates/lightbox.html Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/templates/lightbox.html Tue Apr 26 02:44:02 2011
@@ -74,10 +74,10 @@
goog.events.listen(button, tv.ui.Button.EventType.ACTION, function() {
var previousFocus = tv.ui.Document.getInstance().getFocusedComponent();
lightbox = tv.ui.Lightbox.show(photos, 0 /* start index*/);
- tv.ui.Document.getInstance().setFocusedComponent(lightbox);
+ lightbox.tryFocus();
historyManager.setToken('lightbox');
goog.events.listen(lightbox, tv.ui.Lightbox.EventType.CLOSE,
function() {
- tv.ui.Document.getInstance().setFocusedComponent(previousFocus);
+ previousFocus.tryFocus();
});
});
});
=======================================
--- /trunk/gtv-closure-demo/templates/main.html Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/templates/main.html Tue Apr 26 02:44:02 2011
@@ -1,6 +1,6 @@
<html>
<head>
- <title>tv.ui.Decoration Demos</title>
+ <title>Google TV UI Library Demos</title>
<link rel="stylesheet" href="/static/tv-ui/container.css"
type="text/css"/>
<link rel="stylesheet" href="/static/tv-ui/lightbox.css"
type="text/css"/>
<link rel="stylesheet" href="/static/main.css" type="text/css"/>
@@ -19,6 +19,7 @@
goog.require('tv.ui.Lightbox');
goog.require('tv.ui.Link');
goog.require('tv.ui.Menu');
+ goog.require('tv.ui.SubMenu');
goog.require('tv.ui.TabContainer');
goog.require('tv.ui.ToggleButton');
</script>
@@ -71,7 +72,7 @@

tv.ui.postponeRender(function() {
tv.ui.decorate(document.body, decorateHandler.getHandler());
- tv.ui.Document.getInstance().setFocusedComponent(firstNavItem);
+ firstNavItem.tryFocus();
});
</script>
</body>
=======================================
--- /trunk/gtv-closure-demo/templates/menu.html Wed Feb 2 12:42:08 2011
+++ /trunk/gtv-closure-demo/templates/menu.html Tue Apr 26 02:44:02 2011
@@ -6,57 +6,72 @@

<style>

-.menus-continents-expanding-menu,
+.menus-continents-expanding-menu {
+ position: relative;
+}
+
.menus-continents-sliding-menu {
- overflow: hidden;
- white-space: nowrap;
+ background-color: #4C6EA7;
+ position: relative;
+ width: 190px;
}

.menus-continents-sliding-menu,
-.menus-continents-sliding-menu .menus-countries-menu {
- width: 190px;
+.menus-continents-sliding-menu .tv-menu {
+ -webkit-border-radius: 5px;
+ height: 90px;
}

-.menus-countries-menu,
-.menus-cities {
- display: none;
+.menus-continents-sliding-menu .tv-menu {
+ background-color: #405c8c;
}

-.menus-continents-expanding-menu .menus-countries-menu,
-.menus-continents-expanding-menu .menus-cities {
- -webkit-transition: left 200ms ease;
- left: -190px;
- position: relative;
+.menus-continents-sliding-menu .tv-menu .tv-menu {
+ background-color: #344c73;
}

-.menus-countries-menu.tv-container-selected-child,
-.menus-cities.tv-container-selected-child {
- display: block;
+.tv-sub-menu > .tv-menu {
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ top: 0;
}

-.menus-continents-expanding-menu .menus-countries-menu.tv-component-focused,
-.menus-continents-expanding-menu .menus-cities.tv-component-focused {
+.menus-continents-expanding-menu .tv-sub-menu > .tv-menu {
+ left: 195px;
+ z-index: -1;
+}
+
+.menus-continents-expanding-menu .tv-sub-menu
> .tv-menu.tv-component-focused {
+ z-index: 0;
+}
+
+.menus-continents-sliding-menu .tv-sub-menu > .tv-menu {
+ -webkit-transition: left 200ms ease;
+ left: 190px;
+}
+
+.menus-continents-sliding-menu .tv-sub-menu
> .tv-menu.tv-component-focused {
left: 0;
}

-.menus-continents-sliding-menu > .tv-container-start-scroll,
-.menus-continents-sliding-menu .menus-countries-menu
> .tv-container-start-scroll {
+.menus-viewport {
-webkit-transition: left 200ms ease;
+ position: relative;
+ left: -190px;
}

-.menus-continents,
-.menus-continents-content,
-.menus-countries,
-.menus-countries-content {
- display: inline-block;
- overflow: hidden;
- vertical-align: top;
+.tv-menu.tv-component-focused > .menus-viewport {
+ left: 0;
}

.menus-item {
+ -webkit-border-radius: 5px;
cursor: pointer;
- height: 30px;
- line-height: 30px;
+ height: 28px;
+ line-height: 28px;
+ padding: 1px;
+ width: 188px;
}

.menus-item-text,
@@ -65,29 +80,28 @@
}

.menus-item-text {
- padding-left: 5px;
+ padding-left: 4px;
width: 150px;
}

.menus-item-arrow {
+ padding-right: 4px;
+ text-align: right;
width: 30px;
}

-.menus-continents-expanding-menu.tv-component-focused .menus-item.tv-container-selected-child
{
+.tv-sub-menu.tv-component-focused > .menus-item,
+.menus-item.tv-component-focused {
border: 1px solid white;
- height: 28px;
- line-height: 28px;
- width: 188px;
+ padding: 0;
}

-.menus-item.tv-component-focused,
-.menus-continents-expanding-menu.tv-component-focused .menus-item.tv-component-focused
{
+.menus-item.tv-component-focused {
background: white;
color: black;
}

-.menus-item.menus-splash,
-.menus-continents-expanding-menu.tv-component-focused .menus-item.menus-splash
{
+.menus-item.menus-splash {
-webkit-transition: background 300ms, color 300ms;
background: none;
color: #FCFCFC;
@@ -98,292 +112,253 @@
<p>Menu extends tab container by introducing new ways of navigation, such
as entering submenu when <code>Enter</code> is pressed.
As other TV UI components, menu doesn't imply any specific looks. This
expanding menu...

-<div class="menus-continents-expanding-menu tv-menu
tv-container-horizontal">
- <div class="menus-continents tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button">
- <div class="menus-item-text">Asia</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">Europe</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">North America</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
+<div class="menus-continents-expanding-menu tv-menu tv-container-vertical">
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Asia</div><div
class="menus-item-arrow">&rarr;</div>
</div>
- <div class="menus-continents-content tv-container tv-container-stack
tv-tab-container-content">
-
- <div class="menus-countries-menu tv-menu tv-container-horizontal">
- <div class="menus-countries tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button">
- <div class="menus-item-text">China</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">South Korea</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">China</div><div
class="menus-item-arrow">&rarr;</div>
</div>
- <div class="menus-countries-content tv-container
tv-container-stack tv-tab-container-content">
- <div class="menus-cities tv-container tv-container-vertical">
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Beijing</div>
- <div class="menus-item-arrow"></div>
</div>
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Shanghai</div>
- <div class="menus-item-arrow"></div>
</div>
</div>
- <div class="menus-cities tv-container tv-container-vertical">
+ </div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">South Korea</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Busan</div>
- <div class="menus-item-arrow"></div>
</div>
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Seoul</div>
- <div class="menus-item-arrow"></div>
</div>
</div>
</div>
+ </div>
</div>
-
- <div class="menus-countries-menu tv-menu tv-container-horizontal">
- <div class="menus-countries tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button">
- <div class="menus-item-text">Poland</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">Switzerland</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
+ </div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Europe</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Poland</div><div
class="menus-item-arrow">&rarr;</div>
</div>
- <div class="menus-countries-content tv-container
tv-container-stack tv-tab-container-content">
- <div class="menus-cities tv-container tv-container-vertical">
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Krakow</div>
- <div class="menus-item-arrow"></div>
</div>
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Warsaw</div>
- <div class="menus-item-arrow"></div>
</div>
</div>
- <div class="menus-cities tv-container tv-container-vertical">
+ </div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Switzerland</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Geneva</div>
- <div class="menus-item-arrow"></div>
</div>
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Zurich</div>
- <div class="menus-item-arrow"></div>
</div>
</div>
</div>
+ </div>
</div>
-
- <div class="menus-countries-menu tv-menu tv-container-horizontal">
- <div class="menus-countries tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button">
- <div class="menus-item-text">Canada</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">United States</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
+ </div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">North America</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Canada</div><div
class="menus-item-arrow">&rarr;</div>
</div>
- <div class="menus-countries-content tv-container
tv-container-stack tv-tab-container-content">
- <div class="menus-cities tv-container tv-container-vertical">
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Montreal</div>
- <div class="menus-item-arrow"></div>
</div>
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Toronto</div>
- <div class="menus-item-arrow"></div>
</div>
</div>
- <div class="menus-cities tv-container tv-container-vertical">
+ </div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">United States</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-viewport">
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">Los Angeles</div>
- <div class="menus-item-arrow"></div>
</div>
<div class="menus-city menus-item tv-button">
<div class="menus-item-text">New York</div>
- <div class="menus-item-arrow"></div>
</div>
</div>
</div>
+ </div>
</div>
-
</div>
+ </div>
</div>

<p>...mostly differs from sliding menu by CSS styles, having almost
identical HTML structure.

-<div class="menus-continents-sliding-menu tv-menu tv-container-horizontal">
- <div class="tv-container-start-scroll">
- <div class="menus-continents tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button">
- <div class="menus-item-text">Asia</div>
- <div class="menus-item-arrow">&rarr;</div>
+<div class="menus-continents-sliding-menu tv-menu tv-container-vertical">
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Asia</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
</div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">Europe</div>
- <div class="menus-item-arrow">&rarr;</div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">China</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Beijing</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Shanghai</div>
+ </div>
+ </div>
</div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">North America</div>
- <div class="menus-item-arrow">&rarr;</div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">South Korea</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Busan</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Seoul</div>
+ </div>
+ </div>
</div>
</div>
- <div class="menus-continents-content tv-container tv-container-stack
tv-tab-container-content">
-
- <div class="menus-countries-menu tv-menu tv-container-horizontal">
- <div class="tv-container-start-scroll">
- <div class="menus-countries tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">China</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">South Korea</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Europe</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Poland</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
</div>
- <div class="menus-countries-content tv-container
tv-container-stack tv-tab-container-content">
- <div class="tv-container"></div>
- <div class="menus-cities tv-container tv-container-vertical">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Beijing</div>
- <div class="menus-item-arrow"></div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Shanghai</div>
- <div class="menus-item-arrow"></div>
- </div>
- </div>
- <div class="menus-cities tv-container tv-container-vertical">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Busan</div>
- <div class="menus-item-arrow"></div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Seoul</div>
- <div class="menus-item-arrow"></div>
- </div>
- </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Krakow</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Warsaw</div>
</div>
</div>
</div>
-
- <div class="menus-countries-menu tv-menu tv-container-horizontal">
- <div class="tv-container-start-scroll">
- <div class="menus-countries tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">Poland</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">Switzerland</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Switzerland</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
</div>
- <div class="menus-countries-content tv-container
tv-container-stack tv-tab-container-content">
- <div class="tv-container"></div>
- <div class="menus-cities tv-container tv-container-vertical">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Krakow</div>
- <div class="menus-item-arrow"></div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Warsaw</div>
- <div class="menus-item-arrow"></div>
- </div>
- </div>
- <div class="menus-cities tv-container tv-container-vertical">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Geneva</div>
- <div class="menus-item-arrow"></div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Zurich</div>
- <div class="menus-item-arrow"></div>
- </div>
- </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Geneva</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Zurich</div>
</div>
</div>
</div>
-
- <div class="menus-countries-menu tv-menu tv-container-horizontal">
- <div class="tv-container-start-scroll">
- <div class="menus-countries tv-container tv-container-vertical
tv-tab-container-bar">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">Canada</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
- <div class="menus-item tv-button">
- <div class="menus-item-text">United States</div>
- <div class="menus-item-arrow">&rarr;</div>
- </div>
+ </div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">North America</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
+ </div>
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">Canada</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
</div>
- <div class="menus-countries-content tv-container
tv-container-stack tv-tab-container-content">
- <div class="tv-container"></div>
- <div class="menus-cities tv-container tv-container-vertical">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Montreal</div>
- <div class="menus-item-arrow"></div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Toronto</div>
- <div class="menus-item-arrow"></div>
- </div>
- </div>
- <div class="menus-cities tv-container tv-container-vertical">
- <div class="menus-item tv-button tv-menu-back-button">
- <div class="menus-item-text">&larr; Back</div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">Los Angeles</div>
- <div class="menus-item-arrow"></div>
- </div>
- <div class="menus-city menus-item tv-button">
- <div class="menus-item-text">New York</div>
- <div class="menus-item-arrow"></div>
- </div>
- </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Montreal</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Toronto</div>
</div>
</div>
</div>
-
+ <div class="tv-sub-menu tv-container-horizontal">
+ <div class="menus-item tv-button">
+ <div class="menus-item-text">United States</div><div
class="menus-item-arrow">&rarr;</div>
+ </div>
+ <div class="tv-menu tv-container-vertical">
+ <div class="menus-item tv-button tv-sub-menu-back-button">
+ <div class="menus-item-text">&larr; Back</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">Los Angeles</div>
+ </div>
+ <div class="menus-city menus-item tv-button">
+ <div class="menus-item-text">New York</div>
+ </div>
+ </div>
+ </div>
</div>
</div>
</div>

Reply all
Reply to author
Forward
0 new messages