The Scroller widget provides the scrolling behavior in Mojo. A scroller is installed automatically in every scene, and you can have any number of additional scrollers anywhere in the DOM.
You can disable the scroller in a scene by setting the disableSceneScroller
property to true in the
scene arguments to pushScene
.
In the current release of Mojo, you can select one of six scrolling modes, specified in the mode property of the widget’s attributes:
free
Allow scrolling along both the horizontal and vertical axes.
horizontal
vertical
dominant
Allow scrolling along the horizontal or vertical axis, but not both at once. The direction of the initial drag will determine the scrolling axis.
horizontal-snap
In this mode, scrolling is locked to the horizontal axis, but
snaps to points determined by the position of the block elements
found in the model’s snapElements
property. As the scroller scrolls from snap point to snap point it
will send a propertyChange
event.
vertical-snap
This mode locks scrolling to the vertical axis, and snaps to points determined by the elements in the
snapElements
property
array.
Upon rendering, the widget targets its single child element for scrolling. If it has more than one child element, it will create a single div to wrap the child elements. It will never update this element, so if you replace the contents of a Scroller widget after it is instantiated, scrolling might not work. Instead, put another block element inside the scroller and update its contents as needed.
The size of the scroller’s target div, the child element, must be set in CSS. By default, the div will expand to the size of the contents. You must constrain the width, on a horizontal scroller, or the height, on a vertical scroller, within your CSS or the scroller will not function.
A Scroller widget will ignore any drag start event that doesn’t indicate a valid scroll start for its mode setting, so you can nest scrollers if they don’t conflict. For example, you can put a small horizontal scroller inside the default scene vertical scroller. This configuration will pass horizontal swipes to the horizontal scroller, but vertical swipes on the horizontal scroller, or any kinds of swipes outside the horizontal scroller, will be passed to the scene scroller.
In this example, a rotating feature story will be added to the News
application. This will present a title and story from the feed list for
5 seconds, after which time it will be replaced by another story. This
rotating feature story will be displayed in a fixed-size area above the
feedListWgt
to allow for a stable
view, but we’ll attach a vertical scroller to allow users to read the
full story if it is longer than the view, and we’ll enclose it all in a
drawer so that users can selectively enable or disable this view.
Start by modifying the feedList
scene (feedList-scene.html) to add
an icon to the palm-header
to serve
as a tap target to open and close the drawer. We’re going to change the
image to match the state of the drawer by using two different classes,
featureFeed-close
and featureFeed-open
. We’ll start with the drawer
closed.
The Scroller is declared and encloses featureStoryDiv
, which will be fixed to a
specific height through CSS and filled by the story title and text. This
is all placed above the feedList
widget in the scene’s layout.
Within the featureStoryDiv
,
define the splashScreen
div to fill
the space for the initial launch case where there are no stories to
display. The update-image
style will
insert the News icon alongside a copyright notice for the application.
We’ll hide this div when there are stories to display; each story
title
and text
will be inserted in the following divs,
identified as featureStoryTitle
and
featureStory
:
<div id="feedListScene"> <div id="feedListMain"> <!-- Rotating Feature Story --> <div id="feedList_view_header" class="palm-header left"> Latest News <div id="featureDrawer" class="featureFeed-close"></div> </div> <div class="palm-header-spacer"></div> <div x-mojo-element="Drawer" id="featureFeedDrawer"> <div x-mojo-element="Scroller" id="featureScroller" > <div id="featureStoryDiv" class="featureScroller"> <div id="splashScreen" class="splashScreen"> <div class="update-image"></div> <div class="title">News v0.8<span>#{version}</span> <div class="palm-body-text"> Copyright 2009, Palm® </div> </div> </div> <div id="featureStoryTitle" class="palm-body-title"> </div> <div id="featureStory" class="palm-body-text"> </div> </div> </div> </div> <!-- Feed List --> <div class="palm-list"> <div x-mojo-element="List" id="feedListWgt"></div> </div> </div> </div>
Set up the Drawer and the Scroller in the feedList-assistant.js. Set up a listener for
taps to the new tap target in the header and then the Drawer widget with
the state defined by a new global, News.featureFeedEnable
. In Chapter 6, we’ll add saved preferences and we will retain the
drawer’s state at that time.
The featureScrollerModel
defines a single property, the scroller mode
, in this case set to vertical, and the
Scroller is set up with just its ID and model as arguments. The scroller
simply responds to vertical swipes to scroll the content, and will
generate scrolling events if you want to receive them, although you
don’t normally need to:
// Setup header, drawer, scroller and handler for feature feeds this.featureDrawerHandler = this.toggleFeatureDrawer.bindAsEventListener(this); this.controller.listen("featureDrawer", Mojo.Event.tap, this.featureDrawerHandler); this.controller.setupWidget("featureFeedDrawer", {}, this.featureFeedDrawer = {open: News.featureFeedEnable}); this.featureScrollerModel = { mode: "vertical" }; this.controller.setupWidget("featureScroller", this.featureScrollerModel); this.readFeatureStoryHandler = this.readFeatureStory.bindAsEventListener(this); this.controller.listen("featureStoryDiv", Mojo.Event.tap, this.readFeatureStoryHandler); // If feature story is enabled, then set the icon to open if (this.featureFeedDrawer.open === true) { this.controller.get("featureDrawer").className = "featureFeed-open"; } else { this.controller.get("featureDrawer").className = "featureFeed-close"; }
A listener is set up to handle taps in the feature story div, but
it is unrelated to the Scroller. If users see a story they want to read
further, they can scroll the story or tap it to go to the storyView
scene with that story. And we’ll
finish by setting the featureDrawer
element’s className
to match the
state of the drawer; this is also a future provision for using saved
preferences when the scene could be activated with the drawer in the
open state.
The CSS completes the implementation by fixing the size of the Scroller div and formatting the contents. The first two rules support the tap target, either open or closed, and the drawer background. You’ll have to add the appropriate images to the images folder of your application to reproduce this:
/* FeedList Header styles for feature drawer and selection */ .featureFeed-close { float:right; margin: 8px −12px 0px 0px; height:35px; width: 35px; background: url(../images/details-open-arrow.png) no-repeat; } .featureFeed-open { float:right; margin: 8px −12px 0px 0px; height:35px; width: 35px; background: url(../images/details-closed-arrow.png) no-repeat; } .palm-drawer-container { border-width: 20px 1px 20px 1px; -webkit-border-image: url(../images/palm-drawer-background-2.png) 20 1 20 1 repeat repeat; -webkit-box-sizing: border-box; overflow: visible; } .featureScroller { height: 100px; width: 280px; margin-left: 20px; }
The palm-drawer-container
selector sets the
drawer container’s dimensions and applies a
white background to contrast with the scene’s main background.
The featureScroller
style bounds the
scroller’s height because we have a vertical scroller; in the case of a
horizontal scroller, we’d bound the width. If you aren’t careful with
your CSS, the scroller will not behave as expected.
The rest of the sample is related to handling the feature story.
Create a new method, toggleFeatureDrawer()
, to open or close the
drawer. If the drawer is open, the method will close it by setting the
drawer’s model to false, and setting the div class to featureFeed-close
. If the drawer is closed,
the actions are reversed to open the drawer and we’ll start the story
rotation if it’s not already running. The controller’s modelChanged()
method is
called to signal the model changes to the drawer widget:
// toggleFeatureDrawer - handles taps to the featureFeed drawer. Toggle // drawer and icon class to reflect drawer state. FeedListAssistant.prototype.toggleFeatureDrawer = function(event) { var featureDrawer = this.controller.get("featureDrawer"); if (this.featureFeedDrawer.open === true) { this.featureFeedDrawer.open = false; News.featureFeedEnable = false; featureDrawer.className = "featureFeed-close"; } else { this.featureFeedDrawer.open = true; News.featureFeedEnable = true; featureDrawer.className = "featureFeed-open"; // If there's some stories in the feedlist, then start // the story rotation even if the featureFeed is disabled as we'll use // the rotation timer to update the DB if(this.feeds.list[this.featureIndexFeed].stories.length > 0) { var splashScreenElement = this.controller.get("splashScreen"); splashScreenElement.hide(); this.showFeatureStory(); } } this.controller.modelChanged(this.featureFeedDrawer); };
Create the showFeed()
method to
present the feature story and set up the timer for the next story. We set the timer
default to 5 seconds (5000 milliseconds) in News.featureStoryInterval
, another new
global variable, added to stage-assistant.js. If the timer has been
set, rotate the story by taking the next story in the current feed or
the first story in the next feed if it’s at the end of the current feed.
We will also add some logic to strip URLs and other HTML from the title
and text:
// ------------------------------------------------------------------- // Feature story functions // // showFeatureStory - simply rotate the stories within the // featured feed, which the user can set in their preferences. FeedListAssistant.prototype.showFeatureStory = function() { // If timer is null, either initial story or restarting. Start with // previous story.. if (News.featureStoryTimer === null) { // ** These next two lines are wrapped for book formatting only ** News.featureStoryTimer = this.controller.window.setInterval( this.showFeatureStory.bind(this), News.featureStoryInterval); } else { this.featureIndexStory = this.featureIndexStory+1; // ** These next two lines are wrapped for book formatting only ** if(this.featureIndexStory >= this.feeds.list[this.featureIndexFeed].stories.length) { this.featureIndexStory = 0; this.featureIndexFeed = this.featureIndexFeed+1; if (this.featureIndexFeed >= this.feeds.list.length) { this.featureIndexFeed = 0; } } } var summary = this.feeds.list[this.featureIndexFeed].stories[ this.featureIndexStory].text.replace(/(<([^>]+)>)/ig,""); summary = summary.replace(/http:S+/ig,""); var featureStoryTitleElement = this.controller.get("featureStoryTitle"); // ** These next two lines are wrapped for book formatting only ** featureStoryElement.innerHTML = unescape(this.feeds.list[this.featureIndexFeed].stories[ this.featureIndexStory].title); var featureStoryElement = this.controller.get("featureStory"); featureStoryElement.innerHTML = summary; // Because this is periodic and not tied to a screen transition, use // this to update the db when changes have been made if (News.feedListChanged === true) { News.feedListChanged = false; this.feedWgtModel.items = this.feeds.list; this.controller.modelChanged(this.feedWgtModel, this); } }; // readFeatureStory - handler when user taps on feature story; // will push storyView with the current feature story. FeedListAssistant.prototype.readFeatureStory = function() { Mojo.Controller.stageController.pushScene("storyView", this.feeds.list[this.featureIndexFeed], this.featureIndexStory); };
Following showFeatureStory()
is
readFeatureStory()
, which simply
pushes the storyView
scene with the
current feature story.
When you run the application, you’ll see a different look, with
the feature story now filling the top third of the feedList
scene, as shown in Figure 5-5.
There’s still some cleanup needed. We must maintain this.featureIndexFeed
during list reordering,
and delete. You can see the changes made to the listDeleteHandler()
and listReorderHandler()
in the feedList-assistant.js in the News source
listing in Appendix D.
18.117.137.252