blob: 458e4eae5be7234a5f5e27dcd68be834203f24d2 [file] [log] [blame]
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
part of swarmlib;
// TODO(jacobr): there is a lot of dead code in this class. Checking is as is
// and then doing a large pass to remove functionality that doesn't make sense
// given the UI layout.
/**
* Front page of Swarm.
*/
// TODO(jacobr): this code now needs a large refactoring.
// Suggested refactorings:
// Move animation specific code into helper classes.
class FrontView extends CompositeView {
final Swarm swarm;
/** View containing all UI anchored to the top of the page. */
CompositeView topView;
/** View containing all UI anchored to the left side of the page. */
CompositeView bottomView;
HeaderView headerView;
SliderMenu sliderMenu;
/**
* When the user is viewing a story, the data source for that story is
* detached from the section and shown at the bottom of the screen. This keeps
* track of that so we can restore it later.
*/
DataSourceView detachedView;
/**
* Map from section title to the View that shows this section. This
* is populated lazily.
*/
StoryContentView storyView;
bool nextPrevShown;
ConveyorView sections;
/**
* The set of keys that produce a given behavior (going down one story,
* navigating to the column to the right, etc).
*/
//TODO(jmesserly): we need a key code enumeration
final Set downKeyPresses;
final Set upKeyPresses;
final Set rightKeyPresses;
final Set leftKeyPresses;
final Set openKeyPresses;
final Set backKeyPresses;
final Set nextPageKeyPresses;
final Set previousPageKeyPresses;
FrontView(this.swarm)
: downKeyPresses = new Set.from([74 /*j*/, 40 /*down*/]),
upKeyPresses = new Set.from([75 /*k*/, 38 /*up*/]),
rightKeyPresses = new Set.from([39 /*right*/, 68 /*d*/, 76 /*l*/]),
leftKeyPresses = new Set.from([37 /*left*/, 65 /*a*/, 72 /*h*/]),
openKeyPresses = new Set.from([13 /*enter*/, 79 /*o*/]),
backKeyPresses = new Set.from([8 /*delete*/, 27 /*escape*/]),
nextPageKeyPresses = new Set.from([78 /*n*/]),
previousPageKeyPresses = new Set.from([80 /*p*/]),
nextPrevShown = false,
super('front-view fullpage') {
topView = new CompositeView('top-view', false, false, false);
headerView = new HeaderView(swarm);
topView.addChild(headerView);
sliderMenu = new SliderMenu(swarm.sections.sectionTitles, (sectionTitle) {
swarm.state.moveToNewSection(sectionTitle);
_onSectionSelected(sectionTitle);
// Start with no articles selected.
swarm.state.selectedArticle.value = null;
});
topView.addChild(sliderMenu);
addChild(topView);
bottomView = new CompositeView('bottom-view', false, false, false);
addChild(bottomView);
sections = new ConveyorView();
sections.viewSelected = _onSectionTransitionEnded;
}
SectionView get currentSection {
var view = sections.selectedView;
// TODO(jmesserly): this code works around a bug in the DartC --optimize
if (view == null) {
view = sections.childViews[0];
sections.selectView(view);
}
return view;
}
void afterRender(Element node) {
_createSectionViews();
attachWatch(swarm.state.currentArticle, (e) {
_refreshCurrentArticle();
});
attachWatch(swarm.state.storyMaximized, (e) {
_refreshMaximized();
});
}
void _refreshCurrentArticle() {
if (!swarm.state.inMainView) {
_animateToStory(swarm.state.currentArticle.value);
} else {
_animateToMainView();
}
}
/**
* Animates back from the story view to the main grid view.
*/
void _animateToMainView() {
sliderMenu.removeClass('hidden');
storyView.addClass('hidden-story');
currentSection.storyMode = false;
headerView.startTransitionToMainView();
currentSection.dataSourceView
.reattachSubview(detachedView.source, detachedView, true);
storyView.node.onTransitionEnd.first.then((e) {
currentSection.hidden = false;
// TODO(rnystrom): Should move this "mode" into SwarmState and have
// header view respond to change events itself.
removeChild(storyView);
storyView = null;
detachedView.removeClass('sel');
detachedView = null;
});
}
void _animateToStory(Article item) {
final source = item.dataSource;
if (detachedView != null && detachedView.source != source) {
// Ignore spurious item selection clicks that occur while a data source
// is already selected. These are likely clicks that occur while an
// animation is in progress.
return;
}
if (storyView != null) {
// Remove the old story. This happens if we're already in the Story View
// and the user has clicked to see a new story.
removeChild(storyView);
// Create the new story view and place in the frame.
storyView = addChild(new StoryContentView(swarm, item));
} else {
// We are animating from the main view to the story view.
// TODO(jmesserly): make this code better
final view = currentSection.findView(source);
final newPosition =
FxUtil.computeRelativePosition(view.node, bottomView.node);
currentSection.dataSourceView.detachSubview(view.source);
detachedView = view;
FxUtil.setPosition(view.node, newPosition);
bottomView.addChild(view);
view.addClass('sel');
currentSection.storyMode = true;
// Create the new story view.
storyView = new StoryContentView(swarm, item);
new Timer(const Duration(milliseconds: 0), () {
_animateDataSourceToMinimized();
sliderMenu.addClass('hidden');
// Make the fancy sliding into the window animation.
new Timer(const Duration(milliseconds: 0), () {
storyView.addClass('hidden-story');
addChild(storyView);
new Timer(const Duration(milliseconds: 0), () {
storyView.removeClass('hidden-story');
});
headerView.endTransitionToStoryView();
});
});
}
}
void _refreshMaximized() {
if (swarm.state.storyMaximized.value) {
_animateDataSourceToMaximized();
} else {
_animateDataSourceToMinimized();
}
}
void _animateDataSourceToMaximized() {
FxUtil.setWebkitTransform(topView.node, 0, -HeaderView.HEIGHT);
if (detachedView != null) {
FxUtil.setWebkitTransform(
detachedView.node, 0, -DataSourceView.TAB_ONLY_HEIGHT);
}
}
void _animateDataSourceToMinimized() {
if (detachedView != null) {
FxUtil.setWebkitTransform(detachedView.node, 0, 0);
FxUtil.setWebkitTransform(topView.node, 0, 0);
}
}
/**
* Called when the animation to switch to a section has completed.
*/
void _onSectionTransitionEnded(SectionView selectedView) {
// Show the section and hide the others.
for (SectionView view in sections.childViews) {
if (view == selectedView) {
// Always refresh the sources in case they've changed.
view.showSources();
} else {
// Only show the current view for performance.
view.hideSources();
}
}
}
/**
* Called when the user chooses a section on the SliderMenu. Hides
* all views except the one they want to see.
*/
void _onSectionSelected(String sectionTitle) {
final section = swarm.sections.findSection(sectionTitle);
// Find the view for this section.
for (SectionView view in sections.childViews) {
if (view.section == section) {
// Have the conveyor show it.
sections.selectView(view);
break;
}
}
}
/**
* Create SectionViews for each Section in the app and add them to the
* conveyor. Note that the SectionViews won't actually populate or load data
* sources until they are shown in response to [:_onSectionSelected():].
*/
void _createSectionViews() {
for (final section in swarm.sections) {
final viewFactory = new DataSourceViewFactory(swarm);
final sectionView = new SectionView(swarm, section, viewFactory);
// TODO(rnystrom): Hack temp. Access node to make sure SectionView has
// rendered and created scroller. This can go away when event registration
// is being deferred.
sectionView.node;
sections.addChild(sectionView);
}
addChild(sections);
}
/**
* Controls the logic of how to respond to keypresses and then update the
* UI accordingly.
*/
void processKeyEvent(KeyboardEvent e) {
int code = e.keyCode;
if (swarm.state.inMainView) {
// Option 1: We're in the Main Grid mode.
if (!swarm.state.hasArticleSelected) {
// Then a key has been pressed. Select the first item in the
// top left corner.
swarm.state.goToFirstArticleInSection();
} else if (rightKeyPresses.contains(code)) {
// Store original state that is needed if we need to move
// to the next section.
swarm.state.goToNextFeed();
} else if (leftKeyPresses.contains(code)) {
// Store original state that is needed if we need to move
// to the next section.
swarm.state.goToPreviousFeed();
} else if (downKeyPresses.contains(code)) {
swarm.state.goToNextSelectedArticle();
} else if (upKeyPresses.contains(code)) {
swarm.state.goToPreviousSelectedArticle();
} else if (openKeyPresses.contains(code)) {
// View a story in the larger Story View.
swarm.state.selectStoryAsCurrent();
} else if (nextPageKeyPresses.contains(code)) {
swarm.state.goToNextSection(sliderMenu);
} else if (previousPageKeyPresses.contains(code)) {
swarm.state.goToPreviousSection(sliderMenu);
}
} else {
// Option 2: We're in Story Mode. In this mode, the user can move up
// and down through stories, which automatically loads the next story.
if (downKeyPresses.contains(code)) {
swarm.state.goToNextArticle();
} else if (upKeyPresses.contains(code)) {
swarm.state.goToPreviousArticle();
} else if (backKeyPresses.contains(code)) {
// Move back to the main grid view.
swarm.state.clearCurrentArticle();
}
}
}
}
/** Transitions the app back to the main screen. */
void _backToMain(SwarmState state) {
if (state.currentArticle.value != null) {
state.clearCurrentArticle();
state.storyTextMode.value = true;
state.pushToHistory();
}
}
/** A back button that sends the user back to the front page. */
class SwarmBackButton extends View {
Swarm swarm;
SwarmBackButton(this.swarm);
Element render() => new Element.html('<div class="back-arrow button"></div>');
void afterRender(Element node) {
addOnClick((e) {
_backToMain(swarm.state);
});
}
}
/** Top view constaining the title and standard buttons. */
class HeaderView extends CompositeView {
// TODO(jacobr): make this value be coupled with the CSS file.
static const HEIGHT = 80;
Swarm swarm;
View _title;
View _infoButton;
View _configButton;
View _refreshButton;
SwarmBackButton _backButton;
View _infoDialog;
View _configDialog;
// For (text/web) article view controls
View _webBackButton;
View _webForwardButton;
View _newWindowButton;
HeaderView(this.swarm) : super('header-view') {
_backButton = addChild(new SwarmBackButton(swarm));
_title = addChild(View.div('app-title', 'Swarm'));
_configButton = addChild(View.div('config button'));
_refreshButton = addChild(View.div('refresh button'));
_infoButton = addChild(View.div('info-button button'));
// TODO(rnystrom): No more web/text mode (it's just text) so get rid of
// these.
_webBackButton = addChild(new WebBackButton());
_webForwardButton = addChild(new WebForwardButton());
_newWindowButton = addChild(View.div('new-window-button button'));
}
void afterRender(Element node) {
// Respond to changes to whether the story is being shown as text or web.
attachWatch(swarm.state.storyTextMode, (e) {
refreshWebStoryButtons();
});
_title.addOnClick((e) {
_backToMain(swarm.state);
});
// Wire up the events.
_configButton.addOnClick((e) {
// Bring up the config dialog.
if (this._configDialog == null) {
// TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
this._configDialog = new ConfigHintDialog(swarm.frontView, () {
swarm.frontView.removeChild(this._configDialog);
this._configDialog = null;
// TODO: Need to push these to the server on a per-user basis.
// Update the storage now.
swarm.sections.refresh();
});
swarm.frontView.addChild(this._configDialog);
}
// TODO(jimhug): Graceful redirection to reader.
});
// On click of the refresh button, refresh the swarm.
_refreshButton.addOnClick(EventBatch.wrap((e) {
swarm.refresh();
}));
// On click of the info button, show Dart info page in new window/tab.
_infoButton.addOnClick((e) {
// Bring up the config dialog.
if (this._infoDialog == null) {
// TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
this._infoDialog = new HelpDialog(swarm.frontView, () {
swarm.frontView.removeChild(this._infoDialog);
this._infoDialog = null;
swarm.sections.refresh();
});
swarm.frontView.addChild(this._infoDialog);
}
});
// On click of the new window button, show web article in new window/tab.
_newWindowButton.addOnClick((e) {
String currentArticleSrcUrl = swarm.state.currentArticle.value.srcUrl;
window.open(currentArticleSrcUrl, '_blank');
});
startTransitionToMainView();
}
/**
* Refreshes whether or not the buttons specific to the display of a story in
* the web perspective are visible.
*/
void refreshWebStoryButtons() {
bool webButtonsHidden = true;
if (swarm.state.currentArticle.value != null) {
// Set if web buttons are hidden
webButtonsHidden = swarm.state.storyTextMode.value;
}
_webBackButton.hidden = webButtonsHidden;
_webForwardButton.hidden = webButtonsHidden;
_newWindowButton.hidden = webButtonsHidden;
}
void startTransitionToMainView() {
_title.removeClass('in-story');
_backButton.removeClass('in-story');
_configButton.removeClass('in-story');
_refreshButton.removeClass('in-story');
_infoButton.removeClass('in-story');
refreshWebStoryButtons();
}
void endTransitionToStoryView() {
_title.addClass('in-story');
_backButton.addClass('in-story');
_configButton.addClass('in-story');
_refreshButton.addClass('in-story');
_infoButton.addClass('in-story');
}
}
/** A back button for the web view of a story that is equivalent to clicking
* "back" in the browser. */
// TODO(rnystrom): We have nearly identical versions of this littered through
// the sample apps. Should consolidate into one.
class WebBackButton extends View {
WebBackButton();
Element render() {
return new Element.html('<div class="web-back-button button"></div>');
}
void afterRender(Element node) {
addOnClick((e) {
back();
});
}
/** Equivalent to [window.history.back] */
static void back() {
window.history.back();
}
}
/** A back button for the web view of a story that is equivalent to clicking
* "forward" in the browser. */
// TODO(rnystrom): We have nearly identical versions of this littered through
// the sample apps. Should consolidate into one.
class WebForwardButton extends View {
WebForwardButton();
Element render() {
return new Element.html('<div class="web-forward-button button"></div>');
}
void afterRender(Element node) {
addOnClick((e) {
forward();
});
}
/** Equivalent to [window.history.forward] */
static void forward() {
window.history.forward();
}
}
/**
* A factory that creates a view for data sources.
*/
class DataSourceViewFactory implements ViewFactory<Feed> {
Swarm swarm;
DataSourceViewFactory(this.swarm) {}
View newView(Feed data) => new DataSourceView(data, swarm);
int get width => ArticleViewLayout.getSingleton().width;
int get height => null; // Width for this view isn't known.
}
/**
* A view for the items from a single data source.
* Shows a title and a list of items.
*/
class DataSourceView extends CompositeView {
// TODO(jacobr): make this value be coupled with the CSS file.
static const TAB_ONLY_HEIGHT = 34;
final Feed source;
VariableSizeListView<Article> itemsView;
DataSourceView(this.source, Swarm swarm) : super('query') {
// TODO(jacobr): make the title a view or decide it is sane for a subclass
// of component view to manually add some DOM cruft.
node.nodes.add(new Element.html('<h2>${source.title}</h2>'));
// TODO(jacobr): use named arguments when available.
itemsView = addChild(new VariableSizeListView<Article>(
source.articles,
new ArticleViewFactory(swarm),
true,
/* scrollable */
true,
/* vertical */
swarm.state.currentArticle,
/* selectedItem */
!Device.supportsTouch /* snapToArticles */,
false /* paginate */,
true /* removeClippedViews */,
!Device.supportsTouch /* showScrollbar */));
itemsView.addClass('story-section');
node.nodes.add(new Element.html('<div class="query-name-shadow"></div>'));
// Clicking the view (i.e. its title area) unmaximizes to show the entire
// view.
node.onMouseDown.listen((e) {
swarm.state.storyMaximized.value = false;
});
}
}
/** A button that toggles between states. */
class ToggleButton extends View {
EventListeners onChanged;
List<String> states;
ToggleButton(this.states) : onChanged = new EventListeners();
Element render() => new Element.tag('button');
void afterRender(Element node) {
state = states[0];
node.onClick.listen((event) {
toggle();
});
}
String get state {
final currentState = node.innerHtml;
assert(states.indexOf(currentState, 0) >= 0);
return currentState;
}
void set state(String state) {
assert(states.indexOf(state, 0) >= 0);
node.innerHtml = state;
onChanged.fire(null);
}
void toggle() {
final oldState = state;
int index = states.indexOf(oldState, 0);
index = (index + 1) % states.length;
state = states[index];
}
}
/**
* A factory that creates a view for generic items.
*/
class ArticleViewFactory implements VariableSizeViewFactory<Article> {
Swarm swarm;
ArticleViewLayout layout;
ArticleViewFactory(this.swarm) : layout = ArticleViewLayout.getSingleton();
View newView(Article item) => new ArticleView(item, swarm, layout);
int getWidth(Article item) => layout.width;
int getHeight(Article item) => layout.computeHeight(item);
}
class ArticleViewMetrics {
final int height;
final int titleLines;
final int bodyLines;
const ArticleViewMetrics(this.height, this.titleLines, this.bodyLines);
}
class ArticleViewLayout {
// TODO(terry): clean this up once we have a framework for sharing constants
// between JS and CSS. See bug #5405307.
static const IPAD_WIDTH = 257;
static const DESKTOP_WIDTH = 297;
static const CHROME_OS_WIDTH = 317;
static const TITLE_MARGIN_LEFT = 257 - 150;
static const BODY_MARGIN_LEFT = 257 - 221;
static const LINE_HEIGHT = 18;
static const TITLE_FONT = 'bold 13px arial,sans-serif';
static const BODY_FONT = '13px arial,sans-serif';
static const TOTAL_MARGIN = 16 * 2 + 70;
static const MIN_TITLE_HEIGHT = 36;
static const MAX_TITLE_LINES = 2;
static const MAX_BODY_LINES = 4;
MeasureText measureTitleText;
MeasureText measureBodyText;
int width;
static ArticleViewLayout _singleton;
ArticleViewLayout()
: measureBodyText = new MeasureText(BODY_FONT),
measureTitleText = new MeasureText(TITLE_FONT) {
num screenWidth = window.screen.width;
width = DESKTOP_WIDTH;
}
static ArticleViewLayout getSingleton() {
if (_singleton == null) {
_singleton = new ArticleViewLayout();
}
return _singleton;
}
int computeHeight(Article item) {
if (item == null) {
// TODO(jacobr): find out why this is happening..
print('Null item encountered.');
return 0;
}
return computeLayout(item, null, null).height;
}
/**
* titleContainer and snippetContainer may be null in which case the size is
* computed but no actual layout is performed.
*/
ArticleViewMetrics computeLayout(
Article item, StringBuffer titleBuffer, StringBuffer snippetBuffer) {
int titleWidth = width - BODY_MARGIN_LEFT;
if (item.hasThumbnail) {
titleWidth = width - TITLE_MARGIN_LEFT;
}
final titleLines = measureTitleText.addLineBrokenText(
titleBuffer, item.title, titleWidth, MAX_TITLE_LINES);
final bodyLines = measureBodyText.addLineBrokenText(
snippetBuffer, item.textBody, width - BODY_MARGIN_LEFT, MAX_BODY_LINES);
int height = bodyLines * LINE_HEIGHT + TOTAL_MARGIN;
if (bodyLines == 0) {
height = 92;
}
return new ArticleViewMetrics(height, titleLines, bodyLines);
}
}
/**
* A view for a generic item.
*/
class ArticleView extends View {
// Set to false to make inspecting the HTML more pleasant...
static const SAVE_IMAGES = false;
final Article item;
final Swarm swarm;
final ArticleViewLayout articleLayout;
ArticleView(this.item, this.swarm, this.articleLayout);
Element render() {
Element node;
final byline = item.author.length > 0 ? item.author : item.dataSource.title;
final date = DateUtils.toRecentTimeString(item.date);
String storyClass = 'story no-thumb';
String thumbnail = '';
if (item.hasThumbnail) {
storyClass = 'story';
thumbnail = '<img src="${item.thumbUrl}"></img>';
}
final title = new StringBuffer();
final snippet = new StringBuffer();
// Note: also populates title and snippet elements.
final metrics = articleLayout.computeLayout(item, title, snippet);
node = new Element.html('''
<div class="$storyClass">
$thumbnail
<div class="title">$title</div>
<div class="byline">$byline</div>
<div class="dateline">$date</div>
<div class="snippet">$snippet</div>
</div>''');
// Remove the snippet entirely if it's empty. This keeps it from taking up
// space and pushing the padding down.
if ((item.textBody == null) || (item.textBody.trim() == '')) {
node.querySelector('.snippet').remove();
}
return node;
}
void afterRender(Element node) {
// Select this view's item.
addOnClick((e) {
// Mark the item as read, so it shows as read in other views
item.unread.value = false;
final oldArticle = swarm.state.currentArticle.value;
swarm.state.currentArticle.value = item;
swarm.state.storyTextMode.value = true;
if (oldArticle == null) {
swarm.state.pushToHistory();
}
});
watch(swarm.state.currentArticle, (e) {
if (!swarm.state.inMainView) {
swarm.state.markCurrentAsRead();
}
_refreshSelected(swarm.state.currentArticle);
//TODO(efortuna): add in history stuff while reading articles?
});
watch(swarm.state.selectedArticle, (e) {
_refreshSelected(swarm.state.selectedArticle);
_updateViewForSelectedArticle();
});
watch(item.unread, (e) {
// TODO(rnystrom): Would be nice to do:
// node.classes.set('story-unread', item.unread.value)
if (item.unread.value) {
node.classes.add('story-unread');
} else {
node.classes.remove('story-unread');
}
});
}
/**
* Notify the view to jump to a different area if we are selecting an
* article that is currently outside of the visible area.
*/
void _updateViewForSelectedArticle() {
Article selArticle = swarm.state.selectedArticle.value;
if (swarm.state.hasArticleSelected) {
// Ensure that the selected article is visible in the view.
if (!swarm.state.inMainView) {
// Story View.
swarm.frontView.detachedView.itemsView.showView(selArticle);
} else {
if (swarm.frontView.currentSection.inCurrentView(selArticle)) {
// Scroll horizontally if needed.
swarm.frontView.currentSection.dataSourceView
.showView(selArticle.dataSource);
DataSourceView dataView =
swarm.frontView.currentSection.findView(selArticle.dataSource);
if (dataView != null) {
dataView.itemsView.showView(selArticle);
}
}
}
}
}
String getDataUriForImage(final img) {
// TODO(hiltonc,jimhug) eval perf of this vs. reusing one canvas element
final CanvasElement canvas =
new CanvasElement(height: img.height, width: img.width);
final CanvasRenderingContext2D ctx = canvas.getContext("2d");
ctx.drawImageScaled(img, 0, 0, img.width, img.height);
return canvas.toDataUrl("image/png");
}
/**
* Update this view's selected appearance based on the currently selected
* Article.
*/
void _refreshSelected(curItem) {
if (curItem.value == item) {
addClass('sel');
} else {
removeClass('sel');
}
}
void _saveToStorage(String thumbUrl, ImageElement img) {
// TODO(jimhug): Reimplement caching of images.
}
}
/**
* An internal view of a story as text. In other words, the article is shown
* in-place as opposed to as an embedded web-page.
*/
class StoryContentView extends View {
final Swarm swarm;
final Article item;
View _pagedStory;
StoryContentView(this.swarm, this.item);
get childViews => [_pagedStory];
Element render() {
final storyContent =
new Element.html('<div class="story-content">${item.htmlBody}</div>');
for (Element element in storyContent.querySelectorAll(
"iframe, script, style, object, embed, frameset, frame")) {
element.remove();
}
_pagedStory = new PagedContentView(new View.fromNode(storyContent));
// Modify all links to open in new windows....
// TODO(jacobr): would it be better to add an event listener on click that
// intercepts these instead?
for (AnchorElement anchor in storyContent.querySelectorAll('a')) {
anchor.target = '_blank';
}
final date = DateUtils.toRecentTimeString(item.date);
final container = new Element.html('''
<div class="story-view">
<div class="story-text-view">
<div class="story-header">
<a class="story-title" href="${item.srcUrl}" target="_blank">
${item.title}</a>
<div class="story-byline">
${item.author} - ${item.dataSource.title}
</div>
<div class="story-dateline">$date</div>
</div>
<div class="paged-story"></div>
<div class="spacer"></div>
</div>
</div>''');
container.querySelector('.paged-story').replaceWith(_pagedStory.node);
return container;
}
}
class SectionView extends CompositeView {
final Section section;
final Swarm swarm;
final DataSourceViewFactory _viewFactory;
final View loadingText;
ListView<Feed> dataSourceView;
PageNumberView pageNumberView;
final PageState pageState;
SectionView(this.swarm, this.section, this._viewFactory)
: loadingText = new View.html('<div class="loading-section"></div>'),
pageState = new PageState(),
super('section-view') {
addChild(loadingText);
}
/**
* Hides the loading text, reloads the data sources, and shows them.
*/
void showSources() {
loadingText.node.style.display = 'none';
// Lazy initialize the data source view.
if (dataSourceView == null) {
// TODO(jacobr): use named arguments when available.
dataSourceView = new ListView<Feed>(
section.feeds,
_viewFactory,
true /* scrollable */,
false /* vertical */,
null /* selectedItem */,
true /* snapToItems */,
true /* paginate */,
true /* removeClippedViews */,
false,
/* showScrollbar */
pageState);
dataSourceView.addClass("data-source-view");
addChild(dataSourceView);
pageNumberView = addChild(new PageNumberView(pageState));
node.style.opacity = '1';
} else {
addChild(dataSourceView);
addChild(pageNumberView);
node.style.opacity = '1';
}
// TODO(jacobr): get rid of this call to reconfigure when it is not needed.
dataSourceView.scroller.reconfigure(() {});
}
/**
* Hides the data sources and shows the loading text.
*/
void hideSources() {
if (dataSourceView != null) {
node.style.opacity = '0.6';
removeChild(dataSourceView);
removeChild(pageNumberView);
}
loadingText.node.style.display = 'block';
}
set storyMode(bool inStoryMode) {
if (inStoryMode) {
addClass('hide-all-queries');
} else {
removeClass('hide-all-queries');
}
}
/**
* Find the [DataSourceView] in this SectionView that's displaying the given
* [Feed].
*/
DataSourceView findView(Feed dataSource) {
return dataSourceView.getSubview(dataSourceView.findIndex(dataSource));
}
bool inCurrentView(Article article) {
return dataSourceView.findIndex(article.dataSource) != null;
}
}