Apply dart fix to swarm sample

Change-Id: Ib111d262b0dc2572ce6cb773aed5547fe897b4dd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/252520
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Vijay Menon <vsm@google.com>
This commit is contained in:
Vijay Menon 2022-08-01 15:57:53 +00:00 committed by Commit Bot
parent 48693c6b3d
commit a9092c76ac
53 changed files with 1841 additions and 2262 deletions

View file

@ -6,18 +6,16 @@
part of swarmlib;
/**
* The base class that should be extended by all HTML applications.
*
* It should both be easy to use for users coming over from JavaScript, but
* also offer a clear notion of OO encapsulation.
*
* This class or something similar belongs in the standard DOM library.
*/
/// The base class that should be extended by all HTML applications.
///
/// It should both be easy to use for users coming over from JavaScript, but
/// also offer a clear notion of OO encapsulation.
///
/// This class or something similar belongs in the standard DOM library.
class App {
App() {}
App();
/** Begins executing code in this [App]. */
/// Begins executing code in this [App]. */
void run() {
// If the script is async, by the time we get here the DOM content may
// already be loaded, so waiting on the DOMContentLoaded event is a no-op.
@ -40,12 +38,10 @@ class App {
}
}
/**
* Called when the DOM is fully loaded but potentially before resources.
*
* For most apps, any startup code should be in this method. Be sure to call
* the superclass implementation.
*/
/// Called when the DOM is fully loaded but potentially before resources.
///
/// For most apps, any startup code should be in this method. Be sure to call
/// the superclass implementation.
void onLoad() {
// Prevent the default browser behavior of scrolling the window.
document.onTouchMove.listen((Event event) => event.preventDefault());
@ -57,11 +53,9 @@ class App {
}
}
/**
* Erase the static splash screen.
*
* Assumption: if a splash screen exists, an element #appSplash contains it.
*/
/// Erase the static splash screen.
///
/// Assumption: if a splash screen exists, an element #appSplash contains it.
void eraseSplashScreen() {
final splash = document.querySelector("#appSplash");
// Delete it if found, but it's okay for it not to be -- maybe
@ -71,10 +65,8 @@ class App {
}
}
/**
* Swaps and reloads the app cache if an update is ready. Returns false if
* an update is not ready.
*/
/// Swaps and reloads the app cache if an update is ready. Returns false if
/// an update is not ready.
bool swapAndReloadCache() {
ApplicationCache appCache = window.applicationCache;
if (!identical(appCache.status, ApplicationCache.UPDATEREADY)) {
@ -88,7 +80,7 @@ class App {
return true;
}
/** Returns true if we are running as a packaged application. */
/// Returns true if we are running as a packaged application. */
static bool get isPackaged {
return window.location.protocol == 'chrome-extension:';
}

View file

@ -2,33 +2,25 @@
part of swarmlib;
/**
* An iterator that allows the user to move forward and backward though
* a set of items. (Bi-directional)
*/
/// An iterator that allows the user to move forward and backward though
/// a set of items. (Bi-directional)
class BiIterator<E> {
/**
* Provides forward and backward iterator functionality to keep track
* which item is currently selected.
*/
/// Provides forward and backward iterator functionality to keep track
/// which item is currently selected.
ObservableValue<int> currentIndex;
/**
* The collection of items we will be iterating through.
*/
/// The collection of items we will be iterating through.
List<E> list;
BiIterator(this.list, [List<ChangeListener> oldListeners = null])
: currentIndex = new ObservableValue<int>(0) {
BiIterator(this.list, [List<ChangeListener> oldListeners])
: currentIndex = ObservableValue<int>(0) {
if (oldListeners != null) {
currentIndex.listeners = oldListeners;
}
}
/**
* Returns the next section from the sections, given the current
* position. Returns the last source if there is no next section.
*/
/// Returns the next section from the sections, given the current
/// position. Returns the last source if there is no next section.
E next() {
if (currentIndex.value < list.length - 1) {
currentIndex.value += 1;
@ -36,19 +28,15 @@ class BiIterator<E> {
return list[currentIndex.value];
}
/**
* Returns the current Section (page in the UI) that the user is
* looking at.
*/
/// Returns the current Section (page in the UI) that the user is
/// looking at.
E get current {
return list[currentIndex.value];
}
/**
* Returns the previous section from the sections, given the current
* position. Returns the front section if we are already at the front of
* the list.
*/
/// Returns the previous section from the sections, given the current
/// position. Returns the front section if we are already at the front of
/// the list.
E previous() {
if (currentIndex.value > 0) {
currentIndex.value -= 1;
@ -56,9 +44,7 @@ class BiIterator<E> {
return list[currentIndex.value];
}
/**
* Move the iterator pointer over so that it points to a given list item.
*/
/// Move the iterator pointer over so that it points to a given list item.
void jumpToValue(E val) {
for (int i = 0; i < list.length; i++) {
if (identical(list[i], val)) {

View file

@ -8,7 +8,7 @@ part of swarmlib;
// TODO(jimhug): Work out correct copyright for this file.
class CannedData {
static const Map<String, String> data = const {
static const Map<String, String> data = {
'Test0_0_0.html': 'Lorem ipsum something or other...',
'Test0_0_1.html': 'Lorem ipsum something or other...',
'Test0_0_2.html': 'Lorem ipsum something or other...',

View file

@ -14,18 +14,19 @@ class ConfigHintDialog extends DialogView {
factory ConfigHintDialog(CompositeView parent, Function doneHandler) {
View content = ConfigHintDialog.makeContent();
return new ConfigHintDialog._impl(parent, doneHandler, content);
return ConfigHintDialog._impl(parent, doneHandler, content);
}
ConfigHintDialog._impl(this._parent, this._doneHandler, View content)
: super('Feed configuration', '', content);
@override
void onDone() {
_doneHandler();
}
static View makeContent() {
return new View.html('''
return View.html('''
<div>
Add or remove feeds in
<a href="https://www.google.com/reader" target="_blank">

View file

@ -7,7 +7,7 @@
part of swarmlib;
/** The top-level collection of all sections for a user. */
/// The top-level collection of all sections for a user. */
// TODO(jimhug): This is known as UserData in the server model.
class Sections extends IterableBase<Section> {
final List<Section> _sections;
@ -16,6 +16,7 @@ class Sections extends IterableBase<Section> {
operator [](int i) => _sections[i];
@override
int get length => _sections.length;
List<String> get sectionTitles => _sections.map((s) => s.title).toList();
@ -24,15 +25,14 @@ class Sections extends IterableBase<Section> {
// TODO(jimhug): http://b/issue?id=5351067
}
/**
* Find the Section object that has a given title.
* This is used to integrate well with [ConveyorView].
*/
/// Find the Section object that has a given title.
/// This is used to integrate well with [ConveyorView].
Section findSection(String name) {
return CollectionUtils.find(_sections, (sect) => sect.title == name);
}
// TODO(jimhug): Track down callers!
@override
Iterator<Section> get iterator => _sections.iterator;
// TODO(jimhug): Better support for switching between local dev and server.
@ -47,19 +47,19 @@ class Sections extends IterableBase<Section> {
}
// This method is exposed for tests.
static void initializeFromData(String data, void callback(Sections sects)) {
final decoder = new Decoder(data);
static void initializeFromData(String data, void Function(Sections sects) callback) {
final decoder = Decoder(data);
int nSections = decoder.readInt();
final sections = new List<Section>();
final sections = <Section>[];
for (int i = 0; i < nSections; i++) {
sections.add(Section.decode(decoder));
}
callback(new Sections(sections));
callback(Sections(sections));
}
static void initializeFromUrl(
bool useCannedData, void callback(Sections sections)) {
bool useCannedData, void Function(Sections sections) callback) {
if (Sections.runningFromFile || useCannedData) {
initializeFromData(CannedData.data['user.data'], callback);
} else {
@ -79,9 +79,7 @@ class Sections extends IterableBase<Section> {
return CollectionUtils.find(_sections, (section) => section.id == id);
}
/**
* Given the name of a section, find its index in the set.
*/
/// Given the name of a section, find its index in the set.
int findSectionIndex(String name) {
for (int i = 0; i < _sections.length; i++) {
if (name == _sections[i].title) {
@ -94,10 +92,11 @@ class Sections extends IterableBase<Section> {
List<Section> get sections => _sections;
// TODO(jmesserly): this should be a property
@override
bool get isEmpty => length == 0;
}
/** A collection of data sources representing a page in the UI. */
/// A collection of data sources representing a page in the UI. */
class Section {
final String id;
final String title;
@ -117,11 +116,11 @@ class Section {
final sectionTitle = decoder.readString();
final nSources = decoder.readInt();
final feeds = new ObservableList<Feed>();
final feeds = ObservableList<Feed>();
for (int j = 0; j < nSources; j++) {
feeds.add(Feed.decode(decoder));
}
return new Section(sectionId, sectionTitle, feeds);
return Section(sectionId, sectionTitle, feeds);
}
Feed findFeed(String id_) {
@ -129,7 +128,7 @@ class Section {
}
}
/** Provider of a news feed. */
/// Provider of a news feed. */
class Feed {
String id;
final String title;
@ -138,15 +137,15 @@ class Feed {
ObservableList<Article> articles;
ObservableValue<bool> error; // TODO(jimhug): Check if dead code.
Feed(this.id, this.title, this.iconUrl, {this.description: ''})
: articles = new ObservableList<Article>(),
error = new ObservableValue<bool>(false);
Feed(this.id, this.title, this.iconUrl, {this.description = ''})
: articles = ObservableList<Article>(),
error = ObservableValue<bool>(false);
static Feed decode(Decoder decoder) {
final sourceId = decoder.readString();
final sourceTitle = decoder.readString();
final sourceIcon = decoder.readString();
final feed = new Feed(sourceId, sourceTitle, sourceIcon);
final feed = Feed(sourceId, sourceTitle, sourceIcon);
final nItems = decoder.readInt();
for (int i = 0; i < nItems; i++) {
@ -162,7 +161,7 @@ class Feed {
void refresh() {}
}
/** A single article or posting to display. */
/// A single article or posting to display. */
class Article {
final String id;
DateTime date;
@ -179,9 +178,9 @@ class Article {
Article(this.dataSource, this.id, this.date, this.title, this.author,
this.srcUrl, this.hasThumbnail, this.textBody,
{htmlBody: null, bool unread: true, this.error: false})
: unread = new ObservableValue<bool>(unread),
this._htmlBody = htmlBody;
{htmlBody, bool unread = true, this.error = false})
: unread = ObservableValue<bool>(unread),
_htmlBody = htmlBody;
String get htmlBody {
_ensureLoaded();
@ -198,7 +197,7 @@ class Article {
String get thumbUrl {
if (!hasThumbnail) return null;
var home;
String home;
if (Sections.runningFromFile) {
home = 'http://dart.googleplex.com';
} else {
@ -221,7 +220,7 @@ class Article {
_htmlBody = CannedData.data[name];
} else {
// TODO(jimhug): Remove this truly evil synchronoush xhr.
final req = new HttpRequest();
final req = HttpRequest();
req.open('GET', 'data/$name', async: false);
req.send();
_htmlBody = req.responseText;
@ -236,9 +235,9 @@ class Article {
final author = decoder.readString();
final dateInSeconds = decoder.readInt();
final snippet = decoder.readString();
final date = new DateTime.fromMillisecondsSinceEpoch(dateInSeconds * 1000,
final date = DateTime.fromMillisecondsSinceEpoch(dateInSeconds * 1000,
isUtc: true);
return new Article(
return Article(
source, id, date, title, author, srcUrl, hasThumbnail, snippet);
}
}

View file

@ -12,7 +12,7 @@ class Decoder {
String data;
Decoder(this.data) {
this.index = 0;
index = 0;
}
// Reads numbers in variable-length 7-bit encoding. This matches the

View file

@ -6,25 +6,24 @@
part of swarmlib;
/**
* An informational dialog that shows keyboard shortcuts and provides a
* link to the Dart language webpage.
*/
/// An informational dialog that shows keyboard shortcuts and provides a
/// link to the Dart language webpage.
//TODO(efortuna): fix DialogView so it doesn't require the HTML passed to
// the constructor.
class HelpDialog extends DialogView {
CompositeView _parent;
Function _doneHandler;
final CompositeView _parent;
final Function _doneHandler;
HelpDialog(this._parent, this._doneHandler)
: super('Information', '', makeContent());
@override
void onDone() {
_doneHandler();
}
static View makeContent() {
return new View.html('''
return View.html('''
<div>
<p>
@ -46,40 +45,40 @@ class HelpDialog extends DialogView {
String cellStart = '''<th valign="middle" align="center">''';
return '''<table width="90%" border=1 cellspacing="0" cellpadding="2">
<tr bgcolor="#c3d9ff">
${cellStart} Shortcut Key </th>
${cellStart} Action </th>
$cellStart Shortcut Key </th>
$cellStart Action </th>
</tr>
<tr>
${cellStart} j, &lt;down arrow&gt; </th>
${cellStart} Next Article </th>
$cellStart j, &lt;down arrow&gt; </th>
$cellStart Next Article </th>
</tr>
<tr>
${cellStart} k, &lt;up arrow&gt; </th>
${cellStart} Previous Article </th>
$cellStart k, &lt;up arrow&gt; </th>
$cellStart Previous Article </th>
</tr>
<tr>
${cellStart} o, &lt;enter&gt; </th>
${cellStart} Open Article </th>
$cellStart o, &lt;enter&gt; </th>
$cellStart Open Article </th>
</tr>
<tr>
${cellStart} &lt;esc&gt;, &lt;delete&gt; </th>
${cellStart} Back </th>
$cellStart &lt;esc&gt;, &lt;delete&gt; </th>
$cellStart Back </th>
</tr>
<tr>
${cellStart} a, h, &lt;left arrow&gt; </th>
${cellStart} Left </th>
$cellStart a, h, &lt;left arrow&gt; </th>
$cellStart Left </th>
</tr>
<tr>
${cellStart} d, l, &lt;right arrow&gt; </th>
${cellStart} Right </th>
$cellStart d, l, &lt;right arrow&gt; </th>
$cellStart Right </th>
</tr>
<tr>
${cellStart} n </th>
${cellStart} Next Category </th>
$cellStart n </th>
$cellStart Next Category </th>
</tr>
<tr>
${cellStart} p </th>
${cellStart} Previous Category </th>
$cellStart p </th>
$cellStart Previous Category </th>
</tr>
</table>''';

View file

@ -6,29 +6,25 @@
part of swarmlib;
/**
* A simple news reader in Dart.
*/
/// A simple news reader in Dart.
class Swarm extends App {
/**
* Flag to insure the onLoad isn't called when callback from initializeFromUrl
* could occur before the document's onload event.
*/
/// Flag to insure the onLoad isn't called when callback from initializeFromUrl
/// could occur before the document's onload event.
bool onLoadFired;
/** Collections of datafeeds to show per page. */
/// Collections of datafeeds to show per page. */
Sections sections;
/** The front page of the app. */
/// The front page of the app. */
FrontView frontView;
/** Observable UI state. */
/// Observable UI state. */
SwarmState state;
Swarm({bool useCannedData: false}) : onLoadFired = false {
Swarm({bool useCannedData = false}) : onLoadFired = false {
Sections.initializeFromUrl(useCannedData, (currSections) {
sections = currSections;
state = new SwarmState(sections);
state = SwarmState(sections);
setupApp();
});
// Catch user keypresses and decide whether to use them for the
@ -40,16 +36,14 @@ class Swarm extends App {
});
}
/**
* Tells each data source to check the server for the latest data.
*/
/// Tells each data source to check the server for the latest data.
void refresh() {
sections.refresh();
// Hook up listeners about any data source additions or deletions. We don't
// differentiate additions or deletions just the fact that data feeds have
// changed. We might want more fidelity later.
sections.sectionTitles.forEach((title) {
for (var title in sections.sectionTitles) {
Section section = sections.findSection(title);
// TODO(terry): addChangeListener needs to return an id so previous
// listener can be removed, otherwise anonymous functions
@ -58,19 +52,18 @@ class Swarm extends App {
// TODO(jacobr): implement this.
print("Refresh sections not impl yet.");
});
});
}
}
/** The page load event handler. */
/// The page load event handler. */
@override
void onLoad() {
onLoadFired = true;
super.onLoad();
setupApp();
}
/**
* Setup the application's world.
*/
/// Setup the application's world.
void setupApp() {
// TODO(terry): Should be able to spinup the app w/o waiting for data.
// If the document is already loaded so we can setup the app anytime.
@ -85,7 +78,7 @@ class Swarm extends App {
}
void render() {
frontView = new FrontView(this);
frontView = FrontView(this);
frontView.addToDocument(document.body);
}
}

View file

@ -6,79 +6,62 @@
part of swarmlib;
/**
* The top-level class for the UI state. UI state is essentially a "model" from
* the view's perspective but whose data just describes the UI itself. It
* contains data like the currently selected story, etc.
*/
/// The top-level class for the UI state. UI state is essentially a "model" from
/// the view's perspective but whose data just describes the UI itself. It
/// contains data like the currently selected story, etc.
// TODO(jimhug): Split the two classes here into framework and app-specific.
class SwarmState extends UIState {
/** Core data source for the app. */
/// Core data source for the app. */
final Sections _dataModel;
/**
* Which article the user is currently viewing, or null if they aren't
* viewing an Article.
*/
/// Which article the user is currently viewing, or null if they aren't
/// viewing an Article.
final ObservableValue<Article> currentArticle;
/**
* Which article the user currently has selected (for traversing articles
* via keyboard shortcuts).
*/
/// Which article the user currently has selected (for traversing articles
/// via keyboard shortcuts).
final ObservableValue<Article> selectedArticle;
/**
* True if the story view is maximized and the top and bottom UI elements
* are hidden.
*/
/// True if the story view is maximized and the top and bottom UI elements
/// are hidden.
final ObservableValue<bool> storyMaximized;
/**
* True if the maximized story, if any, is being displayed in text mode
* rather than as an embedded web-page.
*/
/// True if the maximized story, if any, is being displayed in text mode
/// rather than as an embedded web-page.
final ObservableValue<bool> storyTextMode;
/**
* Which article the user currently has selected (by keyboard shortcuts),
* or null if an article isn't selected by the keyboard.
*/
/// Which article the user currently has selected (by keyboard shortcuts),
/// or null if an article isn't selected by the keyboard.
BiIterator<Article> _articleIterator;
/**
* Which feed is currently selected (for keyboard shortcuts).
*/
/// Which feed is currently selected (for keyboard shortcuts).
BiIterator<Feed> _feedIterator;
/**
* Which section is currently selected (for keyboard shortcuts).
*/
/// Which section is currently selected (for keyboard shortcuts).
BiIterator<Section> _sectionIterator;
SwarmState(this._dataModel)
: currentArticle = new ObservableValue<Article>(null),
selectedArticle = new ObservableValue<Article>(null),
storyMaximized = new ObservableValue<bool>(false),
storyTextMode = new ObservableValue<bool>(true) {
: currentArticle = ObservableValue<Article>(null),
selectedArticle = ObservableValue<Article>(null),
storyMaximized = ObservableValue<bool>(false),
storyTextMode = ObservableValue<bool>(true) {
startHistoryTracking();
// TODO(efortuna): consider having this class just hold observable
// currentIndecies instead of iterators with observablevalues..
_sectionIterator = new BiIterator<Section>(_dataModel.sections);
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds);
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles);
_sectionIterator = BiIterator<Section>(_dataModel.sections);
_feedIterator = BiIterator<Feed>(_sectionIterator.current.feeds);
_articleIterator = BiIterator<Article>(_feedIterator.current.articles);
currentArticle.addChangeListener((e) {
_articleIterator.jumpToValue(currentArticle.value);
});
}
/**
* Registers an event to fire on any state change
*
* TODO(jmesserly): fix this so we don't have to enumerate all of our fields
* again. One idea here is UIState becomes Observable, Observables have
* parents and notifications bubble up the parent chain.
*/
/// Registers an event to fire on any state change
///
/// TODO(jmesserly): fix this so we don't have to enumerate all of our fields
/// again. One idea here is UIState becomes Observable, Observables have
/// parents and notifications bubble up the parent chain.
void addChangeListener(ChangeListener listener) {
_sectionIterator.currentIndex.addChangeListener(listener);
_feedIterator.currentIndex.addChangeListener(listener);
@ -86,6 +69,7 @@ class SwarmState extends UIState {
currentArticle.addChangeListener(listener);
}
@override
Map<String, String> toHistory() {
final data = {};
data['section'] = currentSection.id;
@ -96,97 +80,83 @@ class SwarmState extends UIState {
return data;
}
@override
void loadFromHistory(Map values) {
// TODO(jimhug): There's a better way of doing this...
if (values['section'] != null) {
_sectionIterator
.jumpToValue(_dataModel.findSectionById(values['section']));
} else {
_sectionIterator = new BiIterator<Section>(_dataModel.sections);
_sectionIterator = BiIterator<Section>(_dataModel.sections);
}
if (values['feed'] != null && currentSection != null) {
_feedIterator.jumpToValue(currentSection.findFeed(values['feed']));
} else {
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds);
_feedIterator = BiIterator<Feed>(_sectionIterator.current.feeds);
}
if (values['article'] != null && currentFeed != null) {
currentArticle.value = currentFeed.findArticle(values['article']);
_articleIterator.jumpToValue(currentArticle.value);
} else {
_articleIterator =
new BiIterator<Article>(_feedIterator.current.articles);
_articleIterator = BiIterator<Article>(_feedIterator.current.articles);
currentArticle.value = null;
}
storyMaximized.value = false;
}
/**
* Move the currentArticle pointer to the next item in the Feed.
*/
/// Move the currentArticle pointer to the next item in the Feed.
void goToNextArticle() {
currentArticle.value = _articleIterator.next();
selectedArticle.value = _articleIterator.current;
}
/**
* Move the currentArticle pointer to the previous item in the Feed.
*/
/// Move the currentArticle pointer to the previous item in the Feed.
void goToPreviousArticle() {
currentArticle.value = _articleIterator.previous();
selectedArticle.value = _articleIterator.current;
}
/**
* Move the selectedArticle pointer to the next item in the Feed.
*/
/// Move the selectedArticle pointer to the next item in the Feed.
void goToNextSelectedArticle() {
selectedArticle.value = _articleIterator.next();
}
/**
* Move the selectedArticle pointer to the previous item in the Feed.
*/
/// Move the selectedArticle pointer to the previous item in the Feed.
void goToPreviousSelectedArticle() {
selectedArticle.value = _articleIterator.previous();
}
/**
* Move the pointers for selectedArticle to point to the next
* Feed.
*/
/// Move the pointers for selectedArticle to point to the next
/// Feed.
void goToNextFeed() {
var newFeed = _feedIterator.next();
int oldIndex = _articleIterator.currentIndex.value;
_articleIterator = new BiIterator<Article>(
_articleIterator = BiIterator<Article>(
newFeed.articles, _articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
/**
* Move the pointers for selectedArticle to point to the previous
* DataSource.
*/
/// Move the pointers for selectedArticle to point to the previous
/// DataSource.
void goToPreviousFeed() {
var newFeed = _feedIterator.previous();
int oldIndex = _articleIterator.currentIndex.value;
_articleIterator = new BiIterator<Article>(
_articleIterator = BiIterator<Article>(
newFeed.articles, _articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
/**
* Move to the next section (page) of feeds in the UI.
* @param index the previous index (how far down in a given feed)
* from the Source we are moving from.
* This method takes sliderMenu in the event that it needs to move
* to a previous section, it can notify the UI to update.
*/
/// Move to the next section (page) of feeds in the UI.
/// @param index the previous index (how far down in a given feed)
/// from the Source we are moving from.
/// This method takes sliderMenu in the event that it needs to move
/// to a previous section, it can notify the UI to update.
void goToNextSection(SliderMenu sliderMenu) {
//TODO(efortuna): move sections?
var oldSection = currentSection;
@ -195,24 +165,22 @@ class SwarmState extends UIState {
// This check prevents our selector from wrapping around when we try to
// go to the "next section", but we're already at the last section.
if (oldSection != _sectionIterator.current) {
_feedIterator = new BiIterator<Feed>(
_feedIterator = BiIterator<Feed>(
_sectionIterator.current.feeds, _feedIterator.currentIndex.listeners);
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles,
_articleIterator = BiIterator<Article>(_feedIterator.current.articles,
_articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
}
/**
* Move to the previous section (page) of feeds in the UI.
* @param index the previous index (how far down in a given feed)
* from the Source we are moving from.
* @param oldSection the original starting section (before the slider
* menu moved)
* This method takes sliderMenu in the event that it needs to move
* to a previous section, it can notify the UI to update.
*/
/// Move to the previous section (page) of feeds in the UI.
/// @param index the previous index (how far down in a given feed)
/// from the Source we are moving from.
/// @param oldSection the original starting section (before the slider
/// menu moved)
/// This method takes sliderMenu in the event that it needs to move
/// to a previous section, it can notify the UI to update.
void goToPreviousSection(SliderMenu sliderMenu) {
//TODO(efortuna): don't pass sliderMenu here. Just update in view!
var oldSection = currentSection;
@ -222,72 +190,58 @@ class SwarmState extends UIState {
// This check prevents our selector from wrapping around when we try to
// go to the "previous section", but we're already at the first section.
if (oldSection != _sectionIterator.current) {
_feedIterator = new BiIterator<Feed>(
_feedIterator = BiIterator<Feed>(
_sectionIterator.current.feeds, _feedIterator.currentIndex.listeners);
// Jump to back of feed set if we are moving backwards through sections.
_feedIterator.currentIndex.value = _feedIterator.list.length - 1;
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles,
_articleIterator = BiIterator<Article>(_feedIterator.current.articles,
_articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
}
/**
* Set the selected story as the current story (for viewing in the larger
* Story View.)
*/
/// Set the selected story as the current story (for viewing in the larger
/// Story View.)
void selectStoryAsCurrent() {
currentArticle.value = _articleIterator.current;
selectedArticle.value = _articleIterator.current;
}
/**
* Remove our currentArticle selection, to move back to the Main Grid view.
*/
/// Remove our currentArticle selection, to move back to the Main Grid view.
void clearCurrentArticle() {
currentArticle.value = null;
}
/**
* Set the selectedArticle as the first item in that section (UI page).
*/
/// Set the selectedArticle as the first item in that section (UI page).
void goToFirstArticleInSection() {
selectedArticle.value = _articleIterator.current;
}
/**
* Returns true if the UI is currently in the Story View state.
*/
/// Returns true if the UI is currently in the Story View state.
bool get inMainView => currentArticle.value == null;
/**
* Returns true if we currently have an Article selected (for keyboard
* shortcuts browsing).
*/
/// Returns true if we currently have an Article selected (for keyboard
/// shortcuts browsing).
bool get hasArticleSelected => selectedArticle.value != null;
/**
* Mark the current article as read
*/
/// Mark the current article as read
bool markCurrentAsRead() {
currentArticle.value.unread.value = false;
}
/**
* The user has moved to a new section (page). This can occur either
* if the user clicked on a section page, or used keyboard shortcuts.
* The default behavior is to move to the first article in the first
* column. The location of the selected item depends on the previous
* selected item location if the user used keyboard shortcuts. These
* are manipulated in goToPrevious/NextSection().
*/
/// The user has moved to a new section (page). This can occur either
/// if the user clicked on a section page, or used keyboard shortcuts.
/// The default behavior is to move to the first article in the first
/// column. The location of the selected item depends on the previous
/// selected item location if the user used keyboard shortcuts. These
/// are manipulated in goToPrevious/NextSection().
void moveToNewSection(String sectionTitle) {
_sectionIterator.currentIndex.value =
_dataModel.findSectionIndex(sectionTitle);
_feedIterator = new BiIterator<Feed>(
_feedIterator = BiIterator<Feed>(
_sectionIterator.current.feeds, _feedIterator.currentIndex.listeners);
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles,
_articleIterator = BiIterator<Article>(_feedIterator.current.articles,
_articleIterator.currentIndex.listeners);
}

View file

@ -10,42 +10,35 @@ part of swarmlib;
// and then doing a large pass to remove functionality that doesn't make sense
// given the UI layout.
/**
* Front page of Swarm.
*/
/// 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. */
/// 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. */
/// 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.
*/
/// 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.
*/
/// 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).
*/
/// 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;
@ -57,22 +50,22 @@ class FrontView extends CompositeView {
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*/]),
: downKeyPresses = {74 /*j*/, 40 /*down*/},
upKeyPresses = {75 /*k*/, 38 /*up*/},
rightKeyPresses = {39 /*right*/, 68 /*d*/, 76 /*l*/},
leftKeyPresses = {37 /*left*/, 65 /*a*/, 72 /*h*/},
openKeyPresses = {13 /*enter*/, 79 /*o*/},
backKeyPresses = {8 /*delete*/, 27 /*escape*/},
nextPageKeyPresses = {78 /*n*/},
previousPageKeyPresses = {80 /*p*/},
nextPrevShown = false,
super('front-view fullpage') {
topView = new CompositeView('top-view', false, false, false);
topView = CompositeView('top-view', false, false, false);
headerView = new HeaderView(swarm);
headerView = HeaderView(swarm);
topView.addChild(headerView);
sliderMenu = new SliderMenu(swarm.sections.sectionTitles, (sectionTitle) {
sliderMenu = SliderMenu(swarm.sections.sectionTitles, (sectionTitle) {
swarm.state.moveToNewSection(sectionTitle);
_onSectionSelected(sectionTitle);
// Start with no articles selected.
@ -81,10 +74,10 @@ class FrontView extends CompositeView {
topView.addChild(sliderMenu);
addChild(topView);
bottomView = new CompositeView('bottom-view', false, false, false);
bottomView = CompositeView('bottom-view', false, false, false);
addChild(bottomView);
sections = new ConveyorView();
sections = ConveyorView();
sections.viewSelected = _onSectionTransitionEnded;
}
@ -98,6 +91,7 @@ class FrontView extends CompositeView {
return view;
}
@override
void afterRender(Element node) {
_createSectionViews();
attachWatch(swarm.state.currentArticle, (e) {
@ -116,9 +110,7 @@ class FrontView extends CompositeView {
}
}
/**
* Animates back from the story view to the main grid view.
*/
/// Animates back from the story view to the main grid view.
void _animateToMainView() {
sliderMenu.removeClass('hidden');
storyView.addClass('hidden-story');
@ -156,7 +148,7 @@ class FrontView extends CompositeView {
removeChild(storyView);
// Create the new story view and place in the frame.
storyView = addChild(new StoryContentView(swarm, item));
storyView = addChild(StoryContentView(swarm, item));
} else {
// We are animating from the main view to the story view.
// TODO(jmesserly): make this code better
@ -173,16 +165,16 @@ class FrontView extends CompositeView {
currentSection.storyMode = true;
// Create the new story view.
storyView = new StoryContentView(swarm, item);
new Timer(const Duration(milliseconds: 0), () {
storyView = StoryContentView(swarm, item);
Timer(const Duration(milliseconds: 0), () {
_animateDataSourceToMinimized();
sliderMenu.addClass('hidden');
// Make the fancy sliding into the window animation.
new Timer(const Duration(milliseconds: 0), () {
Timer(const Duration(milliseconds: 0), () {
storyView.addClass('hidden-story');
addChild(storyView);
new Timer(const Duration(milliseconds: 0), () {
Timer(const Duration(milliseconds: 0), () {
storyView.removeClass('hidden-story');
});
headerView.endTransitionToStoryView();
@ -214,9 +206,7 @@ class FrontView extends CompositeView {
}
}
/**
* Called when the animation to switch to a section has completed.
*/
/// 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) {
@ -230,10 +220,8 @@ class FrontView extends CompositeView {
}
}
/**
* Called when the user chooses a section on the SliderMenu. Hides
* all views except the one they want to see.
*/
/// 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.
@ -246,15 +234,13 @@ class FrontView extends CompositeView {
}
}
/**
* 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():].
*/
/// 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);
final viewFactory = DataSourceViewFactory(swarm);
final sectionView = 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
@ -266,10 +252,8 @@ class FrontView extends CompositeView {
addChild(sections);
}
/**
* Controls the logic of how to respond to keypresses and then update the
* UI accordingly.
*/
/// 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) {
@ -313,7 +297,7 @@ class FrontView extends CompositeView {
}
}
/** Transitions the app back to the main screen. */
/// Transitions the app back to the main screen. */
void _backToMain(SwarmState state) {
if (state.currentArticle.value != null) {
state.clearCurrentArticle();
@ -322,14 +306,16 @@ void _backToMain(SwarmState state) {
}
}
/** A back button that sends the user back to the front page. */
/// 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>');
@override
Element render() => Element.html('<div class="back-arrow button"></div>');
@override
void afterRender(Element node) {
addOnClick((e) {
_backToMain(swarm.state);
@ -337,7 +323,7 @@ class SwarmBackButton extends View {
}
}
/** Top view constaining the title and standard buttons. */
/// 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;
@ -357,7 +343,7 @@ class HeaderView extends CompositeView {
View _newWindowButton;
HeaderView(this.swarm) : super('header-view') {
_backButton = addChild(new SwarmBackButton(swarm));
_backButton = addChild(SwarmBackButton(swarm));
_title = addChild(View.div('app-title', 'Swarm'));
_configButton = addChild(View.div('config button'));
_refreshButton = addChild(View.div('refresh button'));
@ -365,11 +351,12 @@ class HeaderView extends CompositeView {
// TODO(rnystrom): No more web/text mode (it's just text) so get rid of
// these.
_webBackButton = addChild(new WebBackButton());
_webForwardButton = addChild(new WebForwardButton());
_webBackButton = addChild(WebBackButton());
_webForwardButton = addChild(WebForwardButton());
_newWindowButton = addChild(View.div('new-window-button button'));
}
@override
void afterRender(Element node) {
// Respond to changes to whether the story is being shown as text or web.
attachWatch(swarm.state.storyTextMode, (e) {
@ -383,18 +370,18 @@ class HeaderView extends CompositeView {
// Wire up the events.
_configButton.addOnClick((e) {
// Bring up the config dialog.
if (this._configDialog == null) {
if (_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;
_configDialog = ConfigHintDialog(swarm.frontView, () {
swarm.frontView.removeChild(_configDialog);
_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);
swarm.frontView.addChild(_configDialog);
}
// TODO(jimhug): Graceful redirection to reader.
});
@ -407,16 +394,16 @@ class HeaderView extends CompositeView {
// 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) {
if (_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;
_infoDialog = HelpDialog(swarm.frontView, () {
swarm.frontView.removeChild(_infoDialog);
_infoDialog = null;
swarm.sections.refresh();
});
swarm.frontView.addChild(this._infoDialog);
swarm.frontView.addChild(_infoDialog);
}
});
@ -429,10 +416,8 @@ class HeaderView extends CompositeView {
startTransitionToMainView();
}
/**
* Refreshes whether or not the buttons specific to the display of a story in
* the web perspective are visible.
*/
/// Refreshes whether or not the buttons specific to the display of a story in
/// the web perspective are visible.
void refreshWebStoryButtons() {
bool webButtonsHidden = true;
@ -467,70 +452,73 @@ class HeaderView extends CompositeView {
}
}
/** A back button for the web view of a story that is equivalent to clicking
* "back" in the browser. */
/// 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();
@override
Element render() {
return new Element.html('<div class="web-back-button button"></div>');
return Element.html('<div class="web-back-button button"></div>');
}
@override
void afterRender(Element node) {
addOnClick((e) {
back();
});
}
/** Equivalent to [window.history.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. */
/// 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();
@override
Element render() {
return new Element.html('<div class="web-forward-button button"></div>');
return Element.html('<div class="web-forward-button button"></div>');
}
@override
void afterRender(Element node) {
addOnClick((e) {
forward();
});
}
/** Equivalent to [window.history.forward] */
/// Equivalent to [window.history.forward] */
static void forward() {
window.history.forward();
}
}
/**
* A factory that creates a view for data sources.
*/
/// A factory that creates a view for data sources.
class DataSourceViewFactory implements ViewFactory<Feed> {
Swarm swarm;
DataSourceViewFactory(this.swarm) {}
DataSourceViewFactory(this.swarm);
View newView(Feed data) => new DataSourceView(data, swarm);
@override
View newView(Feed data) => DataSourceView(data, swarm);
@override
int get width => ArticleViewLayout.getSingleton().width;
@override
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.
*/
/// 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;
@ -541,12 +529,12 @@ class DataSourceView extends CompositeView {
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>'));
node.nodes.add(Element.html('<h2>${source.title}</h2>'));
// TODO(jacobr): use named arguments when available.
itemsView = addChild(new VariableSizeListView<Article>(
itemsView = addChild(VariableSizeListView<Article>(
source.articles,
new ArticleViewFactory(swarm),
ArticleViewFactory(swarm),
true,
/* scrollable */
true,
@ -559,7 +547,7 @@ class DataSourceView extends CompositeView {
!Device.supportsTouch /* showScrollbar */));
itemsView.addClass('story-section');
node.nodes.add(new Element.html('<div class="query-name-shadow"></div>'));
node.nodes.add(Element.html('<div class="query-name-shadow"></div>'));
// Clicking the view (i.e. its title area) unmaximizes to show the entire
// view.
@ -569,15 +557,17 @@ class DataSourceView extends CompositeView {
}
}
/** A button that toggles between states. */
/// A button that toggles between states. */
class ToggleButton extends View {
EventListeners onChanged;
List<String> states;
ToggleButton(this.states) : onChanged = new EventListeners();
ToggleButton(this.states) : onChanged = EventListeners();
Element render() => new Element.tag('button');
@override
Element render() => Element.tag('button');
@override
void afterRender(Element node) {
state = states[0];
node.onClick.listen((event) {
@ -587,12 +577,12 @@ class ToggleButton extends View {
String get state {
final currentState = node.innerHtml;
assert(states.indexOf(currentState, 0) >= 0);
assert(states.contains(currentState));
return currentState;
}
void set state(String state) {
assert(states.indexOf(state, 0) >= 0);
set state(String state) {
assert(states.contains(state));
node.innerHtml = state;
onChanged.fire(null);
}
@ -605,18 +595,19 @@ class ToggleButton extends View {
}
}
/**
* A factory that creates a view for generic items.
*/
/// 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);
@override
View newView(Article item) => ArticleView(item, swarm, layout);
@override
int getWidth(Article item) => layout.width;
@override
int getHeight(Article item) => layout.computeHeight(item);
}
@ -650,16 +641,14 @@ class ArticleViewLayout {
int width;
static ArticleViewLayout _singleton;
ArticleViewLayout()
: measureBodyText = new MeasureText(BODY_FONT),
measureTitleText = new MeasureText(TITLE_FONT) {
: measureBodyText = MeasureText(BODY_FONT),
measureTitleText = MeasureText(TITLE_FONT) {
num screenWidth = window.screen.width;
width = DESKTOP_WIDTH;
}
static ArticleViewLayout getSingleton() {
if (_singleton == null) {
_singleton = new ArticleViewLayout();
}
_singleton ??= ArticleViewLayout();
return _singleton;
}
@ -673,10 +662,8 @@ class ArticleViewLayout {
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.
*/
/// 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;
@ -696,13 +683,11 @@ class ArticleViewLayout {
height = 92;
}
return new ArticleViewMetrics(height, titleLines, bodyLines);
return ArticleViewMetrics(height, titleLines, bodyLines);
}
}
/**
* A view for a generic item.
*/
/// 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;
@ -713,10 +698,11 @@ class ArticleView extends View {
ArticleView(this.item, this.swarm, this.articleLayout);
@override
Element render() {
Element node;
final byline = item.author.length > 0 ? item.author : item.dataSource.title;
final byline = item.author.isNotEmpty ? item.author : item.dataSource.title;
final date = DateUtils.toRecentTimeString(item.date);
String storyClass = 'story no-thumb';
@ -727,13 +713,13 @@ class ArticleView extends View {
thumbnail = '<img src="${item.thumbUrl}"></img>';
}
final title = new StringBuffer();
final snippet = new StringBuffer();
final title = StringBuffer();
final snippet = StringBuffer();
// Note: also populates title and snippet elements.
final metrics = articleLayout.computeLayout(item, title, snippet);
node = new Element.html('''
node = Element.html('''
<div class="$storyClass">
$thumbnail
<div class="title">$title</div>
@ -751,6 +737,7 @@ class ArticleView extends View {
return node;
}
@override
void afterRender(Element node) {
// Select this view's item.
addOnClick((e) {
@ -789,10 +776,8 @@ class ArticleView extends View {
});
}
/**
* Notify the view to jump to a different area if we are selecting an
* article that is currently outside of the visible area.
*/
/// 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) {
@ -818,7 +803,7 @@ class ArticleView extends View {
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);
CanvasElement(height: img.height, width: img.width);
final CanvasRenderingContext2D ctx = canvas.getContext("2d");
ctx.drawImageScaled(img, 0, 0, img.width, img.height);
@ -826,10 +811,8 @@ class ArticleView extends View {
return canvas.toDataUrl("image/png");
}
/**
* Update this view's selected appearance based on the currently selected
* Article.
*/
/// Update this view's selected appearance based on the currently selected
/// Article.
void _refreshSelected(curItem) {
if (curItem.value == item) {
addClass('sel');
@ -843,10 +826,8 @@ class ArticleView extends View {
}
}
/**
* 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.
*/
/// 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;
@ -855,16 +836,18 @@ class StoryContentView extends View {
StoryContentView(this.swarm, this.item);
@override
get childViews => [_pagedStory];
@override
Element render() {
final storyContent =
new Element.html('<div class="story-content">${item.htmlBody}</div>');
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));
_pagedStory = PagedContentView(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
@ -874,7 +857,7 @@ class StoryContentView extends View {
}
final date = DateUtils.toRecentTimeString(item.date);
final container = new Element.html('''
final container = Element.html('''
<div class="story-view">
<div class="story-text-view">
<div class="story-header">
@ -906,22 +889,20 @@ class SectionView extends CompositeView {
final PageState pageState;
SectionView(this.swarm, this.section, this._viewFactory)
: loadingText = new View.html('<div class="loading-section"></div>'),
pageState = new PageState(),
: loadingText = View.html('<div class="loading-section"></div>'),
pageState = PageState(),
super('section-view') {
addChild(loadingText);
}
/**
* Hides the loading text, reloads the data sources, and shows them.
*/
/// 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>(
dataSourceView = ListView<Feed>(
section.feeds,
_viewFactory,
true /* scrollable */,
@ -936,7 +917,7 @@ class SectionView extends CompositeView {
dataSourceView.addClass("data-source-view");
addChild(dataSourceView);
pageNumberView = addChild(new PageNumberView(pageState));
pageNumberView = addChild(PageNumberView(pageState));
node.style.opacity = '1';
} else {
@ -949,9 +930,7 @@ class SectionView extends CompositeView {
dataSourceView.scroller.reconfigure(() {});
}
/**
* Hides the data sources and shows the loading text.
*/
/// Hides the data sources and shows the loading text.
void hideSources() {
if (dataSourceView != null) {
node.style.opacity = '0.6';
@ -970,10 +949,8 @@ class SectionView extends CompositeView {
}
}
/**
* Find the [DataSourceView] in this SectionView that's displaying the given
* [Feed].
*/
/// Find the [DataSourceView] in this SectionView that's displaying the given
/// [Feed].
DataSourceView findView(Feed dataSource) {
return dataSourceView.getSubview(dataSourceView.findIndex(dataSource));
}

View file

@ -6,14 +6,10 @@
part of swarmlib;
/**
* The base class for UI state that intends to support browser history.
*/
/// The base class for UI state that intends to support browser history.
abstract class UIState {
/**
* The event listener we hook to the window's "popstate" event.
* This event is triggered by the back button or by the first page load.
*/
/// The event listener we hook to the window's "popstate" event.
/// This event is triggered by the back button or by the first page load.
StreamSubscription _historyTracking;
UIState();
@ -51,7 +47,7 @@ abstract class UIState {
}
}
/** Pushes a state onto the browser history stack */
/// Pushes a state onto the browser history stack */
void pushToHistory() {
if (_historyTracking == null) {
throw 'history tracking not started';
@ -62,17 +58,12 @@ abstract class UIState {
// TODO(jmesserly): [state] should be an Object, and we should pass it to
// the state parameter instead of as a #hash URL. Right now we're working
// around b/4582542.
window.history
.pushState(null, '${document.title}', '${document.title}#$state');
window.history.pushState(null, document.title, '${document.title}#$state');
}
/**
* Serialize the state to a form suitable for storing in browser history.
*/
/// Serialize the state to a form suitable for storing in browser history.
Map<String, String> toHistory();
/**
* Load the UI state from the given [values].
*/
/// Load the UI state from the given [values].
void loadFromHistory(Map<String, String> values);
}

View file

@ -9,32 +9,32 @@ part of swarmlib;
// This file contains View framework classes.
// As it grows, it may need to be split into multiple files.
/** A factory that creates a view from a data model. */
/// A factory that creates a view from a data model. */
abstract class ViewFactory<D> {
View newView(D item);
/** The width of the created view or null if the width is not fixed. */
/// The width of the created view or null if the width is not fixed. */
int get width;
/** The height of the created view or null if the height is not fixed. */
/// The height of the created view or null if the height is not fixed. */
int get height;
}
abstract class VariableSizeViewFactory<D> {
View newView(D item);
/** The width of the created view for a specific data model. */
/// The width of the created view for a specific data model. */
int getWidth(D item);
/** The height of the created view for a specific data model. */
/// The height of the created view for a specific data model. */
int getHeight(D item);
}
/** A collection of event listeners. */
/// A collection of event listeners. */
class EventListeners {
var listeners;
EventListeners() {
listeners = new List();
listeners = [];
}
void addListener(listener) {
@ -48,67 +48,61 @@ class EventListeners {
}
}
/**
* Private view class used to store placeholder views for detached ListView
* elements.
*/
/// Private view class used to store placeholder views for detached ListView
/// elements.
class _PlaceholderView extends View {
_PlaceholderView();
Element render() => new Element.tag('div');
@override
Element render() => Element.tag('div');
}
/**
* Class providing all metrics required to layout a data driven list view.
*/
/// Class providing all metrics required to layout a data driven list view.
abstract class ListViewLayout<D> {
void onDataChange();
// TODO(jacobr): placing the newView member function on this class seems like
// the wrong design.
View newView(int index);
/** Get the height of the view. Possibly expensive to compute. */
/// Get the height of the view. Possibly expensive to compute. */
int getHeight(int viewLength);
/** Get the width of the view. Possibly expensive to compute. */
/// Get the width of the view. Possibly expensive to compute. */
int getWidth(int viewLength);
/** Get the length of the view. Possible expensive to compute. */
/// Get the length of the view. Possible expensive to compute. */
int getLength(int viewLength);
/** Estimated height of the view. Guaranteed to be fast to compute. */
/// Estimated height of the view. Guaranteed to be fast to compute. */
int getEstimatedHeight(int viewLength);
/** Estimated with of the view. Guaranteed to be fast to compute. */
/// Estimated with of the view. Guaranteed to be fast to compute. */
int getEstimatedWidth(int viewLength);
/**
* Returns the offset in px that the ith item in the view should be placed
* at.
*/
/// Returns the offset in px that the ith item in the view should be placed
/// at.
int getOffset(int index);
/**
* The page the ith item in the view should be placed in.
*/
/// The page the ith item in the view should be placed in.
int getPage(int index, int viewLength);
int getPageStartIndex(int index, int viewLength);
int getEstimatedLength(int viewLength);
/**
* Snap a specified index to the nearest visible view given the [viewLength].
*/
/// Snap a specified index to the nearest visible view given the [viewLength].
int getSnapIndex(num offset, num viewLength);
/**
* Returns an interval specifying what views are currently visible given a
* particular [:offset:].
*/
/// Returns an interval specifying what views are currently visible given a
/// particular [:offset:].
Interval computeVisibleInterval(num offset, num viewLength, num bufferLength);
}
/**
* Base class used for the simple fixed size item [:ListView:] classes and more
* complex list view classes such as [:VariableSizeListView:] using a
* [:ListViewLayout:] class to drive the actual layout.
*/
/// Base class used for the simple fixed size item [:ListView:] classes and more
/// complex list view classes such as [:VariableSizeListView:] using a
/// [:ListViewLayout:] class to drive the actual layout.
class GenericListView<D> extends View {
/** Minimum throw distance in pixels to trigger snapping to the next item. */
/// Minimum throw distance in pixels to trigger snapping to the next item. */
static const SNAP_TO_NEXT_THROW_THRESHOLD = 15;
static const INDEX_DATA_ATTRIBUTE = 'data-index';
@ -118,25 +112,24 @@ class GenericListView<D> extends View {
final bool _snapToItems;
Scroller scroller;
Scrollbar _scrollbar;
List<D> _data;
ObservableValue<D> _selectedItem;
Map<int, View> _itemViews;
final List<D> _data;
final ObservableValue<D> _selectedItem;
final Map<int, View> _itemViews;
Element _containerElem;
bool _vertical;
/** Length of the scrollable dimension of the view in px. */
final bool _vertical;
/// Length of the scrollable dimension of the view in px. */
int _viewLength = 0;
Interval _activeInterval;
bool _paginate;
bool _removeClippedViews;
ListViewLayout<D> _layout;
final bool _paginate;
final bool _removeClippedViews;
final ListViewLayout<D> _layout;
D _lastSelectedItem;
PageState _pages;
final PageState _pages;
/**
* Creates a new GenericListView with the given layout and data. If [:_data:]
* is an [:ObservableList<T>:] then it will listen to changes to the list
* and update the view appropriately.
*/
/// Creates a new GenericListView with the given layout and data. If [:_data:]
/// is an [:ObservableList<T>:] then it will listen to changes to the list
/// and update the view appropriately.
GenericListView(
this._layout,
this._data,
@ -148,8 +141,8 @@ class GenericListView<D> extends View {
this._removeClippedViews,
this._showScrollbar,
this._pages)
: _activeInterval = new Interval(0, 0),
_itemViews = new Map<int, View>() {
: _activeInterval = Interval(0, 0),
_itemViews = <int, View>{} {
// TODO(rnystrom): Move this into enterDocument once we have an exitDocument
// that we can use to unregister it.
if (_scrollable) {
@ -169,6 +162,7 @@ class GenericListView<D> extends View {
_lastSelectedItem = _selectedItem.value;
}
@override
Iterable<View> get childViews {
return _itemViews.values.toList();
}
@ -194,16 +188,17 @@ class GenericListView<D> extends View {
int _nodeToIndex(Element node) {
// TODO(jacobr): use data attributes when available.
String index = node.attributes[INDEX_DATA_ATTRIBUTE];
if (index != null && index.length > 0) {
if (index != null && index.isNotEmpty) {
return int.parse(index);
}
return null;
}
@override
Element render() {
final node = new Element.tag('div');
final node = Element.tag('div');
if (_scrollable) {
_containerElem = new Element.tag('div');
_containerElem = Element.tag('div');
_containerElem.tabIndex = -1;
node.nodes.add(_containerElem);
} else {
@ -211,16 +206,16 @@ class GenericListView<D> extends View {
}
if (_scrollable) {
scroller = new Scroller(
scroller = Scroller(
_containerElem,
_vertical /* verticalScrollEnabled */,
!_vertical /* horizontalScrollEnabled */,
true /* momentumEnabled */, () {
num width = _layout.getWidth(_viewLength);
num height = _layout.getHeight(_viewLength);
width = width != null ? width : 0;
height = height != null ? height : 0;
return new Size(width, height);
width = width ?? 0;
height = height ?? 0;
return Size(width, height);
},
_paginate && _snapToItems
? Scroller.FAST_SNAP_DECELERATION_FACTOR
@ -235,7 +230,7 @@ class GenericListView<D> extends View {
scroller.onScrollerDragEnd.listen((e) => _decelStart());
}
if (_showScrollbar) {
_scrollbar = new Scrollbar(scroller, true);
_scrollbar = Scrollbar(scroller, true);
}
} else {
_reserveArea();
@ -245,6 +240,7 @@ class GenericListView<D> extends View {
return node;
}
@override
void afterRender(Element node) {
// If our data source is observable, observe it.
if (_data is ObservableList<D>) {
@ -300,6 +296,7 @@ class GenericListView<D> extends View {
});
}
@override
void enterDocument() {
if (scroller != null) {
onResize();
@ -361,7 +358,7 @@ class GenericListView<D> extends View {
_removeView(i);
}
_itemViews.clear();
_activeInterval = new Interval(0, 0);
_activeInterval = Interval(0, 0);
if (scroller == null) {
_reserveArea();
}
@ -381,9 +378,7 @@ class GenericListView<D> extends View {
: scroller.getHorizontalOffset();
}
/**
* Calculates visible interval, based on the scroller position.
*/
/// Calculates visible interval, based on the scroller position.
Interval getVisibleInterval() {
return _layout.computeVisibleInterval(_offset, _viewLength, 0);
}
@ -394,14 +389,14 @@ class GenericListView<D> extends View {
targetInterval = getVisibleInterval();
} else {
// If the view is not scrollable, render all elements.
targetInterval = new Interval(0, _data.length);
targetInterval = Interval(0, _data.length);
}
if (_pages != null) {
_pages.current.value = _layout.getPage(targetInterval.start, _viewLength);
}
if (_pages != null) {
_pages.length.value = _data.length > 0
_pages.length.value = _data.isNotEmpty
? _layout.getPage(_data.length - 1, _viewLength) + 1
: 0;
}
@ -450,7 +445,7 @@ class GenericListView<D> extends View {
void _removeView(int index) {
// Do not remove placeholder views as they need to stay present in case
// they scroll out of view and then back into view.
if (!(_itemViews[index] is _PlaceholderView)) {
if (_itemViews[index] is! _PlaceholderView) {
// Remove from the active DOM but don't destroy.
_itemViews[index].node.remove();
childViewRemoved(_itemViews[index]);
@ -495,10 +490,8 @@ class GenericListView<D> extends View {
}
}
/**
* Detach a subview from the view replacing it with an empty placeholder view.
* The detached subview can be safely reparented.
*/
/// Detach a subview from the view replacing it with an empty placeholder view.
/// The detached subview can be safely reparented.
View detachSubview(D itemData) {
int index = findIndex(itemData);
View view = _itemViews[index];
@ -509,22 +502,20 @@ class GenericListView<D> extends View {
_addView(index);
view = _itemViews[index];
}
final placeholder = new _PlaceholderView();
final placeholder = _PlaceholderView();
view.node.replaceWith(placeholder.node);
_itemViews[index] = placeholder;
return view;
}
/**
* Reattach a subview from the view that was detached from the view
* by calling detachSubview. [callback] is called once the subview is
* reattached and done animating into position.
*/
/// Reattach a subview from the view that was detached from the view
/// by calling detachSubview. [callback] is called once the subview is
/// reattached and done animating into position.
void reattachSubview(D data, View view, bool animate) {
int index = findIndex(data);
// TODO(jacobr): perform some validation that the view is
// really detached.
var currentPosition;
Coordinate currentPosition;
if (animate) {
currentPosition =
FxUtil.computeRelativePosition(view.node, _containerElem);
@ -614,14 +605,16 @@ class GenericListView<D> extends View {
class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
final ViewFactory<D> itemViewFactory;
final bool _vertical;
List<D> _data;
bool _paginate;
final List<D> _data;
final bool _paginate;
FixedSizeListViewLayout(
this.itemViewFactory, this._data, this._vertical, this._paginate);
@override
void onDataChange() {}
@override
View newView(int index) {
return itemViewFactory.newView(_data[index]);
}
@ -630,35 +623,41 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
return _vertical ? itemViewFactory.height : itemViewFactory.width;
}
@override
int getWidth(int viewLength) {
return _vertical ? itemViewFactory.width : getLength(viewLength);
}
@override
int getHeight(int viewLength) {
return _vertical ? getLength(viewLength) : itemViewFactory.height;
}
@override
int getEstimatedHeight(int viewLength) {
// Returns the exact height as it is trivial to compute for this layout.
return getHeight(viewLength);
}
@override
int getEstimatedWidth(int viewLength) {
// Returns the exact height as it is trivial to compute for this layout.
return getWidth(viewLength);
}
@override
int getEstimatedLength(int viewLength) {
// Returns the exact length as it is trivial to compute for this layout.
return getLength(viewLength);
}
@override
int getLength(int viewLength) {
int itemLength = _vertical ? itemViewFactory.height : itemViewFactory.width;
if (viewLength == null || viewLength == 0) {
return itemLength * _data.length;
} else if (_paginate) {
if (_data.length > 0) {
if (_data.isNotEmpty) {
final pageLength = getPageLength(viewLength);
return getPage(_data.length - 1, viewLength) * pageLength +
Math.max(viewLength, pageLength);
@ -670,6 +669,7 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
}
}
@override
int getOffset(int index) {
return index * _itemLength;
}
@ -679,14 +679,17 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
return Math.max(1, itemsPerPage) * _itemLength;
}
@override
int getPage(int index, int viewLength) {
return getOffset(index) ~/ getPageLength(viewLength);
}
@override
int getPageStartIndex(int page, int viewLength) {
return getPageLength(viewLength) ~/ _itemLength * page;
}
@override
int getSnapIndex(num offset, num viewLength) {
int index = (-offset / _itemLength).round();
if (_paginate) {
@ -695,6 +698,7 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
return GoogleMath.clamp(index, 0, _data.length - 1);
}
@override
Interval computeVisibleInterval(
num offset, num viewLength, num bufferLength) {
int targetIntervalStart =
@ -703,28 +707,24 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
((-offset + viewLength + bufferLength) / _itemLength).ceil(),
targetIntervalStart,
_data.length);
return new Interval(targetIntervalStart, targetIntervalEnd.toInt());
return Interval(targetIntervalStart, targetIntervalEnd.toInt());
}
}
/**
* Simple list view class where each item has fixed width and height.
*/
/// Simple list view class where each item has fixed width and height.
class ListView<D> extends GenericListView<D> {
/**
* Creates a new ListView for the given data. If [:_data:] is an
* [:ObservableList<T>:] then it will listen to changes to the list and
* update the view appropriately.
*/
/// Creates a new ListView for the given data. If [:_data:] is an
/// [:ObservableList<T>:] then it will listen to changes to the list and
/// update the view appropriately.
ListView(List<D> data, ViewFactory<D> itemViewFactory, bool scrollable,
bool vertical, ObservableValue<D> selectedItem,
[bool snapToItems = false,
bool paginate = false,
bool removeClippedViews = false,
bool showScrollbar = false,
PageState pages = null])
PageState pages])
: super(
new FixedSizeListViewLayout<D>(
FixedSizeListViewLayout<D>(
itemViewFactory, data, vertical, paginate),
data,
scrollable,
@ -737,37 +737,38 @@ class ListView<D> extends GenericListView<D> {
pages);
}
/**
* Layout where each item may have variable size along the axis the list view
* extends.
*/
/// Layout where each item may have variable size along the axis the list view
/// extends.
class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
List<D> _data;
final List<D> _data;
List<int> _itemOffsets;
List<int> _lengths;
int _lastOffset = 0;
bool _vertical;
bool _paginate;
final bool _vertical;
final bool _paginate;
VariableSizeViewFactory<D> itemViewFactory;
Interval _lastVisibleInterval;
VariableSizeListViewLayout(
this.itemViewFactory, data, this._vertical, this._paginate)
: _data = data,
_lastVisibleInterval = new Interval(0, 0) {
_lastVisibleInterval = Interval(0, 0) {
_itemOffsets = <int>[];
_lengths = <int>[];
_itemOffsets.add(0);
}
@override
void onDataChange() {
_itemOffsets.clear();
_itemOffsets.add(0);
_lengths.clear();
}
@override
View newView(int index) => itemViewFactory.newView(_data[index]);
@override
int getWidth(int viewLength) {
if (_vertical) {
return itemViewFactory.getWidth(null);
@ -776,6 +777,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
}
}
@override
int getHeight(int viewLength) {
if (_vertical) {
return getLength(viewLength);
@ -784,6 +786,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
}
}
@override
int getEstimatedHeight(int viewLength) {
if (_vertical) {
return getEstimatedLength(viewLength);
@ -792,6 +795,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
}
}
@override
int getEstimatedWidth(int viewLength) {
if (_vertical) {
return itemViewFactory.getWidth(null);
@ -802,12 +806,13 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
// TODO(jacobr): this logic is overly complicated. Replace with something
// simpler.
@override
int getEstimatedLength(int viewLength) {
if (_lengths.length == _data.length) {
// No need to estimate... we have all the data already.
return getLength(viewLength);
}
if (_itemOffsets.length > 1 && _lengths.length > 0) {
if (_itemOffsets.length > 1 && _lengths.isNotEmpty) {
// Estimate length by taking the average of the lengths
// of the known views.
num lengthFromAllButLastElement = 0;
@ -828,8 +833,9 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
}
}
@override
int getLength(int viewLength) {
if (_data.length == 0) {
if (_data.isEmpty) {
return viewLength;
} else {
// Hack so that _lengths[length - 1] is available.
@ -839,6 +845,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
}
}
@override
int getOffset(int index) {
if (index >= _itemOffsets.length) {
int offset = _itemOffsets[_itemOffsets.length - 1];
@ -854,16 +861,19 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
return _itemOffsets[index];
}
@override
int getPage(int index, int viewLength) {
// TODO(jacobr): implement.
throw 'Not implemented';
}
@override
int getPageStartIndex(int page, int viewLength) {
// TODO(jacobr): implement.
throw 'Not implemented';
}
@override
int getSnapIndex(num offset, num viewLength) {
for (int i = 1; i < _data.length; i++) {
if (getOffset(i) + getOffset(i - 1) > -offset * 2) {
@ -873,6 +883,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
return _data.length - 1;
}
@override
Interval computeVisibleInterval(
num offset, num viewLength, num bufferLength) {
offset = offset.toInt();
@ -880,7 +891,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
_lastVisibleInterval != null ? _lastVisibleInterval.start : 0);
int end = _findFirstItemAfter(-offset + viewLength + bufferLength,
_lastVisibleInterval != null ? _lastVisibleInterval.end : 0);
_lastVisibleInterval = new Interval(start, Math.max(start, end));
_lastVisibleInterval = Interval(start, Math.max(start, end));
_lastOffset = offset;
return _lastVisibleInterval;
}
@ -914,9 +925,9 @@ class VariableSizeListView<D> extends GenericListView<D> {
bool paginate = false,
bool removeClippedViews = false,
bool showScrollbar = false,
PageState pages = null])
PageState pages])
: super(
new VariableSizeListViewLayout(
VariableSizeListViewLayout(
itemViewFactory, data, vertical, paginate),
data,
scrollable,
@ -929,19 +940,21 @@ class VariableSizeListView<D> extends GenericListView<D> {
pages);
}
/** A back button that is equivalent to clicking "back" in the browser. */
/// A back button that is equivalent to clicking "back" in the browser. */
class BackButton extends View {
BackButton();
Element render() => new Element.html('<div class="back-arrow button"></div>');
@override
Element render() => Element.html('<div class="back-arrow button"></div>');
@override
void afterRender(Element node) {
addOnClick((e) => window.history.back());
}
}
// TODO(terry): Maybe should be part of ButtonView class in appstack/view?
/** OS button. */
/// OS button. */
class PushButtonView extends View {
final String _text;
final String _cssClass;
@ -949,10 +962,12 @@ class PushButtonView extends View {
PushButtonView(this._text, this._cssClass, this._clickHandler);
@override
Element render() {
return new Element.html('<button class="${_cssClass}">${_text}</button>');
return Element.html('<button class="$_cssClass">$_text</button>');
}
@override
void afterRender(Element node) {
addOnClick(_clickHandler);
}
@ -961,7 +976,7 @@ class PushButtonView extends View {
// TODO(terry): Add a drop shadow around edge and corners need to be rounded.
// Need to support conveyor for contents of dialog so it's not
// larger than the parent window.
/** A generic dialog view supports title, done button and dialog content. */
/// A generic dialog view supports title, done button and dialog content. */
class DialogView extends View {
final String _title;
final String _cssName;
@ -971,8 +986,9 @@ class DialogView extends View {
DialogView(this._title, this._cssName, this._content);
@override
Element render() {
final node = new Element.html('''
final node = Element.html('''
<div class="dialog-modal">
<div class="dialog $_cssName">
<div class="dialog-title-area">
@ -982,8 +998,8 @@ class DialogView extends View {
</div>
</div>''');
_done = new PushButtonView(
'Done', 'done-button', EventBatch.wrap((e) => onDone()));
_done =
PushButtonView('Done', 'done-button', EventBatch.wrap((e) => onDone()));
final titleArea = node.querySelector('.dialog-title-area');
titleArea.nodes.add(_done.node);
@ -993,6 +1009,6 @@ class DialogView extends View {
return node;
}
/** Override to handle dialog done. */
/// Override to handle dialog done. */
void onDone() {}
}

View file

@ -0,0 +1,6 @@
include: package:lints/recommended.yaml
analyzer:
exclude: [build/**]
language:

View file

@ -0,0 +1,5 @@
name: swarm
version: 0.0.1
environment:
sdk: '>=2.17.0 <3.0.0'
dev_dependencies: {lints: ^2.0.0}

View file

@ -9,5 +9,5 @@ library swarm;
import 'swarmlib.dart';
void main() {
new Swarm().run();
Swarm().run();
}

View file

@ -4,7 +4,7 @@
part of base;
typedef void AnimationCallback(num currentTime);
typedef AnimationCallback = void Function(num currentTime);
class CallbackData {
final AnimationCallback callback;
@ -13,28 +13,26 @@ class CallbackData {
static int _nextId = 1;
bool ready(num time) => minTime == null || minTime <= time;
bool ready(num time) => minTime <= time;
CallbackData(this.callback, this.minTime) : id = _nextId++;
}
/**
* Animation scheduler implementing the functionality provided by
* [:window.requestAnimationFrame:] for platforms that do not support it
* or support it badly. When multiple UI components are animating at once,
* this approach yields superior performance to calling setTimeout/Timer
* directly as all pieces of the UI will animate at the same time resulting in
* fewer layouts.
*/
/// Animation scheduler implementing the functionality provided by
/// [:window.requestAnimationFrame:] for platforms that do not support it
/// or support it badly. When multiple UI components are animating at once,
/// this approach yields superior performance to calling setTimeout/Timer
/// directly as all pieces of the UI will animate at the same time resulting in
/// fewer layouts.
// TODO(jacobr): use window.requestAnimationFrame when it is available and
// 60fps for the current browser.
class AnimationScheduler {
static const FRAMES_PER_SECOND = 60;
static const MS_PER_FRAME = 1000 ~/ FRAMES_PER_SECOND;
/** List of callbacks to be executed next animation frame. */
/// List of callbacks to be executed next animation frame. */
List<CallbackData> _callbacks;
bool _isMobileSafari = false;
final bool _isMobileSafari = false;
late CssStyleDeclaration _safariHackStyle;
int _frameCount = 0;
@ -49,23 +47,19 @@ class AnimationScheduler {
}
}
/**
* Cancel the pending callback matching the specified [id].
* This is not heavily optimized as typically users don't cancel animation
* frames.
*/
/// Cancel the pending callback matching the specified [id].
/// This is not heavily optimized as typically users don't cancel animation
/// frames.
void cancelRequestAnimationFrame(int id) {
_callbacks = _callbacks.where((CallbackData e) => e.id != id).toList();
}
/**
* Schedule [callback] to execute at the next animation frame that occurs
* at or after [minTime]. If [minTime] is not specified, the first available
* animation frame is used. Returns an id that can be used to cancel the
* pending callback.
*/
/// Schedule [callback] to execute at the next animation frame that occurs
/// at or after [minTime]. If [minTime] is not specified, the first available
/// animation frame is used. Returns an id that can be used to cancel the
/// pending callback.
int requestAnimationFrame(AnimationCallback callback,
[Element? element = null, num? minTime = null]) {
[Element? element, num? minTime]) {
final callbackData = CallbackData(callback, minTime!);
_requestAnimationFrameHelper(callbackData);
return callbackData.id;
@ -114,7 +108,7 @@ class AnimationScheduler {
(callbackData.callback)(minTime);
} catch (e) {
final msg = e.toString();
print('Suppressed exception ${msg} triggered by callback');
print('Suppressed exception $msg triggered by callback');
}
} else {
_callbacks.add(callbackData);

View file

@ -6,72 +6,48 @@ part of base;
// TODO(jacobr): cache these results.
// TODO(jacobr): figure out how to test this.
/**
* Utils for device detection.
*/
/// Utils for device detection.
class Device {
/**
* The regular expression for detecting an iPhone or iPod.
*/
/// The regular expression for detecting an iPhone or iPod.
static final _IPHONE_REGEX = RegExp('iPhone|iPod');
/**
* The regular expression for detecting an iPhone or iPod or iPad.
*/
/// The regular expression for detecting an iPhone or iPod or iPad.
static final _MOBILE_SAFARI_REGEX = RegExp('iPhone|iPod|iPad');
/**
* The regular expression for detecting an iPhone or iPod or iPad simulator.
*/
/// The regular expression for detecting an iPhone or iPod or iPad simulator.
static final _APPLE_SIM_REGEX = RegExp('iP.*Simulator');
/**
* Gets the browser's user agent. Using this function allows tests to inject
* the user agent.
* Returns the user agent.
*/
/// Gets the browser's user agent. Using this function allows tests to inject
/// the user agent.
/// Returns the user agent.
static String get userAgent => window.navigator.userAgent;
/**
* Determines if the current device is an iPhone or iPod.
* Returns true if the current device is an iPhone or iPod.
*/
/// Determines if the current device is an iPhone or iPod.
/// Returns true if the current device is an iPhone or iPod.
static bool get isIPhone => _IPHONE_REGEX.hasMatch(userAgent);
/**
* Determines if the current device is an iPad.
* Returns true if the current device is an iPad.
*/
/// Determines if the current device is an iPad.
/// Returns true if the current device is an iPad.
static bool get isIPad => userAgent.contains("iPad", 0);
/**
* Determines if the current device is running Firefox.
*/
/// Determines if the current device is running Firefox.
static bool get isFirefox => userAgent.contains("Firefox", 0);
/**
* Determines if the current device is an iPhone or iPod or iPad.
* Returns true if the current device is an iPhone or iPod or iPad.
*/
/// Determines if the current device is an iPhone or iPod or iPad.
/// Returns true if the current device is an iPhone or iPod or iPad.
static bool get isMobileSafari => _MOBILE_SAFARI_REGEX.hasMatch(userAgent);
/**
* Determines if the current device is the iP* Simulator.
* Returns true if the current device is an iP* Simulator.
*/
/// Determines if the current device is the iP* Simulator.
/// Returns true if the current device is an iP* Simulator.
static bool get isAppleSimulator => _APPLE_SIM_REGEX.hasMatch(userAgent);
/**
* Determines if the current device is an Android.
* Returns true if the current device is an Android.
*/
/// Determines if the current device is an Android.
/// Returns true if the current device is an Android.
static bool get isAndroid => userAgent.contains("Android", 0);
/**
* Determines if the current device is WebOS WebKit.
* Returns true if the current device is WebOS WebKit.
*/
/// Determines if the current device is WebOS WebKit.
/// Returns true if the current device is WebOS WebKit.
static bool get isWebOs => userAgent.contains("webOS", 0);
static late bool supportsTouch = isMobileSafari || isAndroid;
static bool supportsTouch = isMobileSafari || isAndroid;
}

View file

@ -4,11 +4,9 @@
part of base;
/**
* Embedded DSL for generating DOM elements.
*/
/// Embedded DSL for generating DOM elements.
class Dom {
static void ready(void f()) {
static void ready(void Function() f) {
if (document.readyState == 'interactive' ||
document.readyState == 'complete') {
Timer.run(f);
@ -20,7 +18,7 @@ class Dom {
}
}
/** Adds the given <style> text to the page. */
/// Adds the given <style> text to the page. */
static void addStyle(String cssText) {
var style = Element.tag('style') as StyleElement;
style.type = 'text/css';

View file

@ -4,34 +4,28 @@
part of base;
/**
* This class has static fields that hold objects that this isolate
* uses to interact with the environment. Which objects are available
* depend on how the isolate was started. In the UI isolate
* of the browser, the window object is available.
*/
/// This class has static fields that hold objects that this isolate
/// uses to interact with the environment. Which objects are available
/// depend on how the isolate was started. In the UI isolate
/// of the browser, the window object is available.
class Env {
static AnimationScheduler _animationScheduler = AnimationScheduler();
static final AnimationScheduler _animationScheduler = AnimationScheduler();
/**
* Provides functionality similar to [:window.requestAnimationFrame:] for
* all platforms. [callback] is executed on the next animation frame that
* occurs at or after [minTime]. If [minTime] is not specified, the first
* available animation frame is used. Returns an id that can be used to
* cancel the pending callback.
*/
/// Provides functionality similar to [:window.requestAnimationFrame:] for
/// all platforms. [callback] is executed on the next animation frame that
/// occurs at or after [minTime]. If [minTime] is not specified, the first
/// available animation frame is used. Returns an id that can be used to
/// cancel the pending callback.
static int requestAnimationFrame(AnimationCallback callback,
[Element? element = null, num? minTime = null]) {
[Element? element, num? minTime]) {
return _animationScheduler.requestAnimationFrame(
callback, element, minTime);
}
/**
* Cancel the pending callback callback matching the specified [id].
*/
/// Cancel the pending callback callback matching the specified [id].
static void cancelRequestAnimationFrame(int id) {
_animationScheduler.cancelRequestAnimationFrame(id);
}
}
typedef void XMLHttpRequestCompleted(HttpRequest req);
typedef XMLHttpRequestCompleted = void Function(HttpRequest req);

View file

@ -4,127 +4,101 @@
part of base;
/**
* A utility class for representing two-dimensional sizes.
*/
/// A utility class for representing two-dimensional sizes.
class Size {
num width;
num height;
Size(num this.width, num this.height) {}
Size(this.width, this.height);
@override
bool operator ==(covariant Size other) {
return other != null && width == other.width && height == other.height;
return width == other.width && height == other.height;
}
@override
int get hashCode => throw UnimplementedError();
/**
* Returns the area of the size (width * height).
*/
/// Returns the area of the size (width * height).
num area() {
return width * height;
}
/**
* Returns the ratio of the size's width to its height.
*/
/// Returns the ratio of the size's width to its height.
num aspectRatio() {
return width / height;
}
/**
* Clamps the width and height parameters upward to integer values.
* Returns this size with ceil'd components.
*/
/// Clamps the width and height parameters upward to integer values.
/// Returns this size with ceil'd components.
Size ceil() {
width = width.ceil();
height = height.ceil();
return this;
}
/**
* Returns a copy of the Size.
*/
/// Returns a copy of the Size.
Size clone() {
return Size(width, height);
}
/**
* Returns true if this Size is the same size or smaller than the
* [target] size in both dimensions.
*/
/// Returns true if this Size is the same size or smaller than the
/// [target] size in both dimensions.
bool fitsInside(Size target) {
return width <= target.width && height <= target.height;
}
/**
* Clamps the width and height parameters downward to integer values.
* Returns this size with floored components.
*/
/// Clamps the width and height parameters downward to integer values.
/// Returns this size with floored components.
Size floor() {
width = width.floor();
height = height.floor();
return this;
}
/**
* Returns the longer of the two dimensions in the size.
*/
/// Returns the longer of the two dimensions in the size.
num getLongest() {
return max(width, height);
}
/**
* Returns the shorter of the two dimensions in the size.
*/
/// Returns the shorter of the two dimensions in the size.
num getShortest() {
return min(width, height);
}
/**
* Returns true if the size has zero area, false if both dimensions
* are non-zero numbers.
*/
/// Returns true if the size has zero area, false if both dimensions
/// are non-zero numbers.
bool get isEmpty {
return area() == 0;
}
/**
* Returns the perimeter of the size (width + height) * 2.
*/
/// Returns the perimeter of the size (width + height) * 2.
num perimeter() {
return (width + height) * 2;
}
/**
* Rounds the width and height parameters to integer values.
* Returns this size with rounded components.
*/
/// Rounds the width and height parameters to integer values.
/// Returns this size with rounded components.
Size round() {
width = width.round();
height = height.round();
return this;
}
/**
* Scales the size uniformly by a factor.
* [s] The scale factor.
* Returns this Size object after scaling.
*/
/// Scales the size uniformly by a factor.
/// [s] The scale factor.
/// Returns this Size object after scaling.
Size scale(num s) {
width *= s;
height *= s;
return this;
}
/**
* Uniformly scales the size to fit inside the dimensions of a given size. The
* original aspect ratio will be preserved.
*
* This function assumes that both Sizes contain strictly positive dimensions.
* Returns this Size object, after optional scaling.
*/
/// Uniformly scales the size to fit inside the dimensions of a given size. The
/// original aspect ratio will be preserved.
///
/// This function assumes that both Sizes contain strictly positive dimensions.
/// Returns this Size object, after optional scaling.
Size scaleToFit(Size target) {
num s = aspectRatio() > target.aspectRatio()
? target.width / width
@ -132,11 +106,10 @@ class Size {
return scale(s);
}
/**
* Returns a nice string representing size.
* Returns in the form (50 x 73).
*/
/// Returns a nice string representing size.
/// Returns in the form (50 x 73).
@override
String toString() {
return "(${width} x ${height})";
return "($width x $height)";
}
}

View file

@ -4,14 +4,12 @@
part of layout;
/**
* Implements a grid-based layout system based on:
* [http://dev.w3.org/csswg/css3-grid-align/]
*
* This layout is designed to support animations and work on browsers that
* don't support grid natively. As such, we implement it on top of absolute
* positioning.
*/
/// Implements a grid-based layout system based on:
/// [http://dev.w3.org/csswg/css3-grid-align/]
///
/// This layout is designed to support animations and work on browsers that
/// don't support grid natively. As such, we implement it on top of absolute
/// positioning.
// TODO(jmesserly): the DOM integration still needs work:
// - The grid assumes it is absolutely positioned in its container.
// Because of that, the grid doesn't work right unless it has at least one
@ -43,31 +41,27 @@ part of layout;
// - Optimize for the case of no content sized tracks
// - Optimize for the "incremental update" cases
class GridLayout extends ViewLayout {
/** Configuration parameters defined in CSS. */
/// Configuration parameters defined in CSS. */
final GridTrackList? rows;
final GridTrackList? columns;
final GridTemplate? template;
/** The default sizing for new rows. */
/// The default sizing for new rows. */
final TrackSizing rowSizing;
/** The default sizing for new columns. */
/// The default sizing for new columns. */
final TrackSizing columnSizing;
/**
* This stores the grid's size during a layout.
* Used for rows/columns with % or fr units.
*/
/// This stores the grid's size during a layout.
/// Used for rows/columns with % or fr units.
int? _gridWidth, _gridHeight;
/**
* During a layout, this stores all row/column size information.
* Because grid-items can implicitly specify their own rows/columns, we can't
* compute this until we know the set of items.
*/
/// During a layout, this stores all row/column size information.
/// Because grid-items can implicitly specify their own rows/columns, we can't
/// compute this until we know the set of items.
late List<GridTrack> _rowTracks, _columnTracks;
/** During a layout, tracks which dimension we're processing. */
/// During a layout, tracks which dimension we're processing. */
Dimension? _dimension;
GridLayout(Positionable view)
@ -83,9 +77,12 @@ class GridLayout extends ViewLayout {
_columnTracks = columns?.tracks ?? [];
}
@override
int? get currentWidth => _gridWidth;
@override
int? get currentHeight => _gridHeight;
@override
void cacheExistingBrowserLayout() {
// We don't need to do anything as we don't rely on the _cachedViewRect
// when the grid layout is used.
@ -93,14 +90,15 @@ class GridLayout extends ViewLayout {
// TODO(jacobr): cleanup this method so that it returns a Future
// rather than taking a Completer as an argument.
/** The main entry point for layout computation. */
/// The main entry point for layout computation. */
@override
void measureLayout(Future<Size> size, Completer<bool>? changed) {
_ensureAllTracks();
size.then((value) {
_gridWidth = value.width as int?;
_gridHeight = value.height as int?;
if (_rowTracks.length > 0 && _columnTracks.length > 0) {
if (_rowTracks.isNotEmpty && _columnTracks.isNotEmpty) {
_measureTracks();
_setBoundsOfChildren();
if (changed != null) {
@ -110,10 +108,8 @@ class GridLayout extends ViewLayout {
});
}
/**
* The top level measurement function.
* [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks]
*/
/// The top level measurement function.
/// [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks]
void _measureTracks() {
// Resolve logical width, then height. Width comes first so we can use
// the width when determining the content-sized height.
@ -137,14 +133,12 @@ class GridLayout extends ViewLayout {
return Math.max(0, remaining);
}
/**
* This is the core Grid Track sizing algorithm. It is run for Grid columns
* and Grid rows. The goal of the function is to ensure:
* 1. That each Grid Track satisfies its minSizing
* 2. That each Grid Track grows from the breadth which satisfied its
* minSizing to a breadth which satifies its
* maxSizing, subject to RemainingSpace.
*/
/// This is the core Grid Track sizing algorithm. It is run for Grid columns
/// and Grid rows. The goal of the function is to ensure:
/// 1. That each Grid Track satisfies its minSizing
/// 2. That each Grid Track grows from the breadth which satisfied its
/// minSizing to a breadth which satifies its
/// maxSizing, subject to RemainingSpace.
// Note: spec does not correctly doc all the parameters to this function.
void _computeUsedBreadthOfTracks(List<GridTrack> tracks) {
// TODO(jmesserly): as a performance optimization we could cache this
@ -161,12 +155,12 @@ class GridLayout extends ViewLayout {
}
// 2. Resolve content-based MinTrackSizingFunctions
final USED_BREADTH = const _UsedBreadthAccumulator();
final MAX_BREADTH = const _MaxBreadthAccumulator();
final USEDBREADTH = const _UsedBreadthAccumulator();
final MAXBREADTH = const _MaxBreadthAccumulator();
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USED_BREADTH);
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USEDBREADTH);
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USED_BREADTH);
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USEDBREADTH);
// 3. Ensure that maxBreadth is as big as usedBreadth for each track
for (final t in tracks) {
@ -176,16 +170,16 @@ class GridLayout extends ViewLayout {
}
// 4. Resolve content-based MaxTrackSizingFunctions
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAX_BREADTH);
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAXBREADTH);
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAX_BREADTH);
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAXBREADTH);
// 5. Grow all Grid Tracks in GridTracks from their usedBreadth up to their
// maxBreadth value until RemainingSpace is exhausted.
// Note: it's not spec'd what to pass as the accumulator, but usedBreadth
// seems right.
_distributeSpaceToTracks(
tracks, _getRemainingSpace(tracks), USED_BREADTH, false);
tracks, _getRemainingSpace(tracks), USEDBREADTH, false);
// Spec wording is confusing about which direction this assignment happens,
// but this is the way that makes sense.
@ -203,10 +197,8 @@ class GridLayout extends ViewLayout {
_computeTrackPositions(tracks);
}
/**
* Final steps to finish positioning tracks. Takes the track size and uses
* it to get start and end positions. Also rounds the positions to integers.
*/
/// Final steps to finish positioning tracks. Takes the track size and uses
/// it to get start and end positions. Also rounds the positions to integers.
void _computeTrackPositions(List<GridTrack> tracks) {
// Compute start positions of tracks, as well as the final position
@ -238,14 +230,12 @@ class GridLayout extends ViewLayout {
}
}
/**
* This method computes a '1fr' value, referred to as the
* tempBreadth, for a set of Grid Tracks. The value computed
* will ensure that when the tempBreadth is multiplied by the
* fractions associated with tracks, that the UsedBreadths of tracks
* will increase by an amount equal to the maximum of zero and the specified
* freeSpace less the sum of the current UsedBreadths.
*/
/// This method computes a '1fr' value, referred to as the
/// tempBreadth, for a set of Grid Tracks. The value computed
/// will ensure that when the tempBreadth is multiplied by the
/// fractions associated with tracks, that the UsedBreadths of tracks
/// will increase by an amount equal to the maximum of zero and the specified
/// freeSpace less the sum of the current UsedBreadths.
num _calcNormalizedFractionBreadth(List<GridTrack> tracks) {
final fractionTracks = tracks.where((t) => t.maxSizing.isFraction).toList();
@ -279,11 +269,9 @@ class GridLayout extends ViewLayout {
return spaceNeededFromFractionTracks / accumulatedFractions;
}
/**
* Ensures that for each Grid Track in tracks, a value will be
* computed, updatedBreadth, that represents the Grid Track's share of
* freeSpace.
*/
/// Ensures that for each Grid Track in tracks, a value will be
/// computed, updatedBreadth, that represents the Grid Track's share of
/// freeSpace.
void _distributeSpaceToTracks(List<GridTrack?> tracks, num? freeSpace,
_BreadthAccumulator breadth, bool ignoreMaxBreadth) {
// TODO(jmesserly): in some cases it would be safe to sort the passed in
@ -321,14 +309,12 @@ class GridLayout extends ViewLayout {
}
}
/**
* This function prioritizes the distribution of space driven by Grid Items
* in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid
* Items having a lower spanCount have an opportunity to increase the size of
* the Grid Tracks they cover before those with larger SpanCounts.
*
* Note: items are assumed to be already sorted in increasing span count
*/
/// This function prioritizes the distribution of space driven by Grid Items
/// in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid
/// Items having a lower spanCount have an opportunity to increase the size of
/// the Grid Tracks they cover before those with larger SpanCounts.
///
/// Note: items are assumed to be already sorted in increasing span count
void _distributeSpaceBySpanCount(List<ViewLayout> items,
ContentSizeMode sizeMode, _BreadthAccumulator breadth) {
items = items
@ -366,10 +352,8 @@ class GridLayout extends ViewLayout {
}
}
/**
* Returns true if we have an appropriate content sized dimension, and don't
* cross a fractional track.
*/
/// Returns true if we have an appropriate content sized dimension, and don't
/// cross a fractional track.
static bool _hasContentSizedTracks(Iterable<GridTrack?> tracks,
ContentSizeMode sizeMode, _BreadthAccumulator breadth) {
for (final t in tracks) {
@ -383,7 +367,7 @@ class GridLayout extends ViewLayout {
return false;
}
/** Ensures that the numbered track exists. */
/// Ensures that the numbered track exists. */
void _ensureTrack(
List<GridTrack> tracks, TrackSizing sizing, int start, int span) {
// Start is 1-based. Make it 0-based.
@ -402,13 +386,11 @@ class GridLayout extends ViewLayout {
}
}
/**
* Scans children creating GridLayoutParams as needed, and creates all of the
* rows and columns that we will need.
*
* Note: this can potentially create new qrows/columns, so this needs to be
* run before the track sizing algorithm.
*/
/// Scans children creating GridLayoutParams as needed, and creates all of the
/// rows and columns that we will need.
///
/// Note: this can potentially create new qrows/columns, so this needs to be
/// run before the track sizing algorithm.
void _ensureAllTracks() {
final items = view.childViews.map((view_) => view_.layout);
@ -423,9 +405,7 @@ class GridLayout extends ViewLayout {
}
}
/**
* Given the track sizes that were computed, position children in the grid.
*/
/// Given the track sizes that were computed, position children in the grid.
void _setBoundsOfChildren() {
final items = view.childViews.map((view_) => view_.layout);
@ -453,6 +433,7 @@ class GridLayout extends ViewLayout {
} else if (_dimension == Dimension.HEIGHT) {
return _gridHeight;
}
return null;
}
_GridLocation _getTrackLocationX(GridLayoutParams childLayout) {
@ -475,7 +456,7 @@ class GridLayout extends ViewLayout {
return _GridLocation(start, end - start);
}
/** Gets the tracks that this item crosses. */
/// Gets the tracks that this item crosses. */
// TODO(jmesserly): might be better to return an iterable
List<GridTrack?> _getTracks(ViewLayout item) {
GridLayoutParams? childLayout = item.layoutParams as GridLayoutParams?;

View file

@ -4,23 +4,22 @@
part of layout;
/**
* Caches the layout parameters that were specified in CSS during a layout
* computation. These values are immutable during a layout.
*/
/// Caches the layout parameters that were specified in CSS during a layout
/// computation. These values are immutable during a layout.
// TODO(jmesserly): I would like all fields to be final, but it's too painful
// to do this right now in Dart. If I create a factory constructor, then I need
// to create locals, and pass all parameters to the real constructor. Each
// field ends up being mentioned 4 times instead of just twice.
class GridLayoutParams extends LayoutParams {
/** The coordinates of this item in the grid. */
/// The coordinates of this item in the grid. */
int? row;
int? column;
int? rowSpan;
int? columnSpan;
@override
int? layer;
/** Alignment within its box */
/// Alignment within its box */
GridItemAlignment rowAlign;
GridItemAlignment columnAlign;
@ -89,16 +88,16 @@ class GridLayoutParams extends LayoutParams {
columnSpan = rect.columnSpan;
} else {
// Apply default row, column span values.
if (rowSpan == null) rowSpan = 1;
if (columnSpan == null) columnSpan = 1;
rowSpan ??= 1;
columnSpan ??= 1;
if (row == null && column == null) {
throw UnsupportedError('grid-flow is not implemented' +
throw UnsupportedError('grid-flow is not implemented'
' so at least one row or one column must be defined');
}
if (row == null) row = 1;
if (column == null) column = 1;
row ??= 1;
column ??= 1;
}
assert(row! > 0 && rowSpan! > 0 && column! > 0 && columnSpan! > 0);

View file

@ -4,10 +4,8 @@
part of layout;
/**
* Base class for simple recursive descent parsers.
* Handles the lower level stuff, i.e. what a scanner/tokenizer would do.
*/
/// Base class for simple recursive descent parsers.
/// Handles the lower level stuff, i.e. what a scanner/tokenizer would do.
class _Parser {
static const WHITESPACE = ' \r\n\t';
@ -124,7 +122,7 @@ class _Parser {
return true;
}
void _eat(String value, [bool eatWhitespace = true]) {
void _eat(String value) {
if (!_maybeEat(value)) {
_error('expected "$value"');
}
@ -159,7 +157,7 @@ class _Parser {
return result;
}
/** Eats something like a keyword. */
/// Eats something like a keyword. */
String _eatWord() {
int start = _offset;
while (_offset < length && _isLetter(_peekChar())) {
@ -168,7 +166,7 @@ class _Parser {
return _src.substring(start, _offset);
}
/** Eats an integer. */
/// Eats an integer. */
int? _maybeEatInt() {
int start = _offset;
bool dot = false;
@ -183,7 +181,7 @@ class _Parser {
return int.parse(_src.substring(start, _offset));
}
/** Eats an integer. */
/// Eats an integer. */
int? _eatInt() {
int? result = _maybeEatInt();
if (result == null) {
@ -192,7 +190,7 @@ class _Parser {
return result;
}
/** Eats something like a positive decimal: 12.345. */
/// Eats something like a positive decimal: 12.345. */
num _eatDouble() {
int start = _offset;
bool dot = false;
@ -217,11 +215,11 @@ class _Parser {
}
}
/** Parses a grid template. */
/// Parses a grid template. */
class _GridTemplateParser extends _Parser {
_GridTemplateParser._internal(String src) : super(src);
/** Parses the grid-rows and grid-columns CSS properties into object form. */
/// Parses the grid-rows and grid-columns CSS properties into object form. */
static GridTemplate? parse(String? str) {
if (str == null) return null;
final p = _GridTemplateParser._internal(str);
@ -230,7 +228,7 @@ class _GridTemplateParser extends _Parser {
return result;
}
/** Parses a grid-cell value. */
/// Parses a grid-cell value. */
static String? parseCell(String? str) {
if (str == null) return null;
final p = _GridTemplateParser._internal(str);
@ -249,18 +247,18 @@ class _GridTemplateParser extends _Parser {
while ((row = _maybeEatString()) != null) {
rows.add(row);
}
if (rows.length == 0) {
if (rows.isEmpty) {
_error('expected at least one cell, or "none"');
}
return GridTemplate(rows);
}
}
/** Parses a grid-row or grid-column */
/// Parses a grid-row or grid-column */
class _GridItemParser extends _Parser {
_GridItemParser._internal(String src) : super(src);
/** Parses the grid-rows and grid-columns CSS properties into object form. */
/// Parses the grid-rows and grid-columns CSS properties into object form. */
static _GridLocation? parse(String? cell, GridTrackList? list) {
if (cell == null) return null;
final p = _GridItemParser._internal(cell);
@ -281,7 +279,7 @@ class _GridItemParser extends _Parser {
_error('expected row/column number or name');
}
int? end = _maybeParseLine(list);
int? span = null;
int? span;
if (end != null) {
span = end - start!;
if (span <= 0) {
@ -318,11 +316,9 @@ class _GridItemParser extends _Parser {
}
}
/**
* Parses grid-rows and grid-column properties, see:
* [http://dev.w3.org/csswg/css3-grid-align/#grid-columns-and-rows-properties]
* This is kept as a recursive descent parser for simplicity.
*/
/// Parses grid-rows and grid-column properties, see:
/// [http://dev.w3.org/csswg/css3-grid-align/#grid-columns-and-rows-properties]
/// This is kept as a recursive descent parser for simplicity.
// TODO(jmesserly): implement missing features from the spec. Mainly around
// CSS units, support for all escape sequences, etc.
class _GridTrackParser extends _Parser {
@ -331,10 +327,10 @@ class _GridTrackParser extends _Parser {
_GridTrackParser._internal(String src)
: _tracks = <GridTrack>[],
_lineNames = Map<String?, int>(),
_lineNames = <String?, int>{},
super(src);
/** Parses the grid-rows and grid-columns CSS properties into object form. */
/// Parses the grid-rows and grid-columns CSS properties into object form. */
static GridTrackList? parse(String? str) {
if (str == null) return null;
final p = _GridTrackParser._internal(str);
@ -343,12 +339,10 @@ class _GridTrackParser extends _Parser {
return result;
}
/**
* Parses the grid-row-sizing and grid-column-sizing CSS properties into
* object form.
*/
/// Parses the grid-row-sizing and grid-column-sizing CSS properties into
/// object form.
static TrackSizing parseTrackSizing(String? str) {
if (str == null) str = 'auto';
str ??= 'auto';
final p = _GridTrackParser._internal(str);
final result = p._parseTrackMinmax();
p._eatEnd();
@ -364,8 +358,8 @@ class _GridTrackParser extends _Parser {
return GridTrackList(_tracks, _lineNames);
}
/** Code shared by _parseTrackList and _parseTrackGroup */
void _parseTrackListHelper([List<GridTrack>? resultTracks = null]) {
/// Code shared by _parseTrackList and _parseTrackGroup */
void _parseTrackListHelper([List<GridTrack>? resultTracks]) {
_maybeEatWhitespace();
while (!endOfInput) {
String? name;
@ -465,9 +459,7 @@ class _GridTrackParser extends _Parser {
}
}
/**
* Exception thrown because the grid style properties had incorrect values.
*/
/// Exception thrown because the grid style properties had incorrect values.
class SyntaxErrorException implements Exception {
final String _message;
final int _offset;
@ -475,6 +467,7 @@ class SyntaxErrorException implements Exception {
const SyntaxErrorException(this._message, this._source, this._offset);
@override
String toString() {
String location;
if (_offset < _source.length) {

View file

@ -6,33 +6,27 @@ part of layout;
// This file has classes representing the grid tracks and grid template
/**
* The data structure representing the grid-rows or grid-columns
* properties.
*/
/// The data structure representing the grid-rows or grid-columns
/// properties.
class GridTrackList {
/** The set of tracks defined in CSS via grid-rows and grid-columns */
/// The set of tracks defined in CSS via grid-rows and grid-columns */
final List<GridTrack> tracks;
/**
* Maps edge names to the corresponding track. Depending on whether the index
* is used as a start or end, it might be interpreted exclusively or
* inclusively.
*/
/// Maps edge names to the corresponding track. Depending on whether the index
/// is used as a start or end, it might be interpreted exclusively or
/// inclusively.
final Map<String?, int> lineNames;
GridTrackList(this.tracks, this.lineNames) {}
GridTrackList(this.tracks, this.lineNames);
}
/** Represents a row or a column. */
/// Represents a row or a column. */
class GridTrack {
/**
* The start position of this track. Equal to the sum of previous track's
* usedBreadth.
*/
/// The start position of this track. Equal to the sum of previous track's
/// usedBreadth.
late num start;
/** The final computed breadth of this track. */
/// The final computed breadth of this track. */
late num usedBreadth;
// Fields used internally by the sizing algorithm
@ -42,18 +36,16 @@ class GridTrack {
final TrackSizing sizing;
GridTrack(this.sizing) {}
GridTrack(this.sizing);
/**
* Support for the feature that repeats rows and columns, e.g.
* [:grid-columns: 10px ("content" 250px 10px)[4]:]
*/
/// Support for the feature that repeats rows and columns, e.g.
/// [:grid-columns: 10px ("content" 250px 10px)[4]:]
GridTrack clone() => GridTrack(sizing.clone());
/** The min sizing function for the track. */
/// The min sizing function for the track. */
SizingFunction get minSizing => sizing.min;
/** The min sizing function for the track. */
/// The min sizing function for the track. */
SizingFunction get maxSizing => sizing.max;
num get end => start + usedBreadth;
@ -61,14 +53,14 @@ class GridTrack {
bool get isFractional => minSizing.isFraction || maxSizing.isFraction;
}
/** Represents the grid-row-align or grid-column-align. */
/// Represents the grid-row-align or grid-column-align. */
class GridItemAlignment {
// TODO(jmesserly): should this be stored as an int for performance?
final String value;
// 'start' | 'end' | 'center' | 'stretch'
GridItemAlignment.fromString(String? value)
: this.value = (value == null) ? 'stretch' : value {
: value = (value == null) ? 'stretch' : value {
switch (this.value) {
case 'start':
case 'end':
@ -98,10 +90,8 @@ class GridItemAlignment {
}
}
/**
* Represents a grid-template. Used in conjunction with a grid-cell to
* place cells in the grid, without needing to specify the exact row/column.
*/
/// Represents a grid-template. Used in conjunction with a grid-cell to
/// place cells in the grid, without needing to specify the exact row/column.
class GridTemplate {
final Map<int, _GridTemplateRect> _rects;
final int _numRows;
@ -112,7 +102,7 @@ class GridTemplate {
_buildRects(rows);
}
/** Scans the template strings and computes bounds for each one. */
/// Scans the template strings and computes bounds for each one. */
void _buildRects(List<String?> templateRows) {
for (int r = 0; r < templateRows.length; r++) {
String row = templateRows[r]!;
@ -133,9 +123,7 @@ class GridTemplate {
}
}
/**
* Looks up the given cell in the template, and returns the rect.
*/
/// Looks up the given cell in the template, and returns the rect.
_GridTemplateRect lookupCell(String cell) {
if (cell.length != 1) {
throw UnsupportedError(
@ -150,13 +138,13 @@ class GridTemplate {
}
}
/** Used by GridTemplate to track a single cell's bounds. */
/// Used by GridTemplate to track a single cell's bounds. */
class _GridTemplateRect {
int row, column, rowSpan, columnSpan, _count, _char;
_GridTemplateRect(this._char, this.row, this.column)
: rowSpan = 1,
columnSpan = 1,
_count = 1 {}
_count = 1;
void add(int r, int c) {
assert(r >= row && c >= column);
@ -177,13 +165,11 @@ class _GridTemplateRect {
}
}
/**
* Used to return a row/column and span during parsing of grid-row and
* grid-column during parsing.
*/
/// Used to return a row/column and span during parsing of grid-row and
/// grid-column during parsing.
class _GridLocation {
final int? start, length;
_GridLocation(this.start, this.length) {}
_GridLocation(this.start, this.length);
int get end => start! + length!;
}

View file

@ -6,9 +6,7 @@ part of layout;
// This file has classes representing the grid sizing functions
/**
* Represents the sizing function used for the min or max of a row or column.
*/
/// Represents the sizing function used for the min or max of a row or column.
// TODO(jmesserly): rename to GridSizing, or make internal
class SizingFunction {
const SizingFunction();
@ -26,13 +24,11 @@ class SizingFunction {
SizingFunction clone() => this;
}
/**
* Fixed size represents a length as defined by CSS3 Values spec.
* Can also be a percentage of the Grid element's logical width (for columns)
* or logical height (for rows). When the width or height of the Grid element
* is undefined, the percentage is ignored and the Grid Track will be
* auto-sized.
*/
/// Fixed size represents a length as defined by CSS3 Values spec.
/// Can also be a percentage of the Grid element's logical width (for columns)
/// or logical height (for rows). When the width or height of the Grid element
/// is undefined, the percentage is ignored and the Grid Track will be
/// auto-sized.
class FixedSizing extends SizingFunction {
final String units;
final num length;
@ -49,10 +45,13 @@ class FixedSizing extends SizingFunction {
}
// TODO(jmesserly): this is only needed because of our mutable property
@override
FixedSizing clone() => FixedSizing(length, units);
@override
bool get isMinContentSized => _contentSized;
@override
num resolveLength(num? gridSize) {
if (units == '%') {
if (gridSize == null) {
@ -68,60 +67,66 @@ class FixedSizing extends SizingFunction {
}
}
String toString() => 'FixedSizing: ${length}${units} $_contentSized';
@override
String toString() => 'FixedSizing: $length$units $_contentSized';
}
/**
* Fraction is a non-negative floating-point number followed by 'fr'. Each
* fraction value takes a share of the remaining space proportional to its
* number.
*/
/// Fraction is a non-negative floating-point number followed by 'fr'. Each
/// fraction value takes a share of the remaining space proportional to its
/// number.
class FractionSizing extends SizingFunction {
@override
final num fractionValue;
FractionSizing(this.fractionValue);
@override
bool get isFraction => true;
@override
String toString() => 'FixedSizing: ${fractionValue}fr';
}
class MinContentSizing extends SizingFunction {
const MinContentSizing();
@override
bool get isMinContentSized => true;
@override
String toString() => 'MinContentSizing';
}
class MaxContentSizing extends SizingFunction {
const MaxContentSizing();
@override
bool get isMaxContentSized {
return true;
}
@override
String toString() => 'MaxContentSizing';
}
/** The min and max sizing functions for a track. */
/// The min and max sizing functions for a track. */
class TrackSizing {
/** The min sizing function for the track. */
/// The min sizing function for the track. */
final SizingFunction min;
/** The min sizing function for the track. */
/// The min sizing function for the track. */
final SizingFunction max;
const TrackSizing.auto()
: min = const MinContentSizing(),
max = const MaxContentSizing();
TrackSizing(this.min, this.max) {}
TrackSizing(this.min, this.max);
// TODO(jmesserly): this is only needed because FixedSizing is mutable
TrackSizing clone() => TrackSizing(min.clone(), max.clone());
}
/** Represents a GridTrack breadth property. */
/// Represents a GridTrack breadth property. */
// TODO(jmesserly): these classes could be replaced with reflection/mirrors
abstract class _BreadthAccumulator {
void setSize(GridTrack? t, num value);
@ -133,23 +138,29 @@ abstract class _BreadthAccumulator {
class _UsedBreadthAccumulator implements _BreadthAccumulator {
const _UsedBreadthAccumulator();
@override
void setSize(GridTrack? t, num value) {
t!.usedBreadth = value;
}
@override
num? getSize(GridTrack? t) => t!.usedBreadth;
@override
SizingFunction getSizingFunction(GridTrack? t) => t!.minSizing;
}
class _MaxBreadthAccumulator implements _BreadthAccumulator {
const _MaxBreadthAccumulator();
@override
void setSize(GridTrack? t, num value) {
t!.maxBreadth = value;
}
@override
num? getSize(GridTrack? t) => t!.maxBreadth;
@override
SizingFunction getSizingFunction(GridTrack? t) => t!.maxSizing;
}

View file

@ -4,27 +4,25 @@
part of layout;
/** The interface that the layout algorithms use to talk to the view. */
/// The interface that the layout algorithms use to talk to the view. */
abstract class Positionable {
ViewLayout get layout;
/** Gets our custom CSS properties, as provided by the CSS preprocessor. */
/// Gets our custom CSS properties, as provided by the CSS preprocessor. */
Map<String, String> get customStyle;
/** Gets the root DOM used for layout. */
/// Gets the root DOM used for layout. */
Element get node;
/** Gets the collection of child views. */
/// Gets the collection of child views. */
Iterable<Positionable> get childViews;
/** Causes a view to layout its children. */
/// Causes a view to layout its children. */
void doLayout();
}
/**
* Caches the layout parameters that were specified in CSS during a layout
* computation. These values are immutable during a layout.
*/
/// Caches the layout parameters that were specified in CSS during a layout
/// computation. These values are immutable during a layout.
class LayoutParams {
// TODO(jmesserly): should be const, but there's a bug in DartC preventing us
// from calling "window." in an initializer. See b/5332777
@ -40,19 +38,19 @@ class LayoutParams {
// TODO(jmesserly): enums would really help here
class Dimension {
// TODO(jmesserly): perhaps this should be X and Y
static const WIDTH = const Dimension._internal('width');
static const HEIGHT = const Dimension._internal('height');
static const WIDTH = Dimension._internal('width');
static const HEIGHT = Dimension._internal('height');
final String name; // for debugging
const Dimension._internal(this.name);
}
class ContentSizeMode {
/** Minimum content size, e.g. min-width and min-height in CSS. */
static const MIN = const ContentSizeMode._internal('min');
/// Minimum content size, e.g. min-width and min-height in CSS. */
static const MIN = ContentSizeMode._internal('min');
/** Maximum content size, e.g. min-width and min-height in CSS. */
static const MAX = const ContentSizeMode._internal('max');
/// Maximum content size, e.g. min-width and min-height in CSS. */
static const MAX = ContentSizeMode._internal('max');
// TODO(jmesserly): we probably want some sort of "auto" or "best fit" mode
// Don't need it yet though.
@ -61,35 +59,27 @@ class ContentSizeMode {
const ContentSizeMode._internal(this.name);
}
/**
* Abstract base class for View layout. Tracks relevant layout state.
* This code was inspired by code in Android's View.java; it's needed for the
* rest of the layout system.
*/
/// Abstract base class for View layout. Tracks relevant layout state.
/// This code was inspired by code in Android's View.java; it's needed for the
/// rest of the layout system.
class ViewLayout {
/**
* The layout parameters associated with this view and used by the parent
* to determine how this view should be laid out.
*/
/// The layout parameters associated with this view and used by the parent
/// to determine how this view should be laid out.
LayoutParams? layoutParams;
int? _offsetWidth;
int? _offsetHeight;
/** The view that this layout belongs to. */
/// The view that this layout belongs to. */
final Positionable view;
/**
* To get a perforant positioning model on top of the DOM, we read all
* properties in the first pass while computing positions. Then we have a
* second pass that actually moves everything.
*/
/// To get a perforant positioning model on top of the DOM, we read all
/// properties in the first pass while computing positions. Then we have a
/// second pass that actually moves everything.
int? _measuredLeft, _measuredTop, _measuredWidth, _measuredHeight;
ViewLayout(this.view);
/**
* Creates the appropriate view layout, depending on the properties.
*/
/// Creates the appropriate view layout, depending on the properties.
// TODO(jmesserly): we should support user defined layouts somehow. Perhaps
// registered with a LayoutProvider.
factory ViewLayout.fromView(Positionable view) {
@ -126,13 +116,11 @@ class ViewLayout {
int get borderWidth => borderLeftWidth + borderRightWidth;
int get borderHeight => borderTopWidth + borderBottomWidth;
/** Implements the custom layout computation. */
/// Implements the custom layout computation. */
void measureLayout(Future<Size> size, Completer<bool>? changed) {}
/**
* Positions the view within its parent container.
* Also performs a layout of its children.
*/
/// Positions the view within its parent container.
/// Also performs a layout of its children.
void setBounds(int? left, int? top, int width, int height) {
assert(width >= 0 && height >= 0);
@ -147,7 +135,7 @@ class ViewLayout {
measureLayout(completer.future, null);
}
/** Applies the layout to the node. */
/// Applies the layout to the node. */
void applyLayout() {
if (_measuredLeft != null) {
// TODO(jmesserly): benchmark the performance of this DOM interaction
@ -180,12 +168,13 @@ class ViewLayout {
}
int? measureContent(ViewLayout parent, Dimension? dimension,
[ContentSizeMode? mode = null]) {
[ContentSizeMode? mode]) {
if (dimension == Dimension.WIDTH) {
return measureWidth(parent, mode);
} else if (dimension == Dimension.HEIGHT) {
return measureHeight(parent, mode);
}
return null;
}
int? measureWidth(ViewLayout parent, ContentSizeMode? mode) {
@ -195,6 +184,7 @@ class ViewLayout {
} else if (mode == ContentSizeMode.MAX) {
return _styleToPixels(style!.maxWidth, currentWidth, parent.currentWidth);
}
return null;
}
int? measureHeight(ViewLayout parent, ContentSizeMode? mode) {
@ -206,6 +196,7 @@ class ViewLayout {
return _styleToPixels(
style!.maxHeight, currentHeight, parent.currentHeight);
}
return null;
}
static int _toPixels(String style) {

View file

@ -4,58 +4,54 @@
part of observable;
/** A change to an observable instance. */
/// A change to an observable instance. */
class ChangeEvent {
// TODO(sigmund): capture language issues around enums & create a canonical
// Dart enum design.
/** Type denoting an in-place update event. */
/// Type denoting an in-place update event. */
static const UPDATE = 0;
/** Type denoting an insertion event. */
/// Type denoting an insertion event. */
static const INSERT = 1;
/** Type denoting a single-remove event. */
/// Type denoting a single-remove event. */
static const REMOVE = 2;
/**
* Type denoting events that affect the entire observable instance. For
* example, a list operation like clear or sort.
*/
/// Type denoting events that affect the entire observable instance. For
/// example, a list operation like clear or sort.
static const GLOBAL = 3;
/** The observable instance that changed. */
/// The observable instance that changed. */
final Observable target;
/** Whether the change was an [INSERT], [REMOVE], or [UPDATE]. */
/// Whether the change was an [INSERT], [REMOVE], or [UPDATE]. */
final int type;
/** The value after the change (or inserted value in a list). */
/// The value after the change (or inserted value in a list). */
final newValue;
/** The value before the change (or removed value from a list). */
/// The value before the change (or removed value from a list). */
final oldValue;
/** Property that changed (null for list changes). */
/// Property that changed (null for list changes). */
final String? propertyName;
/**
* Index of the list operation. Insertions prepend in front of the given
* index (insert at 0 means an insertion at the beginning of the list).
*/
/// Index of the list operation. Insertions prepend in front of the given
/// index (insert at 0 means an insertion at the beginning of the list).
final int? index;
/** Factory constructor for property change events. */
/// Factory constructor for property change events. */
ChangeEvent.property(
this.target, this.propertyName, this.newValue, this.oldValue)
: type = UPDATE,
index = null;
/** Factory constructor for list change events. */
/// Factory constructor for list change events. */
ChangeEvent.list(
this.target, this.type, this.index, this.newValue, this.oldValue)
: propertyName = null;
/** Factory constructor for [GLOBAL] change events. */
/// Factory constructor for [GLOBAL] change events. */
ChangeEvent.global(this.target)
: type = GLOBAL,
newValue = null,
@ -64,7 +60,7 @@ class ChangeEvent {
index = null;
}
/** A collection of change events on a single observable instance. */
/// A collection of change events on a single observable instance. */
class EventSummary {
final Observable target;
@ -77,9 +73,9 @@ class EventSummary {
events.add(e);
}
/** Notify listeners of [target] and parents of [target] about all changes. */
/// Notify listeners of [target] and parents of [target] about all changes. */
void notify() {
if (!events.isEmpty) {
if (events.isNotEmpty) {
for (Observable? obj = target; obj != null; obj = obj.parent) {
for (final listener in obj.listeners) {
listener(this);
@ -89,5 +85,5 @@ class EventSummary {
}
}
/** A listener of change events. */
typedef void ChangeListener(EventSummary events);
/// A listener of change events. */
typedef ChangeListener = void Function(EventSummary events);

View file

@ -4,43 +4,37 @@
part of observable;
/**
* Accumulates change events from several observable objects.
*
* wrap() is public and used by client code. The other methods are used by
* AbstractObservable, which works with this class to implement batching.
*/
/// Accumulates change events from several observable objects.
///
/// wrap() is public and used by client code. The other methods are used by
/// AbstractObservable, which works with this class to implement batching.
class EventBatch {
/** The current active batch, if any. */
/// The current active batch, if any. */
static EventBatch? current;
/** Used to generate unique ids for observable objects. */
/// Used to generate unique ids for observable objects. */
static int nextUid = 1;
/** Map from observable object's uid to their tracked events. */
/// Map from observable object's uid to their tracked events. */
// TODO(sigmund): use [Observable] instead of [int] when [Map] can support it,
Map<int, EventSummary> summaries;
/** Whether this batch is currently firing and therefore is sealed. */
/// Whether this batch is currently firing and therefore is sealed. */
bool sealed = false;
/**
* Private constructor that shouldn't be used externally. Use [wrap] to ensure
* that a batch exists when running a function.
*/
EventBatch._internal() : summaries = Map<int, EventSummary>();
/// Private constructor that shouldn't be used externally. Use [wrap] to ensure
/// that a batch exists when running a function.
EventBatch._internal() : summaries = <int, EventSummary>{};
/**
* Ensure there is an event batch where [userFunction] can accumulate events.
* When the batch is complete, fire all events at once.
*/
/// Ensure there is an event batch where [userFunction] can accumulate events.
/// When the batch is complete, fire all events at once.
static Function wrap(userFunction(var a)) {
return (e) {
if (current == null) {
// Not in a batch so create one.
final batch = EventBatch._internal();
current = batch;
var result = null;
var result;
try {
// TODO(jmesserly): don't return here, otherwise an exception in
// the finally clause will cause it to rerun. See bug#5350131.
@ -74,12 +68,12 @@ class EventBatch {
};
}
/** Returns a unique global id for observable objects. */
/// Returns a unique global id for observable objects. */
static int genUid() {
return nextUid++;
}
/** Retrieves the events associated with {@code obj}. */
/// Retrieves the events associated with {@code obj}. */
EventSummary getEvents(Observable obj) {
int uid = obj.uid;
EventSummary? summary = summaries[uid];
@ -91,7 +85,7 @@ class EventBatch {
return summary;
}
/** Fires all events at once. */
/// Fires all events at once. */
void _notify() {
assert(!sealed);
sealed = true;

View file

@ -7,61 +7,59 @@ library observable;
part 'ChangeEvent.dart';
part 'EventBatch.dart';
/**
* An object whose changes are tracked and who can issue events notifying how it
* has been changed.
*/
/// An object whose changes are tracked and who can issue events notifying how it
/// has been changed.
abstract class Observable {
/** Returns a globally unique identifier for the object. */
/// Returns a globally unique identifier for the object. */
// TODO(sigmund): remove once dart supports maps with arbitrary keys.
int get uid;
/** Listeners on this model. */
/// Listeners on this model. */
List<ChangeListener> get listeners;
/** The parent observable to notify when this child is changed. */
/// The parent observable to notify when this child is changed. */
Observable? get parent;
/**
* Adds a listener for changes on this observable instance. Returns whether
* the listener was added successfully.
*/
/// Adds a listener for changes on this observable instance. Returns whether
/// the listener was added successfully.
bool addChangeListener(ChangeListener listener);
/**
* Removes a listener for changes on this observable instance. Returns whether
* the listener was removed successfully.
*/
/// Removes a listener for changes on this observable instance. Returns whether
/// the listener was removed successfully.
bool removeChangeListener(ChangeListener listener);
}
/** Common functionality for observable objects. */
/// Common functionality for observable objects. */
class AbstractObservable implements Observable {
/** Unique id to identify this model in an event batch. */
/// Unique id to identify this model in an event batch. */
@override
final int uid;
/** The parent observable to notify when this child is changed. */
/// The parent observable to notify when this child is changed. */
@override
final Observable? parent;
/** Listeners on this model. */
/// Listeners on this model. */
@override
List<ChangeListener> listeners;
/** Whether this object is currently observed by listeners or propagators. */
/// Whether this object is currently observed by listeners or propagators. */
bool get isObserved {
for (Observable? obj = this; obj != null; obj = obj.parent) {
if (listeners.length > 0) {
if (listeners.isNotEmpty) {
return true;
}
}
return false;
}
AbstractObservable([this.parent = null])
AbstractObservable([this.parent])
: uid = EventBatch.genUid(),
listeners = List<ChangeListener>.empty();
@override
bool addChangeListener(ChangeListener listener) {
if (listeners.indexOf(listener, 0) == -1) {
if (!listeners.contains(listener)) {
listeners.add(listener);
return true;
}
@ -69,6 +67,7 @@ class AbstractObservable implements Observable {
return false;
}
@override
bool removeChangeListener(ChangeListener listener) {
var index = listeners.indexOf(listener, 0);
if (index != -1) {
@ -126,62 +125,77 @@ class AbstractObservable implements Observable {
}
}
/** A growable list that fires events when it's modified. */
/// A growable list that fires events when it's modified. */
class ObservableList<T> extends AbstractObservable
implements List<T>, Observable {
/** Underlying list. */
/// Underlying list. */
// TODO(rnystrom): Make this final if we get list.remove().
List<T> _internal;
final List<T> _internal;
ObservableList([Observable? parent = null])
ObservableList([Observable? parent])
: _internal = List<T>.empty(),
super(parent);
@override
T operator [](int index) => _internal[index];
@override
void operator []=(int index, T value) {
recordListUpdate(index, value, _internal[index]);
_internal[index] = value;
}
@override
int get length => _internal.length;
@override
List<R> cast<R>() => _internal.cast<R>();
@override
Iterable<R> whereType<R>() => _internal.whereType<R>();
@override
List<T> operator +(List<T> other) => _internal + other;
@override
Iterable<T> followedBy(Iterable<T> other) => _internal.followedBy(other);
int indexWhere(bool test(T element), [int start = 0]) =>
@override
int indexWhere(bool Function(T element) test, [int start = 0]) =>
_internal.indexWhere(test, start);
int lastIndexWhere(bool test(T element), [int? start]) =>
@override
int lastIndexWhere(bool Function(T element) test, [int? start]) =>
_internal.lastIndexWhere(test, start);
void set length(int value) {
@override
set length(int value) {
_internal.length = value;
recordGlobalChange();
}
@override
void clear() {
_internal.clear();
recordGlobalChange();
}
@override
Iterable<T> get reversed => _internal.reversed;
void sort([int compare(T a, T b)?]) {
@override
void sort([int Function(T a, T b)? compare]) {
//if (compare == null) compare = (u, v) => Comparable.compare(u, v);
_internal.sort(compare);
recordGlobalChange();
}
@override
void add(T element) {
recordListInsert(length, element);
_internal.add(element);
}
@override
void addAll(Iterable<T> elements) {
for (T element in elements) {
add(element);
@ -194,49 +208,61 @@ class ObservableList<T> extends AbstractObservable
return _internal.length;
}
@override
T get first => _internal.first;
void set first(T value) {
@override
set first(T value) {
_internal.first = value;
}
@override
T get last => _internal.last;
void set last(T value) {
@override
set last(T value) {
_internal.last = value;
}
@override
T get single => _internal.single;
@override
void insert(int index, T element) {
_internal.insert(index, element);
recordListInsert(index, element);
}
@override
void insertAll(int index, Iterable<T> iterable) {
throw UnimplementedError();
}
@override
void setAll(int index, Iterable<T> iterable) {
throw UnimplementedError();
}
@override
T removeLast() {
final result = _internal.removeLast();
recordListRemove(length, result);
return result;
}
@override
T removeAt(int index) {
T result = _internal.removeAt(index);
recordListRemove(index, result);
return result;
}
@override
int indexOf(T element, [int start = 0]) {
return _internal.indexOf(element, start);
}
@override
int lastIndexOf(T element, [int? start]) {
if (start == null) start = length - 1;
start ??= length - 1;
return _internal.lastIndexOf(element, start);
}
@ -261,8 +287,8 @@ class ObservableList<T> extends AbstractObservable
void copyFrom(List<T> src, int? srcStart, int? dstStart, int count) {
List dst = this;
if (srcStart == null) srcStart = 0;
if (dstStart == null) dstStart = 0;
srcStart ??= 0;
dstStart ??= 0;
if (srcStart < dstStart) {
for (int i = srcStart + count - 1, j = dstStart + count - 1;
@ -277,88 +303,125 @@ class ObservableList<T> extends AbstractObservable
}
}
@override
void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) {
throw UnimplementedError();
}
@override
void removeRange(int start, int end) {
throw UnimplementedError();
}
@override
void replaceRange(int start, int end, Iterable<T> iterable) {
throw UnimplementedError();
}
@override
void fillRange(int start, int end, [T? fillValue]) {
throw UnimplementedError();
}
@override
List<T> sublist(int start, [int? end]) {
throw UnimplementedError();
}
@override
Iterable<T> getRange(int start, int end) {
throw UnimplementedError();
}
@override
bool contains(Object? element) {
throw UnimplementedError();
}
T reduce(T combine(T previousValue, T element)) {
@override
T reduce(T Function(T previousValue, T element) combine) {
throw UnimplementedError();
}
R fold<R>(R initialValue, R combine(R previousValue, T element)) {
@override
R fold<R>(R initialValue, R Function(R previousValue, T element) combine) {
throw UnimplementedError();
}
// Iterable<T>:
@override
Iterator<T> get iterator => _internal.iterator;
Iterable<T> where(bool f(T element)) => _internal.where(f);
Iterable<R> map<R>(R f(T element)) => _internal.map(f);
Iterable<R> expand<R>(Iterable<R> f(T element)) => _internal.expand(f);
@override
Iterable<T> where(bool Function(T element) f) => _internal.where(f);
@override
Iterable<R> map<R>(R Function(T element) f) => _internal.map(f);
@override
Iterable<R> expand<R>(Iterable<R> Function(T element) f) =>
_internal.expand(f);
@override
List<T> skip(int count) => _internal.skip(count) as List<T>;
@override
List<T> take(int count) => _internal.take(count) as List<T>;
bool every(bool f(T element)) => _internal.every(f);
bool any(bool f(T element)) => _internal.any(f);
void forEach(void f(T element)) {
@override
bool every(bool Function(T element) f) => _internal.every(f);
@override
bool any(bool Function(T element) f) => _internal.any(f);
@override
void forEach(void Function(T element) f) {
_internal.forEach(f);
}
@override
String join([String separator = ""]) => _internal.join(separator);
T firstWhere(bool test(T value), {T orElse()?}) {
@override
T firstWhere(bool Function(T value) test, {T Function()? orElse}) {
return _internal.firstWhere(test, orElse: orElse);
}
T lastWhere(bool test(T value), {T orElse()?}) {
@override
T lastWhere(bool Function(T value) test, {T Function()? orElse}) {
return _internal.lastWhere(test, orElse: orElse);
}
@override
void shuffle([random]) => throw UnimplementedError();
@override
bool remove(Object? element) => throw UnimplementedError();
void removeWhere(bool test(T element)) => throw UnimplementedError();
void retainWhere(bool test(T element)) => throw UnimplementedError();
List<T> toList({bool growable: true}) => throw UnimplementedError();
@override
void removeWhere(bool Function(T element) test) => throw UnimplementedError();
@override
void retainWhere(bool Function(T element) test) => throw UnimplementedError();
@override
List<T> toList({bool growable = true}) => throw UnimplementedError();
@override
Set<T> toSet() => throw UnimplementedError();
Iterable<T> takeWhile(bool test(T value)) => throw UnimplementedError();
Iterable<T> skipWhile(bool test(T value)) => throw UnimplementedError();
@override
Iterable<T> takeWhile(bool Function(T value) test) =>
throw UnimplementedError();
@override
Iterable<T> skipWhile(bool Function(T value) test) =>
throw UnimplementedError();
T singleWhere(bool test(T value), {T orElse()?}) {
@override
T singleWhere(bool Function(T value) test, {T Function()? orElse}) {
return _internal.singleWhere(test, orElse: orElse);
}
@override
T elementAt(int index) {
return _internal.elementAt(index);
}
@override
Map<int, T> asMap() {
return _internal.asMap();
}
@override
bool get isEmpty => length == 0;
@override
bool get isNotEmpty => !isEmpty;
}
@ -368,15 +431,15 @@ class ObservableList<T> extends AbstractObservable
// much. Also, making a value observable necessitates adding ".value" to lots
// of places, and constructing all fields with the verbose
// "ObservableValue<DataType>(myValue)".
/** A wrapper around a single value whose change can be observed. */
/// A wrapper around a single value whose change can be observed. */
class ObservableValue<T> extends AbstractObservable {
ObservableValue(T value, [Observable? parent = null])
ObservableValue(T value, [Observable? parent])
: _value = value,
super(parent);
T get value => _value;
void set value(T newValue) {
set value(T newValue) {
// Only fire on an actual change.
if (!identical(newValue, _value)) {
final oldValue = _value;

View file

@ -6,40 +6,34 @@
part of touch;
/**
* Functions to model constant acceleration as a cubic Bezier
* curve (http://en.wikipedia.org/wiki/Bezier_curve). These functions are
* intended to generate the transition timing function for CSS transitions.
* Please see
* [http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag].
*
* The main operation of computing a cubic Bezier is split up into multiple
* functions so that, should it be required, more operations and cases can be
* supported in the future.
*/
/// Functions to model constant acceleration as a cubic Bezier
/// curve (http://en.wikipedia.org/wiki/Bezier_curve). These functions are
/// intended to generate the transition timing function for CSS transitions.
/// Please see
/// [http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag].
///
/// The main operation of computing a cubic Bezier is split up into multiple
/// functions so that, should it be required, more operations and cases can be
/// supported in the future.
class BezierPhysics {
static const _ONE_THIRD = 1 / 3;
static const _TWO_THIRDS = 2 / 3;
/**
* A list [:[x1, y1, x2, y2]:] of the intermediate control points of a cubic
* bezier when the final velocity is zero. This is a special case for which
* these control points are constants.
*/
static const List<num> _FINAL_VELOCITY_ZERO_BEZIER = const [
/// A list [:[x1, y1, x2, y2]:] of the intermediate control points of a cubic
/// bezier when the final velocity is zero. This is a special case for which
/// these control points are constants.
static const List<num> _FINAL_VELOCITY_ZERO_BEZIER = [
_ONE_THIRD,
_TWO_THIRDS,
_TWO_THIRDS,
1
];
/**
* Given consistent kinematics parameters for constant acceleration, returns
* the intermediate control points of the cubic Bezier curve that models the
* motion. All input values must have correct signs.
* Returns a list [:[x1, y1, x2, y2]:] representing the intermediate control
* points of the cubic Bezier.
*/
/// Given consistent kinematics parameters for constant acceleration, returns
/// the intermediate control points of the cubic Bezier curve that models the
/// motion. All input values must have correct signs.
/// Returns a list [:[x1, y1, x2, y2]:] representing the intermediate control
/// points of the cubic Bezier.
static List<num> calculateCubicBezierFromKinematics(num initialVelocity,
num finalVelocity, num totalTime, num totalDisplacement) {
// Total time must be greater than 0.
@ -61,18 +55,16 @@ class BezierPhysics {
return _quadraticToCubic(controlPoint[0], controlPoint[1]);
}
/**
* Given a quadratic curve crossing points (0, 0) and (x2, y2), calculates the
* intermediate control point (x1, y1) of the equivalent quadratic Bezier
* curve with starting point (0, 0) and ending point (x2, y2).
* [m0] The slope of the line tangent to the curve at (0, 0).
* [m2] The slope of the line tangent to the curve at a different
* point (x2, y2).
* [x2] The x-coordinate of the other point on the curve.
* [y2] The y-coordinate of the other point on the curve.
* Returns a list [:[x1, y1]:] representing the intermediate
* control point of the quadratic Bezier.
*/
/// Given a quadratic curve crossing points (0, 0) and (x2, y2), calculates the
/// intermediate control point (x1, y1) of the equivalent quadratic Bezier
/// curve with starting point (0, 0) and ending point (x2, y2).
/// [m0] The slope of the line tangent to the curve at (0, 0).
/// [m2] The slope of the line tangent to the curve at a different
/// point (x2, y2).
/// [x2] The x-coordinate of the other point on the curve.
/// [y2] The y-coordinate of the other point on the curve.
/// Returns a list [:[x1, y1]:] representing the intermediate
/// control point of the quadratic Bezier.
static List<num> _tangentLinesToQuadraticBezier(
num m0, num m2, num x2, num y2) {
if (GoogleMath.nearlyEquals(m0, m2)) {
@ -83,30 +75,26 @@ class BezierPhysics {
return [x1, y1];
}
/**
* Normalizes a quadratic Bezier curve to have end point at (1, 1).
* [x1] The x-coordinate of the intermediate control point.
* [y1] The y-coordinate of the intermediate control point.
* [x2] The x-coordinate of the end point.
* [y2] The y-coordinate of the end point.
* Returns a list [:[x1, y1]:] representing the intermediate control point.
*/
/// Normalizes a quadratic Bezier curve to have end point at (1, 1).
/// [x1] The x-coordinate of the intermediate control point.
/// [y1] The y-coordinate of the intermediate control point.
/// [x2] The x-coordinate of the end point.
/// [y2] The y-coordinate of the end point.
/// Returns a list [:[x1, y1]:] representing the intermediate control point.
static List<num> _normalizeQuadraticBezier(num x1, num y1, num x2, num y2) {
// The end point must not lie on any axes.
assert(!GoogleMath.nearlyEquals(x2, 0) && !GoogleMath.nearlyEquals(y2, 0));
return [x1 / x2, y1 / y2];
}
/**
* Converts a quadratic Bezier curve defined by the control points
* (x0, y0) = (0, 0), (x1, y1) = (x, y), and (x2, y2) = (1, 1) into an
* equivalent cubic Bezier curve with four control points. Note that the start
* and end points will be unchanged.
* [x] The x-coordinate of the intermediate control point.
* [y] The y-coordinate of the intermediate control point.
* Returns a list [:[x1, y1, x2, y2]:] containing the two
* intermediate points of the equivalent cubic Bezier curve.
*/
/// Converts a quadratic Bezier curve defined by the control points
/// (x0, y0) = (0, 0), (x1, y1) = (x, y), and (x2, y2) = (1, 1) into an
/// equivalent cubic Bezier curve with four control points. Note that the start
/// and end points will be unchanged.
/// [x] The x-coordinate of the intermediate control point.
/// [y] The y-coordinate of the intermediate control point.
/// Returns a list [:[x1, y1, x2, y2]:] containing the two
/// intermediate points of the equivalent cubic Bezier curve.
static List<num> _quadraticToCubic(num x, num y) {
// The intermediate control point must have coordinates within the
// interval [0,1].

View file

@ -6,57 +6,47 @@
part of touch;
/**
* Click buster implementation, which is a behavior that prevents native clicks
* from firing at undesirable times. There are two scenarios where we may want
* to 'bust' a click.
*
* Buttons implemented with touch events usually have click handlers as well.
* This is because sometimes touch events stop working, and the click handler
* serves as a fallback. Here we use a click buster to prevent the native click
* from firing if the touchend event was successfully handled.
*
* When native scrolling behavior is disabled (see Scroller), click events will
* fire after the touchend event when the drag sequence is complete. The click
* event also happens to fire at the location of the touchstart event which can
* lead to some very strange behavior.
*
* This class puts a single click handler on the body, and calls preventDefault
* on the click event if we detect that there was a touchend event that already
* fired in the same spot recently.
*/
/// Click buster implementation, which is a behavior that prevents native clicks
/// from firing at undesirable times. There are two scenarios where we may want
/// to 'bust' a click.
///
/// Buttons implemented with touch events usually have click handlers as well.
/// This is because sometimes touch events stop working, and the click handler
/// serves as a fallback. Here we use a click buster to prevent the native click
/// from firing if the touchend event was successfully handled.
///
/// When native scrolling behavior is disabled (see Scroller), click events will
/// fire after the touchend event when the drag sequence is complete. The click
/// event also happens to fire at the location of the touchstart event which can
/// lead to some very strange behavior.
///
/// This class puts a single click handler on the body, and calls preventDefault
/// on the click event if we detect that there was a touchend event that already
/// fired in the same spot recently.
class ClickBuster {
/**
* The threshold for how long we allow a click to occur after a touchstart.
*/
/// The threshold for how long we allow a click to occur after a touchstart.
static const _TIME_THRESHOLD = 2500;
/**
* The threshold for how close a click has to be to the saved coordinate for
* us to allow it.
*/
/// The threshold for how close a click has to be to the saved coordinate for
/// us to allow it.
static const _DISTANCE_THRESHOLD = 25;
/**
* The list of coordinates that we use to measure the distance of clicks from.
* If a click is within the distance threshold of any of these coordinates
* then we allow the click.
*/
/// The list of coordinates that we use to measure the distance of clicks from.
/// If a click is within the distance threshold of any of these coordinates
/// then we allow the click.
static DoubleLinkedQueue<num> _coordinates;
/** The last time preventGhostClick was called. */
/// The last time preventGhostClick was called. */
static int _lastPreventedTime;
/**
* This handler will prevent the default behavior for any clicks unless the
* click is within the distance threshold of one of the temporary allowed
* coordinates.
*/
/// This handler will prevent the default behavior for any clicks unless the
/// click is within the distance threshold of one of the temporary allowed
/// coordinates.
static void _onClick(Event e) {
if (TimeUtil.now() - _lastPreventedTime > _TIME_THRESHOLD) {
return;
}
final coord = new Coordinate.fromClient(e);
final coord = Coordinate.fromClient(e);
// TODO(rnystrom): On Android, we get spurious click events at (0, 0). We
// *do* want those clicks to be busted, so commenting this out fixes it.
// Leaving it commented out instead of just deleting it because I'm not sure
@ -87,33 +77,27 @@ class ClickBuster {
e.preventDefault();
}
/**
* This handler will temporarily allow a click to occur near the touch event's
* coordinates.
*/
/// This handler will temporarily allow a click to occur near the touch event's
/// coordinates.
static void _onTouchStart(Event e) {
TouchEvent te = e;
final coord = new Coordinate.fromClient(te.touches[0]);
final coord = Coordinate.fromClient(te.touches[0]);
_coordinates.add(coord.x);
_coordinates.add(coord.y);
new Timer(const Duration(milliseconds: _TIME_THRESHOLD), () {
Timer(const Duration(milliseconds: _TIME_THRESHOLD), () {
_removeCoordinate(coord.x, coord.y);
});
_toggleTapHighlights(true);
}
/**
* Hit test for whether a coordinate is within the distance threshold of an
* event.
*/
/// Hit test for whether a coordinate is within the distance threshold of an
/// event.
static bool _hitTest(num x, num y, num eventX, num eventY) {
return (eventX - x).abs() < _DISTANCE_THRESHOLD &&
(eventY - y).abs() < _DISTANCE_THRESHOLD;
}
/**
* Remove one specified coordinate from the coordinates list.
*/
/// Remove one specified coordinate from the coordinates list.
static void _removeCoordinate(num x, num y) {
DoubleLinkedQueueEntry<num> entry = _coordinates.firstEntry();
while (entry != null) {
@ -127,26 +111,22 @@ class ClickBuster {
}
}
/**
* Enable or disable tap highlights. They are disabled when preventGhostClick
* is called so that the flicker on links is not invoked when the ghost click
* does fire. This is due to a bug: links get highlighted even if the click
* event has preventDefault called on it.
*/
/// Enable or disable tap highlights. They are disabled when preventGhostClick
/// is called so that the flicker on links is not invoked when the ghost click
/// does fire. This is due to a bug: links get highlighted even if the click
/// event has preventDefault called on it.
static void _toggleTapHighlights(bool enable) {
document.body.style.setProperty(
"-webkit-tap-highlight-color", enable ? "" : "rgba(0,0,0,0)", "");
}
/**
* Registers new touches to create temporary "allowable zones" and registers
* new clicks to be prevented unless they fall in one of the current
* "allowable zones". Note that if the touchstart and touchend locations are
* different, it is still possible for a ghost click to be fired if you
* called preventDefault on all touchmove events. In this case the ghost
* click will be fired at the location of the touchstart event, so the
* coordinate you pass in should be the coordinate of the touchstart.
*/
/// Registers new touches to create temporary "allowable zones" and registers
/// new clicks to be prevented unless they fall in one of the current
/// "allowable zones". Note that if the touchstart and touchend locations are
/// different, it is still possible for a ghost click to be fired if you
/// called preventDefault on all touchmove events. In this case the ghost
/// click will be fired at the location of the touchstart event, so the
/// coordinate you pass in should be the coordinate of the touchstart.
static void preventGhostClick(num x, num y) {
// First time this is called the following occurs:
// 1) Attaches a handler to touchstart events so that each touch will
@ -196,14 +176,14 @@ class ClickBuster {
if (!Device.supportsTouch) {
startFn = mouseToTouchCallback(startFn);
}
var stream;
Stream<UIEvent> stream;
if (Device.supportsTouch) {
stream = Element.touchStartEvent.forTarget(document, useCapture: true);
} else {
stream = Element.mouseDownEvent.forTarget(document, useCapture: true);
}
EventUtil.observe(document, stream, startFn, true);
_coordinates = new DoubleLinkedQueue<num>();
_coordinates = DoubleLinkedQueue<num>();
}
// Turn tap highlights off until we know the ghost click has fired.

View file

@ -7,17 +7,13 @@
part of touch;
/**
* Common events related helpers.
*/
/// Common events related helpers.
class EventUtil {
/**
* Add an event listener to an element.
* The event callback is specified by [handler].
* If [capture] is true, the listener gets events on the capture phase.
* If [removeHandlerOnFocus] is true the handler is removed when there is any
* focus event, and added back on blur events.
*/
/// Add an event listener to an element.
/// The event callback is specified by [handler].
/// If [capture] is true, the listener gets events on the capture phase.
/// If [removeHandlerOnFocus] is true the handler is removed when there is any
/// focus event, and added back on blur events.
static void observe(
/*Element or Document*/ element, Stream stream, Function handler,
[bool removeHandlerOnFocus = false]) {
@ -33,11 +29,9 @@ class EventUtil {
}
}
/**
* Clear the keyboard focus of the currently focused element (if there is
* one). If there is no currently focused element then this function will do
* nothing. For most browsers this will cause the keyboard to be dismissed.
*/
/// Clear the keyboard focus of the currently focused element (if there is
/// one). If there is no currently focused element then this function will do
/// nothing. For most browsers this will cause the keyboard to be dismissed.
static void blurFocusedElement() {
Element focusedEl = document.querySelector("*:focus");
if (focusedEl != null) {

View file

@ -6,23 +6,21 @@
part of touch;
/**
* Common effects related helpers.
*/
/// Common effects related helpers.
class FxUtil {
/** On transition end event. */
/// On transition end event. */
static const TRANSITION_END_EVENT = 'webkitTransitionEnd';
/** The translate3d transform function. */
/// The translate3d transform function. */
static const TRANSLATE_3D = 'translate3d';
/** The rotate transform function. */
/// The rotate transform function. */
static const ROTATE = 'rotate';
/** The scale transform function. */
/// The scale transform function. */
static const SCALE = 'scale';
/** Stops and clears the transition on an element. */
/// Stops and clears the transition on an element. */
static void clearWebkitTransition(Element el) {
el.style.transition = '';
}
@ -30,31 +28,27 @@ class FxUtil {
static void setPosition(Element el, Coordinate point) {
num x = point.x;
num y = point.y;
el.style.transform = '${TRANSLATE_3D}(${x}px,${y}px,0px)';
el.style.transform = '$TRANSLATE_3D(${x}px,${y}px,0px)';
}
/** Apply a transform using translate3d to an HTML element. */
/// Apply a transform using translate3d to an HTML element. */
static void setTranslate(Element el, num x, num y, num z) {
el.style.transform = '${TRANSLATE_3D}(${x}px,${y}px,${z}px)';
el.style.transform = '$TRANSLATE_3D(${x}px,${y}px,${z}px)';
}
/** Apply a -webkit-transform using translate3d to an HTML element. */
/// Apply a -webkit-transform using translate3d to an HTML element. */
static void setWebkitTransform(Element el, num x, num y,
[num z = 0,
num rotation = null,
num scale = null,
num originX = null,
num originY = null]) {
[num z = 0, num rotation, num scale, num originX, num originY]) {
final style = el.style;
// TODO(jacobr): create a helper class that simplifies building
// transformation matricies that will be set as CSS styles. We should
// consider using CSSMatrix although that may be overkill.
String transform = '${TRANSLATE_3D}(${x}px,${y}px,${z}px)';
String transform = '$TRANSLATE_3D(${x}px,${y}px,${z}px)';
if (rotation != null) {
transform += ' ${ROTATE}(${rotation}deg)';
transform += ' $ROTATE(${rotation}deg)';
}
if (scale != null) {
transform += ' ${SCALE}(${scale})';
transform += ' $SCALE($scale)';
}
style.transform = transform;
if (originX != null || originY != null) {
@ -63,15 +57,13 @@ class FxUtil {
}
}
/**
* Determine the position of an [element] relative to a [target] element.
* Moving the [element] to be a child of [target] and setting the
* [element]'s top and left values to the returned coordinate should result
* in the [element]'s position remaining unchanged while its parent is
* changed.
*/
/// Determine the position of an [element] relative to a [target] element.
/// Moving the [element] to be a child of [target] and setting the
/// [element]'s top and left values to the returned coordinate should result
/// in the [element]'s position remaining unchanged while its parent is
/// changed.
static Coordinate computeRelativePosition(Element element, Element target) {
final testPoint = new Point(0, 0);
final testPoint = Point(0, 0);
/*
final pagePoint =
window.convertPointFromNodeToPage(element, testPoint);
@ -83,25 +75,21 @@ class FxUtil {
// `convertPointFromPageToNode`.
var eRect = element.getBoundingClientRect();
var tRect = target.getBoundingClientRect();
return new Coordinate(eRect.left - tRect.left, eRect.top - tRect.top);
return Coordinate(eRect.left - tRect.left, eRect.top - tRect.top);
}
/** Clear a -webkit-transform from an element. */
/// Clear a -webkit-transform from an element. */
static void clearWebkitTransform(Element el) {
el.style.transform = '';
}
/**
* Checks whether an element has a translate3d webkit transform applied.
*/
/// Checks whether an element has a translate3d webkit transform applied.
static bool hasWebkitTransform(Element el) {
return el.style.transform.indexOf(TRANSLATE_3D, 0) != -1;
return el.style.transform.contains(TRANSLATE_3D, 0);
}
/**
* Translates [el], an HTML element that has a relative CSS
* position, by setting its left and top CSS styles.
*/
/// Translates [el], an HTML element that has a relative CSS
/// position, by setting its left and top CSS styles.
static void setLeftAndTop(Element el, num x, num y) {
final style = el.style;
style.left = '${x}px';

View file

@ -6,30 +6,22 @@
part of touch;
/**
* Represents a point in 2 dimensional space.
*/
/// Represents a point in 2 dimensional space.
class Coordinate {
/**
* X-value
*/
/// X-value
num x;
/**
* Y-value
*/
/// Y-value
num y;
Coordinate([num this.x = 0, num this.y = 0]) {}
Coordinate([this.x = 0, this.y = 0]);
/**
* Gets the coordinates of a touch's location relative to the window's
* viewport. [input] is either a touch object or an event object.
*/
/// Gets the coordinates of a touch's location relative to the window's
/// viewport. [input] is either a touch object or an event object.
Coordinate.fromClient(var input) : this(input.client.x, input.client.y);
static Coordinate difference(Coordinate a, Coordinate b) {
return new Coordinate(a.x - b.x, a.y - b.y);
return Coordinate(a.x - b.x, a.y - b.y);
}
static num distance(Coordinate a, Coordinate b) {
@ -38,11 +30,13 @@ class Coordinate {
return Math.sqrt(dx * dx + dy * dy);
}
@override
bool operator ==(covariant Coordinate other) {
return other != null && x == other.x && y == other.y;
return x == other.x && y == other.y;
}
int get hashCode => throw new UnimplementedError();
@override
int get hashCode => throw UnimplementedError();
static num squaredDistance(Coordinate a, Coordinate b) {
final dx = a.x - b.x;
@ -51,45 +45,45 @@ class Coordinate {
}
static Coordinate sum(Coordinate a, Coordinate b) {
return new Coordinate(a.x + b.x, a.y + b.y);
return Coordinate(a.x + b.x, a.y + b.y);
}
/**
* Returns a new copy of the coordinate.
*/
Coordinate clone() => new Coordinate(x, y);
/// Returns a new copy of the coordinate.
Coordinate clone() => Coordinate(x, y);
@override
String toString() => "($x, $y)";
}
/**
* Represents the interval { x | start <= x < end }.
*/
/// Represents the interval { x | start <= x < end }.
class Interval {
final num start;
final num end;
Interval(num this.start, num this.end) {}
Interval(this.start, this.end);
num get length {
return end - start;
}
@override
bool operator ==(covariant Interval other) {
return other != null && other.start == start && other.end == end;
return other.start == start && other.end == end;
}
int get hashCode => throw new UnimplementedError();
@override
int get hashCode => throw UnimplementedError();
Interval union(Interval other) {
return new Interval(Math.min(start, other.start), Math.max(end, other.end));
return Interval(Math.min(start, other.start), Math.max(end, other.end));
}
bool contains(num value) {
return value >= start && value < end;
}
@override
String toString() {
return '(${start}, ${end})';
return '($start, $end)';
}
}

View file

@ -6,50 +6,44 @@
part of touch;
/**
* Adds a listener to the scroller with triggers events
* when a trigger point at the top, or bottom, of the screen is reached.
*
* To use this you will need to have an element with a scroller attached
* to it. You need to have defined (in pixels) how far from the top or
* bottom the scroll position must be in order to trigger (the "trigger
* point") The element using this must have functions for hitting the top
* trigger, and the bottom trigger. In general, these methods will
* ascertain whether we have more data to scroll to (i.e. when we hit
* the bottom trigger point but have reached the end of the data
* displayed in the element we should ignore it), make the call for
* more data and reposition the scroller - repositioning is key to
* good user experience.
*
* Triggers are generated by listening for the SCROLL_END event from the
* scroller, so data calls are not initiated whilst scrolling is happening,
* but after.
*
* Controls changing divs between the usual (non-loading) div and the
* loading div. To take advantage of this, callback function should return
* a boolean indicating whether the usual div should be replaced by the
* loading div.
*/
/// Adds a listener to the scroller with triggers events
/// when a trigger point at the top, or bottom, of the screen is reached.
///
/// To use this you will need to have an element with a scroller attached
/// to it. You need to have defined (in pixels) how far from the top or
/// bottom the scroll position must be in order to trigger (the "trigger
/// point") The element using this must have functions for hitting the top
/// trigger, and the bottom trigger. In general, these methods will
/// ascertain whether we have more data to scroll to (i.e. when we hit
/// the bottom trigger point but have reached the end of the data
/// displayed in the element we should ignore it), make the call for
/// more data and reposition the scroller - repositioning is key to
/// good user experience.
///
/// Triggers are generated by listening for the SCROLL_END event from the
/// scroller, so data calls are not initiated whilst scrolling is happening,
/// but after.
///
/// Controls changing divs between the usual (non-loading) div and the
/// loading div. To take advantage of this, callback function should return
/// a boolean indicating whether the usual div should be replaced by the
/// loading div.
class InfiniteScroller {
Scroller _scroller;
final Scroller _scroller;
/**
* Function to invoke when trigger point is reached at the top of the view.
*/
Function _onTopScroll;
/// Function to invoke when trigger point is reached at the top of the view.
final Function _onTopScroll;
/**
* Function to invoke when trigger point is reached at the bottom of the view.
*/
Function _onBottomScroll;
/// Function to invoke when trigger point is reached at the bottom of the view.
final Function _onBottomScroll;
/** Offset for trigger point at the top of the view. */
double _offsetTop;
/// Offset for trigger point at the top of the view. */
final double _offsetTop;
/** Offset for trigger point at the bottom of the view. */
double _offsetBottom;
/// Offset for trigger point at the bottom of the view. */
final double _offsetBottom;
/** Saves the last Y position. */
/// Saves the last Y position. */
double _lastScrollY;
Element _topDiv;
Element _topLoadingDiv;
@ -58,28 +52,26 @@ class InfiniteScroller {
InfiniteScroller(Scroller scroller, Function onTopScroll,
Function onBottomScroll, double offsetTop,
[double offsetBottom = null])
[double offsetBottom])
: _scroller = scroller,
_onTopScroll = onTopScroll,
_onBottomScroll = onBottomScroll,
_offsetTop = offsetTop,
_offsetBottom = offsetBottom == null ? offsetTop : offsetBottom,
_lastScrollY = 0.0 {}
_offsetBottom = offsetBottom ?? offsetTop,
_lastScrollY = 0.0;
/**
* Adds the loading divs.
* [topDiv] The div usually shown at the top.
* [topLoadingDiv] is the div to show at the top when waiting for more
* content to load at the top of the page.
* [bottomDiv] is the div usually shown at the bottom.
* [bottomLoadingDiv] is the div to show at the bottom when waiting for more
* content to load at the end of the page.
*/
/// Adds the loading divs.
/// [topDiv] The div usually shown at the top.
/// [topLoadingDiv] is the div to show at the top when waiting for more
/// content to load at the top of the page.
/// [bottomDiv] is the div usually shown at the bottom.
/// [bottomLoadingDiv] is the div to show at the bottom when waiting for more
/// content to load at the end of the page.
void addLoadingDivs(
[Element topDiv = null,
Element topLoadingDiv = null,
Element bottomDiv = null,
Element bottomLoadingDiv = null]) {
[Element topDiv,
Element topLoadingDiv,
Element bottomDiv,
Element bottomLoadingDiv]) {
_topDiv = topDiv;
_topLoadingDiv = topLoadingDiv;
_bottomDiv = bottomDiv;
@ -92,18 +84,14 @@ class InfiniteScroller {
_registerEventListeners();
}
/**
* Switch back the divs after loading complete. Delegate should call
* this function after loading is complete.
*/
/// Switch back the divs after loading complete. Delegate should call
/// this function after loading is complete.
void loadEnd() {
_updateVisibility(false, _topDiv, _topLoadingDiv);
_updateVisibility(false, _bottomDiv, _bottomLoadingDiv);
}
/**
* Called at the end of a scroll event.
*/
/// Called at the end of a scroll event.
void _onScrollEnd() {
double ypos = _scroller.getVerticalOffset();
@ -127,18 +115,14 @@ class InfiniteScroller {
_lastScrollY = ypos;
}
/**
* Register the event listeners.
*/
/// Register the event listeners.
void _registerEventListeners() {
_scroller.onScrollerEnd.listen((Event event) {
_onScrollEnd();
});
}
/**
* Hides one div and shows another.
*/
/// Hides one div and shows another.
void _updateVisibility(
bool isLoading, Element element, Element loadingElement) {
if (element != null) {

View file

@ -7,25 +7,19 @@
part of touch;
// TODO(jacobr): pick a better name. This was goog.math in Closure.
/**
* Math utility functions originally from the closure Math library.
*/
/// Math utility functions originally from the closure Math library.
class GoogleMath {
/**
* Takes a [value] and clamps it to within the bounds specified by
* [min] and [max].
*/
/// Takes a [value] and clamps it to within the bounds specified by
/// [min] and [max].
static num clamp(num value, num min, num max) {
return Math.min(Math.max(value, min), max);
}
/**
* Tests whether the two values are equal to each other, within a certain
* tolerance to adjust for floating point errors.
* The optional [tolerance] value d Defaults to 0.000001. If specified,
* it should be greater than 0.
* Returns whether [a] and [b] are nearly equal.
*/
/// Tests whether the two values are equal to each other, within a certain
/// tolerance to adjust for floating point errors.
/// The optional [tolerance] value d Defaults to 0.000001. If specified,
/// it should be greater than 0.
/// Returns whether [a] and [b] are nearly equal.
static bool nearlyEquals(num a, num b, [num tolerance = 0.000001]) {
return (a - b).abs() <= tolerance;
}

View file

@ -6,31 +6,29 @@
part of touch;
/**
* Implementations can be used to simulate the deceleration of an element within
* a certain region. To use this behavior you need to provide an initial
* velocity that is meant to represent the gesture that is initiating this
* deceleration. You also provide the bounds of the region that the element
* exists in, and the current offset of the element within that region. The
* transitions will have the element decelerate to rest, or stretch past the
* offset boundaries and then come to rest.
*
* This is primarily designed to solve the problem of slow scrolling in mobile
* safari. You can use this along with the [Scroller] behavior to make a
* scrollable area scroll the same way it would in a native application.
*
* Implementations of this interface do not maintain any references to HTML
* elements, and therefore cannot do any redrawing of elements. They only
* calculates where the element should be on an interval. It is the delegate's
* responsibility to redraw the element when the onDecelerate callback is
* invoked. It is recommended that you move the element with a hardware
* accelerated method such as using 'translate3d' on the element's
* -webkit-transform style property.
*/
/// Implementations can be used to simulate the deceleration of an element within
/// a certain region. To use this behavior you need to provide an initial
/// velocity that is meant to represent the gesture that is initiating this
/// deceleration. You also provide the bounds of the region that the element
/// exists in, and the current offset of the element within that region. The
/// transitions will have the element decelerate to rest, or stretch past the
/// offset boundaries and then come to rest.
///
/// This is primarily designed to solve the problem of slow scrolling in mobile
/// safari. You can use this along with the [Scroller] behavior to make a
/// scrollable area scroll the same way it would in a native application.
///
/// Implementations of this interface do not maintain any references to HTML
/// elements, and therefore cannot do any redrawing of elements. They only
/// calculates where the element should be on an interval. It is the delegate's
/// responsibility to redraw the element when the onDecelerate callback is
/// invoked. It is recommended that you move the element with a hardware
/// accelerated method such as using 'translate3d' on the element's
/// -webkit-transform style property.
abstract class Momentum {
factory Momentum(MomentumDelegate delegate,
[num defaultDecelerationFactor = 1]) =>
new TimeoutMomentum(delegate, defaultDecelerationFactor);
TimeoutMomentum(delegate, defaultDecelerationFactor);
bool get decelerating;
@ -42,50 +40,40 @@ abstract class Momentum {
*/
void onTransitionEnd();
/**
* Start decelerating.
* The [velocity] passed should be in terms of number of pixels / millisecond.
* [minCoord] and [maxCoord] specify the content's scrollable boundary.
* The current offset of the element within its boundaries is specified by
* [initialOffset].
* Returns true if deceleration has been initiated.
*/
/// Start decelerating.
/// The [velocity] passed should be in terms of number of pixels / millisecond.
/// [minCoord] and [maxCoord] specify the content's scrollable boundary.
/// The current offset of the element within its boundaries is specified by
/// [initialOffset].
/// Returns true if deceleration has been initiated.
bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
Coordinate initialOffset,
[num decelerationFactor]);
/**
* Calculate the velocity required to transition between coordinates [start]
* and [target] optionally specifying a custom [decelerationFactor].
*/
/// Calculate the velocity required to transition between coordinates [start]
/// and [target] optionally specifying a custom [decelerationFactor].
Coordinate calculateVelocity(Coordinate start, Coordinate target,
[num decelerationFactor]);
/** Stop decelerating and return the current velocity. */
/// Stop decelerating and return the current velocity. */
Coordinate stop();
/** Aborts decelerating without dispatching any notification events. */
/// Aborts decelerating without dispatching any notification events. */
void abort();
/** null if no transition is in progress. */
/// null if no transition is in progress. */
Coordinate get destination;
}
/**
* Momentum Delegate interface.
* You are required to implement this interface in order to use the
* Momentum behavior.
*/
/// Momentum Delegate interface.
/// You are required to implement this interface in order to use the
/// Momentum behavior.
abstract class MomentumDelegate {
/**
* Callback for a deceleration step. The delegate is responsible for redrawing
* the element in its new position specified in px.
*/
/// Callback for a deceleration step. The delegate is responsible for redrawing
/// the element in its new position specified in px.
void onDecelerate(num x, num y);
/**
* Callback for end of deceleration.
*/
/// Callback for end of deceleration.
void onDecelerationEnd();
}
@ -105,14 +93,12 @@ class _Move {
_Move(this.x, this.y, this.vx, this.vy, this.time);
}
/**
* Secant method root solver helper class.
* We use http://en.wikipedia.org/wiki/Secant_method
* falling back to the http://en.wikipedia.org/wiki/Bisection_method
* if it doesn't appear we are converging properlty.
* TODO(jacobr): simplify the code so we don't have to use this solver
* class at all.
*/
/// Secant method root solver helper class.
/// We use http://en.wikipedia.org/wiki/Secant_method
/// falling back to the http://en.wikipedia.org/wiki/Bisection_method
/// if it doesn't appear we are converging properlty.
/// TODO(jacobr): simplify the code so we don't have to use this solver
/// class at all.
class Solver {
static num solve(num Function(num) fn, num targetY, num startX,
[int maxIterations = 50]) {
@ -120,8 +106,8 @@ class Solver {
num lastY = fn(lastX);
num deltaX;
num deltaY;
num minX = null;
num maxX = null;
num minX;
num maxX;
num x = startX;
num delta = startX;
for (int i = 0; i < maxIterations; i++) {
@ -151,94 +137,74 @@ class Solver {
x = (minX + maxX) / 2;
}
}
window.console.warn('''Could not find an exact solution. LastY=${lastY},
targetY=${targetY} lastX=$lastX delta=$delta deltaX=$deltaX
window.console.warn('''Could not find an exact solution. LastY=$lastY,
targetY=$targetY lastX=$lastX delta=$delta deltaX=$deltaX
deltaY=$deltaY''');
return x;
}
}
/**
* Helper class modeling the physics of a throwable scrollable area along a
* single dimension.
*/
/// Helper class modeling the physics of a throwable scrollable area along a
/// single dimension.
class SingleDimensionPhysics {
/** The number of frames per second the animation should run at. */
/// The number of frames per second the animation should run at. */
static const _FRAMES_PER_SECOND = 60;
/**
* The spring coefficient for when the element has passed a boundary and is
* decelerating to change direction and bounce back. Each frame, the velocity
* will be changed by x times this coefficient, where x is the current stretch
* value of the element from its boundary. This will end when velocity reaches
* zero.
*/
/// The spring coefficient for when the element has passed a boundary and is
/// decelerating to change direction and bounce back. Each frame, the velocity
/// will be changed by x times this coefficient, where x is the current stretch
/// value of the element from its boundary. This will end when velocity reaches
/// zero.
static const _PRE_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
/**
* The spring coefficient for when the element is bouncing back from a
* stretched offset to a min or max position. Each frame, the velocity will
* be changed to x times this coefficient, where x is the current stretch
* value of the element from its boundary. This will end when the stretch
* value reaches 0.
*/
/// The spring coefficient for when the element is bouncing back from a
/// stretched offset to a min or max position. Each frame, the velocity will
/// be changed to x times this coefficient, where x is the current stretch
/// value of the element from its boundary. This will end when the stretch
/// value reaches 0.
static const _POST_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
/**
* The number of milliseconds per animation frame.
*/
/// The number of milliseconds per animation frame.
static const _MS_PER_FRAME = 1000.0 / _FRAMES_PER_SECOND;
/**
* The constant factor applied to velocity at each frame to simulate
* deceleration.
*/
/// The constant factor applied to velocity at each frame to simulate
/// deceleration.
static const _DECELERATION_FACTOR = 0.97;
static const _MAX_VELOCITY_STATIC_FRICTION = 0.08 * _MS_PER_FRAME;
static const _DECELERATION_FACTOR_STATIC_FRICTION = 0.92;
/**
* Minimum velocity required to start or continue deceleration, in
* pixels/frame. This is equivalent to 0.25 px/ms.
*/
/// Minimum velocity required to start or continue deceleration, in
/// pixels/frame. This is equivalent to 0.25 px/ms.
static const _MIN_VELOCITY = 0.25 * _MS_PER_FRAME;
/**
* Minimum velocity during a step, in pixels/frame. This is equivalent to 0.01
* px/ms.
*/
/// Minimum velocity during a step, in pixels/frame. This is equivalent to 0.01
/// px/ms.
static const _MIN_STEP_VELOCITY = 0.01 * _MS_PER_FRAME;
/**
* Boost the initial velocity by a certain factor before applying momentum.
* This just gives the momentum a better feel.
*/
/// Boost the initial velocity by a certain factor before applying momentum.
/// This just gives the momentum a better feel.
static const _INITIAL_VELOCITY_BOOST_FACTOR = 1.25;
/**
* Additional deceleration factor to apply for the current move only. This
* is helpful for cases such as scroll wheel scrolling where the default
* amount of deceleration is inadequate.
*/
/// Additional deceleration factor to apply for the current move only. This
/// is helpful for cases such as scroll wheel scrolling where the default
/// amount of deceleration is inadequate.
num customDecelerationFactor = 1;
num _minCoord;
num _maxCoord;
/** The bouncing state. */
/// The bouncing state. */
int _bouncingState;
num velocity;
num _currentOffset;
/**
* constant used when guessing at the velocity required to throw to a specific
* location. Chosen arbitrarily. All that really matters is that the velocity
* is large enough that a throw gesture will occur.
*/
/// constant used when guessing at the velocity required to throw to a specific
/// location. Chosen arbitrarily. All that really matters is that the velocity
/// is large enough that a throw gesture will occur.
static const _VELOCITY_GUESS = 20;
SingleDimensionPhysics() : _bouncingState = BouncingState.NOT_BOUNCING {}
SingleDimensionPhysics() : _bouncingState = BouncingState.NOT_BOUNCING;
void configure(num minCoord, num maxCoord, num initialOffset,
num customDecelerationFactor_, num velocity_) {
@ -246,7 +212,7 @@ class SingleDimensionPhysics {
_minCoord = minCoord;
_maxCoord = maxCoord;
_currentOffset = initialOffset;
this.customDecelerationFactor = customDecelerationFactor_;
customDecelerationFactor = customDecelerationFactor_;
_adjustInitialVelocityAndBouncingState(velocity_);
}
@ -268,11 +234,9 @@ class SingleDimensionPhysics {
targetOffset > initialOffset ? _VELOCITY_GUESS : -_VELOCITY_GUESS);
}
/**
* Helper method to calculate initial velocity.
* The [velocity] passed here should be in terms of number of
* pixels / millisecond. Returns the adjusted x and y velocities.
*/
/// Helper method to calculate initial velocity.
/// The [velocity] passed here should be in terms of number of
/// pixels / millisecond. Returns the adjusted x and y velocities.
void _adjustInitialVelocityAndBouncingState(num v) {
velocity = v * _MS_PER_FRAME * _INITIAL_VELOCITY_BOOST_FACTOR;
@ -289,9 +253,7 @@ class SingleDimensionPhysics {
}
}
/**
* Apply deceleration.
*/
/// Apply deceleration.
void _adjustVelocity() {
num speed = velocity.abs();
velocity *= _DECELERATION_FACTOR;
@ -346,10 +308,8 @@ class SingleDimensionPhysics {
}
}
/**
* Whether or not the current velocity is above the threshold required to
* continue decelerating.
*/
/// Whether or not the current velocity is above the threshold required to
/// continue decelerating.
bool isVelocityAboveThreshold(num threshold) {
return velocity.abs() >= threshold;
}
@ -360,35 +320,31 @@ class SingleDimensionPhysics {
}
}
/**
* Implementation of a momentum strategy using webkit-transforms
* and timeouts.
*/
/// Implementation of a momentum strategy using webkit-transforms
/// and timeouts.
class TimeoutMomentum implements Momentum {
SingleDimensionPhysics physicsX;
SingleDimensionPhysics physicsY;
Coordinate _previousOffset;
Queue<_Move> _moves;
final Queue<_Move> _moves;
num _stepTimeout;
bool _decelerating;
MomentumDelegate _delegate;
final MomentumDelegate _delegate;
int _nextY;
int _nextX;
Coordinate _minCoord;
Coordinate _maxCoord;
num _customDecelerationFactor;
num _defaultDecelerationFactor;
final num _defaultDecelerationFactor;
TimeoutMomentum(this._delegate, [num defaultDecelerationFactor = 1])
: _defaultDecelerationFactor = defaultDecelerationFactor,
_decelerating = false,
_moves = new Queue<_Move>(),
physicsX = new SingleDimensionPhysics(),
physicsY = new SingleDimensionPhysics();
_moves = Queue<_Move>(),
physicsX = SingleDimensionPhysics(),
physicsY = SingleDimensionPhysics();
/**
* Calculate and return the moves for the deceleration motion.
*/
/// Calculate and return the moves for the deceleration motion.
void _calculateMoves() {
_moves.clear();
num time = TimeUtil.now();
@ -396,43 +352,43 @@ class TimeoutMomentum implements Momentum {
_stepWithoutAnimation();
time += SingleDimensionPhysics._MS_PER_FRAME;
if (_isStepNecessary()) {
_moves.add(new _Move(
_nextX, _nextY, physicsX.velocity, physicsY.velocity, time));
_moves.add(
_Move(_nextX, _nextY, physicsX.velocity, physicsY.velocity, time));
_previousOffset.y = _nextY;
_previousOffset.x = _nextX;
}
}
}
@override
bool get decelerating => _decelerating;
@override
num get decelerationFactor => _customDecelerationFactor;
/**
* Checks whether or not an animation step is necessary or not. Animations
* steps are not necessary when the velocity gets so low that in several
* frames the offset is the same.
* Returns true if there is movement to be done in the next frame.
*/
/// Checks whether or not an animation step is necessary or not. Animations
/// steps are not necessary when the velocity gets so low that in several
/// frames the offset is the same.
/// Returns true if there is movement to be done in the next frame.
bool _isStepNecessary() {
return _nextY != _previousOffset.y || _nextX != _previousOffset.x;
}
/**
* The [TouchHandler] requires this function but we don't need to do
* anything here.
*/
/// The [TouchHandler] requires this function but we don't need to do
/// anything here.
@override
void onTransitionEnd() {}
@override
Coordinate calculateVelocity(Coordinate start_, Coordinate target,
[num decelerationFactor = null]) {
return new Coordinate(
physicsX.solve(start_.x, target.x, decelerationFactor),
[num decelerationFactor]) {
return Coordinate(physicsX.solve(start_.x, target.x, decelerationFactor),
physicsY.solve(start_.y, target.y, decelerationFactor));
}
@override
bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
Coordinate initialOffset,
[num decelerationFactor = null]) {
[num decelerationFactor]) {
_customDecelerationFactor = _defaultDecelerationFactor;
if (decelerationFactor != null) {
_customDecelerationFactor = decelerationFactor;
@ -453,7 +409,7 @@ class TimeoutMomentum implements Momentum {
_customDecelerationFactor, velocity.y);
if (!physicsX.isDone() || !physicsY.isDone()) {
_calculateMoves();
if (!_moves.isEmpty) {
if (_moves.isNotEmpty) {
num firstTime = _moves.first.time;
_stepTimeout = Env.requestAnimationFrame(_step, null, firstTime);
_decelerating = true;
@ -464,12 +420,10 @@ class TimeoutMomentum implements Momentum {
return false;
}
/**
* Update the x, y values of the element offset without actually moving the
* element. This is done because we store decimal values for x, y for
* precision, but moving is only required when the offset is changed by at
* least a whole integer.
*/
/// Update the x, y values of the element offset without actually moving the
/// element. This is done because we store decimal values for x, y for
/// precision, but moving is only required when the offset is changed by at
/// least a whole integer.
void _stepWithoutAnimation() {
physicsX.step();
physicsY.step();
@ -477,25 +431,23 @@ class TimeoutMomentum implements Momentum {
_nextY = physicsY._currentOffset.round();
}
/**
* Calculate the next offset of the element and animate it to that position.
*/
/// Calculate the next offset of the element and animate it to that position.
void _step(num timestamp) {
_stepTimeout = null;
// Prune moves that are more than 1 frame behind when we have more
// available moves.
num lastEpoch = timestamp - SingleDimensionPhysics._MS_PER_FRAME;
while (!_moves.isEmpty &&
while (_moves.isNotEmpty &&
!identical(_moves.first, _moves.last) &&
_moves.first.time < lastEpoch) {
_moves.removeFirst();
}
if (!_moves.isEmpty) {
if (_moves.isNotEmpty) {
final move = _moves.removeFirst();
_delegate.onDecelerate(move.x, move.y);
if (!_moves.isEmpty) {
if (_moves.isNotEmpty) {
num nextTime = _moves.first.time;
assert(_stepTimeout == null);
_stepTimeout = Env.requestAnimationFrame(_step, null, nextTime);
@ -505,6 +457,7 @@ class TimeoutMomentum implements Momentum {
}
}
@override
void abort() {
_decelerating = false;
_moves.clear();
@ -514,20 +467,20 @@ class TimeoutMomentum implements Momentum {
}
}
@override
Coordinate stop() {
final wasDecelerating = _decelerating;
_decelerating = false;
Coordinate velocity;
if (!_moves.isEmpty) {
if (_moves.isNotEmpty) {
final move = _moves.first;
// This is a workaround for the ugly hacks that get applied when a user
// passed a velocity in to this Momentum implementation.
num velocityScale = SingleDimensionPhysics._MS_PER_FRAME *
SingleDimensionPhysics._INITIAL_VELOCITY_BOOST_FACTOR;
velocity =
new Coordinate(move.vx / velocityScale, move.vy / velocityScale);
velocity = Coordinate(move.vx / velocityScale, move.vy / velocityScale);
} else {
velocity = new Coordinate(0, 0);
velocity = Coordinate(0, 0);
}
_moves.clear();
if (_stepTimeout != null) {
@ -540,10 +493,11 @@ class TimeoutMomentum implements Momentum {
return velocity;
}
@override
Coordinate get destination {
if (!_moves.isEmpty) {
if (_moves.isNotEmpty) {
final lastMove = _moves.last;
return new Coordinate(lastMove.x, lastMove.y);
return Coordinate(lastMove.x, lastMove.y);
} else {
return null;
}

View file

@ -7,49 +7,41 @@
part of touch;
abstract class ScrollListener {
/**
* The callback invoked for a scroll event.
* [decelerating] specifies whether or not the content is moving due
* to deceleration. It should be false if the content is moving because the
* user is dragging the content.
*/
/// The callback invoked for a scroll event.
/// [decelerating] specifies whether or not the content is moving due
/// to deceleration. It should be false if the content is moving because the
/// user is dragging the content.
void onScrollerMoved(double scrollX, double scrollY, bool decelerating);
}
/**
* The scroll watcher is intended to provide a single way to
* listen for scroll events from instances of Scroller.
* TODO(jacobr): this class is obsolete.
*/
/// The scroll watcher is intended to provide a single way to
/// listen for scroll events from instances of Scroller.
/// TODO(jacobr): this class is obsolete.
class ScrollWatcher {
Scroller _scroller;
final Scroller _scroller;
List<ScrollListener> _listeners;
final List<ScrollListener> _listeners;
Element _scrollerEl;
ScrollWatcher(Scroller scroller)
: _scroller = scroller,
_listeners = new List<ScrollListener>() {}
_listeners = <ScrollListener>[];
void addListener(ScrollListener listener) {
_listeners.add(listener);
}
/**
* Send the scroll event to all listeners.
* [decelerating] is true if the offset is changing because of deceleration.
*/
/// Send the scroll event to all listeners.
/// [decelerating] is true if the offset is changing because of deceleration.
void _dispatchScroll(num scrollX, num scrollY, [bool decelerating = false]) {
for (final listener in _listeners) {
listener.onScrollerMoved(scrollX, scrollY, decelerating);
}
}
/**
* Initializes elements and event handlers. Must be called after construction
* and before usage.
*/
/// Initializes elements and event handlers. Must be called after construction
/// and before usage.
void initialize() {
_scrollerEl = _scroller.getElement();
_scroller.onContentMoved.listen((e) {
@ -57,9 +49,7 @@ class ScrollWatcher {
});
}
/**
* This callback is invoked any time the scroller content offset changes.
*/
/// This callback is invoked any time the scroller content offset changes.
void _onContentMoved(Event e) {
num scrollX = _scroller.getHorizontalOffset();
num scrollY = _scroller.getVerticalOffset();

View file

@ -6,45 +6,35 @@
part of touch;
/**
* Implementation of a scrollbar for the custom scrolling behavior
* defined in [:Scroller:].
*/
/// Implementation of a scrollbar for the custom scrolling behavior
/// defined in [:Scroller:].
class Scrollbar implements ScrollListener {
/**
* The minimum size of scrollbars when not compressed.
*/
/// The minimum size of scrollbars when not compressed.
static const _MIN_SIZE = 30;
/**
* The minimum compressed size of scrollbars. Scrollbars are compressed when
* the content is stretching past its boundaries.
*/
/// The minimum compressed size of scrollbars. Scrollbars are compressed when
/// the content is stretching past its boundaries.
static const _MIN_COMPRESSED_SIZE = 8;
/** Padding in pixels to add above and bellow the scrollbar. */
/// Padding in pixels to add above and bellow the scrollbar. */
static const _PADDING_LENGTH = 10;
/**
* The amount of time to wait before hiding scrollbars after showing them.
* Measured in ms.
*/
/// The amount of time to wait before hiding scrollbars after showing them.
/// Measured in ms.
static const _DISPLAY_TIME = 300;
static const DRAG_CLASS_NAME = 'drag';
Scroller _scroller;
Element _frame;
final Scroller _scroller;
final Element _frame;
bool _scrollInProgress = false;
bool _scrollBarDragInProgressValue = false;
/**
* Cached values of height and width. Keys will be 'height' and 'width'
* depending on if they are applied to vertical or horizontal scrollbar.
*/
Map<String, num> _cachedSize;
/// Cached values of height and width. Keys will be 'height' and 'width'
/// depending on if they are applied to vertical or horizontal scrollbar.
final Map<String, num> _cachedSize;
/**
* This bound function will be used as the input to window.setTimeout when
* scheduling the hiding of the scrollbars.
*/
/// This bound function will be used as the input to window.setTimeout when
/// scheduling the hiding of the scrollbars.
Function _boundHideFn;
Element _verticalElement;
@ -56,14 +46,14 @@ class Scrollbar implements ScrollListener {
num _currentScrollRatio;
Timer _timer;
bool _displayOnHover;
final bool _displayOnHover;
bool _hovering = false;
Scrollbar(Scroller scroller, [displayOnHover = true])
: _displayOnHover = displayOnHover,
_scroller = scroller,
_frame = scroller.getFrame(),
_cachedSize = new Map<String, num>() {
_cachedSize = <String, num>{} {
_boundHideFn = () {
_showScrollbars(false);
};
@ -71,7 +61,7 @@ class Scrollbar implements ScrollListener {
bool get _scrollBarDragInProgress => _scrollBarDragInProgressValue;
void set _scrollBarDragInProgress(bool value) {
set _scrollBarDragInProgress(bool value) {
_scrollBarDragInProgressValue = value;
_toggleClass(
_verticalElement, DRAG_CLASS_NAME, value && _currentScrollVertical);
@ -90,10 +80,8 @@ class Scrollbar implements ScrollListener {
}
}
/**
* Initializes elements and event handlers. Must be called after
* construction and before usage.
*/
/// Initializes elements and event handlers. Must be called after
/// construction and before usage.
void initialize() {
// Don't initialize if we have already been initialized.
// TODO(jacobr): remove this once bugs are fixed and enterDocument is only
@ -101,9 +89,9 @@ class Scrollbar implements ScrollListener {
if (_verticalElement != null) {
return;
}
_verticalElement = new Element.html(
_verticalElement = Element.html(
'<div class="touch-scrollbar touch-scrollbar-vertical"></div>');
_horizontalElement = new Element.html(
_horizontalElement = Element.html(
'<div class="touch-scrollbar touch-scrollbar-horizontal"></div>');
_scroller.addScrollListener(this);
@ -223,19 +211,17 @@ class Scrollbar implements ScrollListener {
void _onEnd(Event e) {
_scrollBarDragInProgress = false;
// TODO(jacobr): make scrollbar less tightly coupled to the scroller.
_scroller._onScrollerDragEnd.add(new Event(ScrollerEventType.DRAG_END));
_scroller._onScrollerDragEnd.add(Event(ScrollerEventType.DRAG_END));
}
/**
* When scrolling ends, schedule a timeout to hide the scrollbars.
*/
/// When scrolling ends, schedule a timeout to hide the scrollbars.
void _onScrollerEnd(Event e) {
_cancelTimeout();
_timer =
new Timer(const Duration(milliseconds: _DISPLAY_TIME), _boundHideFn);
_timer = Timer(const Duration(milliseconds: _DISPLAY_TIME), _boundHideFn);
_scrollInProgress = false;
}
@override
void onScrollerMoved(num scrollX, num scrollY, bool decelerating) {
if (_scrollInProgress == false) {
// Display the scrollbar and then immediately prepare to hide it...
@ -270,9 +256,7 @@ class Scrollbar implements ScrollListener {
}
}
/**
* When scrolling starts, show scrollbars and clear hide intervals.
*/
/// When scrolling starts, show scrollbars and clear hide intervals.
void _onScrollerStart(Event e) {
_scrollInProgress = true;
_cancelTimeout();
@ -286,9 +270,7 @@ class Scrollbar implements ScrollListener {
}
}
/**
* Show or hide the scrollbars by changing the opacity.
*/
/// Show or hide the scrollbars by changing the opacity.
void _showScrollbars(bool show) {
if (_hovering == true && _displayOnHover) {
show = true;
@ -312,12 +294,10 @@ class Scrollbar implements ScrollListener {
frameSize - _PADDING_LENGTH * 2);
}
/**
* Update the vertical or horizontal scrollbar based on the new scroll
* properties. The CSS property to adjust for position (bottom|right) is
* specified by [cssPos]. The CSS property to adjust for size (height|width)
* is specified by [cssSize].
*/
/// Update the vertical or horizontal scrollbar based on the new scroll
/// properties. The CSS property to adjust for position (bottom|right) is
/// specified by [cssPos]. The CSS property to adjust for size (height|width)
/// is specified by [cssSize].
void _updateScrollbar(Element element, num offset, num scrollPercent,
num frameSize, num contentSize, String cssPos, String cssSize) {
if (!_cachedSize.containsKey(cssSize)) {

View file

@ -6,35 +6,33 @@
part of touch;
/**
* Implementation of a custom scrolling behavior.
* This behavior overrides native scrolling for an area. This area can be a
* single defined part of a page, the entire page, or several different parts
* of a page.
*
* To use this scrolling behavior you need to define a frame and the content.
* The frame defines the area that the content will scroll within. The frame and
* content must both be HTML Elements, with the content being a direct child of
* the frame. Usually the frame is smaller in size than the content. This is
* not necessary though, if the content is smaller then bouncing will occur to
* provide feedback that you are past the scrollable area.
*
* The scrolling behavior works using the webkit translate3d transformation,
* which means browsers that do not have hardware accelerated transformations
* will not perform as well using this. Simple scrolling should be fine even
* without hardware acceleration, but animating momentum and deceleration is
* unacceptably slow without it. There is also the option to use relative
* positioning (setting the left and top styles).
*
* For this to work properly you need to set -webkit-text-size-adjust to 'none'
* on an ancestor element of the frame, or on the frame itself. If you forget
* this you may see the text content of the scrollable area changing size as it
* moves.
*
* The behavior is intended to support vertical and horizontal scrolling, and
* scrolling with momentum when a touch gesture flicks with enough velocity.
*/
typedef void Callback();
/// Implementation of a custom scrolling behavior.
/// This behavior overrides native scrolling for an area. This area can be a
/// single defined part of a page, the entire page, or several different parts
/// of a page.
///
/// To use this scrolling behavior you need to define a frame and the content.
/// The frame defines the area that the content will scroll within. The frame and
/// content must both be HTML Elements, with the content being a direct child of
/// the frame. Usually the frame is smaller in size than the content. This is
/// not necessary though, if the content is smaller then bouncing will occur to
/// provide feedback that you are past the scrollable area.
///
/// The scrolling behavior works using the webkit translate3d transformation,
/// which means browsers that do not have hardware accelerated transformations
/// will not perform as well using this. Simple scrolling should be fine even
/// without hardware acceleration, but animating momentum and deceleration is
/// unacceptably slow without it. There is also the option to use relative
/// positioning (setting the left and top styles).
///
/// For this to work properly you need to set -webkit-text-size-adjust to 'none'
/// on an ancestor element of the frame, or on the frame itself. If you forget
/// this you may see the text content of the scrollable area changing size as it
/// moves.
///
/// The behavior is intended to support vertical and horizontal scrolling, and
/// scrolling with momentum when a touch gesture flicks with enough velocity.
typedef Callback = void Function();
// Helper method to await the completion of 2 futures.
void joinFutures(List<Future> futures, Callback callback) {
@ -53,7 +51,7 @@ void joinFutures(List<Future> futures, Callback callback) {
}
class Scroller implements Draggable, MomentumDelegate {
/** Pixels to move each time an arrow key is pressed. */
/// Pixels to move each time an arrow key is pressed. */
static const ARROW_KEY_DELTA = 30;
static const SCROLL_WHEEL_VELOCITY = 0.01;
static const FAST_SNAP_DECELERATION_FACTOR = 0.84;
@ -62,16 +60,14 @@ class Scroller implements Draggable, MomentumDelegate {
// TODO(jacobr): remove this static variable.
static bool _dragInProgress = false;
/** The node that will actually scroll. */
Element _element;
/// The node that will actually scroll. */
final Element _element;
/**
* Frame is the node that will serve as the container for the scrolling
* content.
*/
Element _frame;
/// Frame is the node that will serve as the container for the scrolling
/// content.
final Element _frame;
/** Touch manager to track the events on the scrollable area. */
/// Touch manager to track the events on the scrollable area. */
TouchHandler _touchHandler;
Momentum _momentum;
@ -87,57 +83,48 @@ class Scroller implements Draggable, MomentumDelegate {
StreamController<Event> _onDecelStart;
Stream<Event> _onDecelStartStream;
/** Set if vertical scrolling should be enabled. */
/// Set if vertical scrolling should be enabled. */
@override
bool verticalEnabled;
/** Set if horizontal scrolling should be enabled. */
/// Set if horizontal scrolling should be enabled. */
@override
bool horizontalEnabled;
/**
* Set if momentum should be enabled.
*/
/// Set if momentum should be enabled.
bool _momentumEnabled;
/** Set which type of scrolling translation technique should be used. */
int _scrollTechnique;
/// Set which type of scrolling translation technique should be used. */
final int _scrollTechnique;
/**
* The maximum coordinate that the left upper corner of the content can scroll
* to.
*/
/// The maximum coordinate that the left upper corner of the content can scroll
/// to.
Coordinate _maxPoint;
/**
* An offset to subtract from the maximum coordinate that the left upper
* corner of the content can scroll to.
*/
Coordinate _maxOffset;
/// An offset to subtract from the maximum coordinate that the left upper
/// corner of the content can scroll to.
final Coordinate _maxOffset;
/**
* An offset to add to the minimum coordinate that the left upper corner of
* the content can scroll to.
*/
Coordinate _minOffset;
/// An offset to add to the minimum coordinate that the left upper corner of
/// the content can scroll to.
final Coordinate _minOffset;
/** Initialize the current content offset. */
Coordinate _contentOffset;
/// Initialize the current content offset. */
final Coordinate _contentOffset;
// TODO(jacobr): the function type is
// [:Function(Element, num, num)->void:].
/**
* The function to use that will actually translate the scrollable node.
*/
/// The function to use that will actually translate the scrollable node.
Function _setOffsetFunction;
/**
* Function that returns the content size that can be specified instead of
* querying the DOM.
*/
Function _lookupContentSizeDelegate;
/// Function that returns the content size that can be specified instead of
/// querying the DOM.
final Function _lookupContentSizeDelegate;
Size _scrollSize;
Size _contentSize;
Coordinate _minPoint;
bool _isStopping = false;
final bool _isStopping = false;
Coordinate _contentStartOffset;
bool _started = false;
bool _activeGesture = false;
@ -147,24 +134,23 @@ class Scroller implements Draggable, MomentumDelegate {
[this.verticalEnabled = false,
this.horizontalEnabled = false,
momentumEnabled = true,
lookupContentSizeDelegate = null,
lookupContentSizeDelegate,
num defaultDecelerationFactor = 1,
int scrollTechnique = null,
int scrollTechnique,
bool capture = false])
: _momentumEnabled = momentumEnabled,
_lookupContentSizeDelegate = lookupContentSizeDelegate,
_element = scrollableElem,
_frame = scrollableElem.parent,
_scrollTechnique = scrollTechnique != null
? scrollTechnique
: ScrollerScrollTechnique.TRANSFORM_3D,
_minPoint = new Coordinate(0, 0),
_maxPoint = new Coordinate(0, 0),
_maxOffset = new Coordinate(0, 0),
_minOffset = new Coordinate(0, 0),
_contentOffset = new Coordinate(0, 0) {
_touchHandler = new TouchHandler(this, scrollableElem.parent);
_momentum = new Momentum(this, defaultDecelerationFactor);
_scrollTechnique =
scrollTechnique ?? ScrollerScrollTechnique.TRANSFORM_3D,
_minPoint = Coordinate(0, 0),
_maxPoint = Coordinate(0, 0),
_maxOffset = Coordinate(0, 0),
_minOffset = Coordinate(0, 0),
_contentOffset = Coordinate(0, 0) {
_touchHandler = TouchHandler(this, scrollableElem.parent);
_momentum = Momentum(this, defaultDecelerationFactor);
Element parentElem = scrollableElem.parent;
assert(parentElem != null);
@ -246,7 +232,7 @@ class Scroller implements Draggable, MomentumDelegate {
Stream<Event> get onScrollerStart {
if (_onScrollerStart == null) {
_onScrollerStart = new StreamController<Event>.broadcast(sync: true);
_onScrollerStart = StreamController<Event>.broadcast(sync: true);
_onScrollerStartStream = _onScrollerStart.stream;
}
return _onScrollerStartStream;
@ -254,7 +240,7 @@ class Scroller implements Draggable, MomentumDelegate {
Stream<Event> get onScrollerEnd {
if (_onScrollerEnd == null) {
_onScrollerEnd = new StreamController<Event>.broadcast(sync: true);
_onScrollerEnd = StreamController<Event>.broadcast(sync: true);
_onScrollerEndStream = _onScrollerEnd.stream;
}
return _onScrollerEndStream;
@ -262,7 +248,7 @@ class Scroller implements Draggable, MomentumDelegate {
Stream<Event> get onScrollerDragEnd {
if (_onScrollerDragEnd == null) {
_onScrollerDragEnd = new StreamController<Event>.broadcast(sync: true);
_onScrollerDragEnd = StreamController<Event>.broadcast(sync: true);
_onScrollerDragEndStream = _onScrollerDragEnd.stream;
}
return _onScrollerDragEndStream;
@ -270,7 +256,7 @@ class Scroller implements Draggable, MomentumDelegate {
Stream<Event> get onContentMoved {
if (_onContentMoved == null) {
_onContentMoved = new StreamController<Event>.broadcast(sync: true);
_onContentMoved = StreamController<Event>.broadcast(sync: true);
_onContentMovedStream = _onContentMoved.stream;
}
return _onContentMovedStream;
@ -278,28 +264,24 @@ class Scroller implements Draggable, MomentumDelegate {
Stream<Event> get onDecelStart {
if (_onDecelStart == null) {
_onDecelStart = new StreamController<Event>.broadcast(sync: true);
_onDecelStart = StreamController<Event>.broadcast(sync: true);
_onDecelStartStream = _onDecelStart.stream;
}
return _onDecelStartStream;
}
/**
* Add a scroll listener. This allows other classes to subscribe to scroll
* notifications from this scroller.
*/
/// Add a scroll listener. This allows other classes to subscribe to scroll
/// notifications from this scroller.
void addScrollListener(ScrollListener listener) {
if (_scrollWatcher == null) {
_scrollWatcher = new ScrollWatcher(this);
_scrollWatcher = ScrollWatcher(this);
_scrollWatcher.initialize();
}
_scrollWatcher.addListener(listener);
}
/**
* Adjust the new calculated scroll position based on the minimum allowed
* position and returns the adjusted scroll value.
*/
/// Adjust the new calculated scroll position based on the minimum allowed
/// position and returns the adjusted scroll value.
num _adjustValue(num newPosition, num minPosition, num maxPosition) {
assert(minPosition <= maxPosition);
@ -313,32 +295,24 @@ class Scroller implements Draggable, MomentumDelegate {
return newPosition;
}
/**
* Coordinate we would end up at if we did nothing.
*/
/// Coordinate we would end up at if we did nothing.
Coordinate get currentTarget {
Coordinate end = _momentum.destination;
if (end == null) {
end = _contentOffset;
}
end ??= _contentOffset;
return end;
}
Coordinate get contentOffset => _contentOffset;
/**
* Animate the position of the scroller to the specified [x], [y] coordinates
* by applying the throw gesture with the correct velocity to end at that
* location.
*/
void throwTo(num x, num y, [num decelerationFactor = null]) {
/// Animate the position of the scroller to the specified [x], [y] coordinates
/// by applying the throw gesture with the correct velocity to end at that
/// location.
void throwTo(num x, num y, [num decelerationFactor]) {
reconfigure(() {
final snappedTarget = _snapToBounds(x, y);
// If a deceleration factor is not specified, use the existing
// deceleration factor specified by the momentum simulator.
if (decelerationFactor == null) {
decelerationFactor = _momentum.decelerationFactor;
}
decelerationFactor ??= _momentum.decelerationFactor;
if (snappedTarget != currentTarget) {
_momentum.abort();
@ -348,13 +322,13 @@ class Scroller implements Draggable, MomentumDelegate {
_contentOffset, snappedTarget, decelerationFactor),
decelerationFactor);
if (_onDecelStart != null) {
_onDecelStart.add(new Event(ScrollerEventType.DECEL_START));
_onDecelStart.add(Event(ScrollerEventType.DECEL_START));
}
}
});
}
void throwDelta(num deltaX, num deltaY, [num decelerationFactor = null]) {
void throwDelta(num deltaX, num deltaY, [num decelerationFactor]) {
Coordinate start = _contentOffset;
Coordinate end = currentTarget;
int x = end.x.toInt();
@ -380,28 +354,25 @@ class Scroller implements Draggable, MomentumDelegate {
_setContentOffset(_contentOffset.x, _contentOffset.y);
}
/**
* Adjusted content size is a size with the combined largest height and width
* of both the content and the frame.
*/
/// Adjusted content size is a size with the combined largest height and width
/// of both the content and the frame.
Size _getAdjustedContentSize() {
return new Size(Math.max(_scrollSize.width, _contentSize.width),
return Size(Math.max(_scrollSize.width, _contentSize.width),
Math.max(_scrollSize.height, _contentSize.height));
}
// TODO(jmesserly): these should be properties instead of get* methods
num getDefaultVerticalOffset() => _maxPoint.y;
@override
Element getElement() => _element;
Element getFrame() => _frame;
num getHorizontalOffset() => _contentOffset.x;
/**
* [x] Value to use as reference for percent measurement. If
* none is provided then the content's current x offset will be used.
* Returns the percent of the page scrolled horizontally.
*/
num getHorizontalScrollPercent([num x = null]) {
x = x != null ? x : _contentOffset.x;
/// [x] Value to use as reference for percent measurement. If
/// none is provided then the content's current x offset will be used.
/// Returns the percent of the page scrolled horizontally.
num getHorizontalScrollPercent([num x]) {
x = x ?? _contentOffset.x;
return (x - _minPoint.x) / (_maxPoint.x - _minPoint.x);
}
@ -409,25 +380,19 @@ class Scroller implements Draggable, MomentumDelegate {
num getMinPointY() => _minPoint.y;
Momentum get momentum => _momentum;
/**
* Provide access to the touch handler that the scroller created to manage
* touch events.
*/
/// Provide access to the touch handler that the scroller created to manage
/// touch events.
TouchHandler getTouchHandler() => _touchHandler;
num getVerticalOffset() => _contentOffset.y;
/**
* [y] value is used as reference for percent measurement. If
* none is provided then the content's current y offset will be used.
*/
num getVerticalScrollPercent([num y = null]) {
y = y != null ? y : _contentOffset.y;
/// [y] value is used as reference for percent measurement. If
/// none is provided then the content's current y offset will be used.
num getVerticalScrollPercent([num y]) {
y = y ?? _contentOffset.y;
return (y - _minPoint.y) / Math.max(1, _maxPoint.y - _minPoint.y);
}
/**
* Initialize the dom elements necessary for the scrolling to work.
*/
/// Initialize the dom elements necessary for the scrolling to work.
void _initLayer() {
// The scrollable node provided to Scroller must be a direct child
// of the scrollable frame.
@ -436,17 +401,20 @@ class Scroller implements Draggable, MomentumDelegate {
_setContentOffset(_maxPoint.x, _maxPoint.y);
}
@override
void onDecelerate(num x, num y) {
_setContentOffset(x, y);
}
@override
void onDecelerationEnd() {
if (_onScrollerEnd != null) {
_onScrollerEnd.add(new Event(ScrollerEventType.SCROLLER_END));
_onScrollerEnd.add(Event(ScrollerEventType.SCROLLER_END));
}
_started = false;
}
@override
void onDragEnd() {
_dragInProgress = false;
@ -458,23 +426,24 @@ class Scroller implements Draggable, MomentumDelegate {
}
if (_onScrollerDragEnd != null) {
_onScrollerDragEnd.add(new Event(ScrollerEventType.DRAG_END));
_onScrollerDragEnd.add(Event(ScrollerEventType.DRAG_END));
}
if (!decelerating) {
_snapContentOffsetToBounds();
if (_onScrollerEnd != null) {
_onScrollerEnd.add(new Event(ScrollerEventType.SCROLLER_END));
_onScrollerEnd.add(Event(ScrollerEventType.SCROLLER_END));
}
_started = false;
} else {
if (_onDecelStart != null) {
_onDecelStart.add(new Event(ScrollerEventType.DECEL_START));
_onDecelStart.add(Event(ScrollerEventType.DECEL_START));
}
}
_activeGesture = false;
}
@override
void onDragMove() {
if (_isStopping || (!_activeGesture && _dragInProgress)) {
return;
@ -497,12 +466,13 @@ class Scroller implements Draggable, MomentumDelegate {
if (!_started) {
_started = true;
if (_onScrollerStart != null) {
_onScrollerStart.add(new Event(ScrollerEventType.SCROLLER_START));
_onScrollerStart.add(Event(ScrollerEventType.SCROLLER_START));
}
}
_setContentOffset(newX, newY);
}
@override
bool onDragStart(TouchEvent e) {
if (e.touches.length > 1) {
return false;
@ -514,11 +484,11 @@ class Scroller implements Draggable, MomentumDelegate {
return !!(shouldVertical || shouldHorizontal && !verticalish);
}
@override
void onTouchEnd() {}
/**
* Prepare the scrollable area for possible movement.
*/
/// Prepare the scrollable area for possible movement.
@override
bool onTouchStart(TouchEvent e) {
reconfigure(() {
final touch = e.touches[0];
@ -533,12 +503,10 @@ class Scroller implements Draggable, MomentumDelegate {
return true;
}
/**
* Recalculate dimensions of the frame and the content. Adjust the minPoint
* and maxPoint allowed for scrolling and scroll to a valid position. Call
* this method if you know the frame or content has been updated. Called
* internally on every touchstart event the frame receives.
*/
/// Recalculate dimensions of the frame and the content. Adjust the minPoint
/// and maxPoint allowed for scrolling and scroll to a valid position. Call
/// this method if you know the frame or content has been updated. Called
/// internally on every touchstart event the frame receives.
void reconfigure(Callback callback) {
_resize(() {
_snapContentOffsetToBounds();
@ -556,22 +524,20 @@ class Scroller implements Draggable, MomentumDelegate {
reconfigure(() => _setContentOffset(_maxPoint.x, _maxPoint.y));
}
/**
* Recalculate dimensions of the frame and the content. Adjust the minPoint
* and maxPoint allowed for scrolling.
*/
/// Recalculate dimensions of the frame and the content. Adjust the minPoint
/// and maxPoint allowed for scrolling.
void _resize(Callback callback) {
scheduleMicrotask(() {
if (_lookupContentSizeDelegate != null) {
_contentSize = _lookupContentSizeDelegate();
} else {
_contentSize = new Size(_element.scrollWidth, _element.scrollHeight);
_contentSize = Size(_element.scrollWidth, _element.scrollHeight);
}
_scrollSize = new Size(_frame.offset.width, _frame.offset.height);
_scrollSize = Size(_frame.offset.width, _frame.offset.height);
Size adjusted = _getAdjustedContentSize();
_maxPoint = new Coordinate(-_maxOffset.x, -_maxOffset.y);
_minPoint = new Coordinate(
_maxPoint = Coordinate(-_maxOffset.x, -_maxOffset.y);
_minPoint = Coordinate(
Math.min(
_scrollSize.width - adjusted.width + _minOffset.x, _maxPoint.x),
Math.min(_scrollSize.height - adjusted.height + _minOffset.y,
@ -583,61 +549,49 @@ class Scroller implements Draggable, MomentumDelegate {
Coordinate _snapToBounds(num x, num y) {
num clampX = GoogleMath.clamp(_minPoint.x, x, _maxPoint.x);
num clampY = GoogleMath.clamp(_minPoint.y, y, _maxPoint.y);
return new Coordinate(clampX, clampY);
return Coordinate(clampX, clampY);
}
/**
* Translate the content to a new position specified in px.
*/
/// Translate the content to a new position specified in px.
void _setContentOffset(num x, num y) {
_contentOffset.x = x;
_contentOffset.y = y;
_setOffsetFunction(_element, x, y);
if (_onContentMoved != null) {
_onContentMoved.add(new Event(ScrollerEventType.CONTENT_MOVED));
_onContentMoved.add(Event(ScrollerEventType.CONTENT_MOVED));
}
}
/**
* Enable or disable momentum.
*/
/// Enable or disable momentum.
void setMomentum(bool enable) {
_momentumEnabled = enable;
}
/**
* Sets the vertical scrolled offset of the element where [y] is the amount
* of vertical space to be scrolled, in pixels.
*/
/// Sets the vertical scrolled offset of the element where [y] is the amount
/// of vertical space to be scrolled, in pixels.
void setVerticalOffset(num y) {
_setContentOffset(_contentOffset.x, y);
}
/**
* Whether the scrollable area should scroll horizontally. Only
* returns true if the client has enabled horizontal scrolling, and the
* content is wider than the frame.
*/
/// Whether the scrollable area should scroll horizontally. Only
/// returns true if the client has enabled horizontal scrolling, and the
/// content is wider than the frame.
bool _shouldScrollHorizontally() {
return horizontalEnabled && _scrollSize.width < _contentSize.width;
}
/**
* Whether the scrollable area should scroll vertically. Only
* returns true if the client has enabled vertical scrolling.
* Vertical bouncing will occur even if frame is taller than content, because
* this is what iPhone web apps tend to do. If this is not the desired
* behavior, either disable vertical scrolling for this scroller or add a
* 'bouncing' parameter to this interface.
*/
/// Whether the scrollable area should scroll vertically. Only
/// returns true if the client has enabled vertical scrolling.
/// Vertical bouncing will occur even if frame is taller than content, because
/// this is what iPhone web apps tend to do. If this is not the desired
/// behavior, either disable vertical scrolling for this scroller or add a
/// 'bouncing' parameter to this interface.
bool _shouldScrollVertically() {
return verticalEnabled;
}
/**
* In the event that the content is currently beyond the bounds of
* the frame, snap it back in to place.
*/
/// In the event that the content is currently beyond the bounds of
/// the frame, snap it back in to place.
void _snapContentOffsetToBounds() {
num clampX = GoogleMath.clamp(_minPoint.x, _contentOffset.x, _maxPoint.x);
num clampY = GoogleMath.clamp(_minPoint.y, _contentOffset.y, _maxPoint.y);
@ -646,12 +600,9 @@ class Scroller implements Draggable, MomentumDelegate {
}
}
/**
* Initiate the deceleration behavior given a flick [velocity].
* Returns true if deceleration has been initiated.
*/
bool _startDeceleration(Coordinate velocity,
[num decelerationFactor = null]) {
/// Initiate the deceleration behavior given a flick [velocity].
/// Returns true if deceleration has been initiated.
bool _startDeceleration(Coordinate velocity, [num decelerationFactor]) {
if (!_shouldScrollHorizontally()) {
velocity.x = 0;
}
@ -668,9 +619,7 @@ class Scroller implements Draggable, MomentumDelegate {
return _momentum.stop();
}
/**
* Stop the deceleration of the scrollable content given a new position in px.
*/
/// Stop the deceleration of the scrollable content given a new position in px.
void _stopDecelerating(num x, num y) {
_momentum.stop();
_setContentOffset(x, y);

View file

@ -6,13 +6,11 @@
part of touch;
/**
* Convenience methods for dealing with time.
* In the future this could also provide an entry point to mock out time
* calculation for tests.
*/
/// Convenience methods for dealing with time.
/// In the future this could also provide an entry point to mock out time
/// calculation for tests.
class TimeUtil {
static int now() {
return new DateTime.now().millisecondsSinceEpoch;
return DateTime.now().millisecondsSinceEpoch;
}
}

View file

@ -6,98 +6,84 @@
part of touch;
/**
* Touch Handler. Class that handles all touch events and
* uses them to interpret higher level gestures and behaviors. TouchEvent is a
* built in mobile safari type:
* [http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html].
*
* Examples of higher level gestures this class is intended to support
* - click, double click, long click
* - dragging, swiping, zooming
*
* Touch Behavior:
* Use this class to make your elements 'touchable' (see Touchable.dart).
* Intended to work with all webkit browsers.
*
* Drag Behavior:
* Use this class to make your elements 'draggable' (see draggable.js).
* This behavior will handle all of the required events and report the
* properties of the drag to you while the touch is happening and at the
* end of the drag sequence. This behavior will NOT perform the actual
* dragging (redrawing the element) for you, this responsibility is left to
* the client code. This behavior contains a work around for a mobile
* safari bug where the 'touchend' event is not dispatched when the touch
* goes past the bottom of the browser window.
* This is intended to work well in iframes.
* Intended to work with all webkit browsers, tested only on iPhone 3.x so
* far.
*
* Click Behavior:
* Not yet implemented.
*
* Zoom Behavior:
* Not yet implemented.
*
* Swipe Behavior:
* Not yet implemented.
*/
/// Touch Handler. Class that handles all touch events and
/// uses them to interpret higher level gestures and behaviors. TouchEvent is a
/// built in mobile safari type:
/// [http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html].
///
/// Examples of higher level gestures this class is intended to support
/// - click, double click, long click
/// - dragging, swiping, zooming
///
/// Touch Behavior:
/// Use this class to make your elements 'touchable' (see Touchable.dart).
/// Intended to work with all webkit browsers.
///
/// Drag Behavior:
/// Use this class to make your elements 'draggable' (see draggable.js).
/// This behavior will handle all of the required events and report the
/// properties of the drag to you while the touch is happening and at the
/// end of the drag sequence. This behavior will NOT perform the actual
/// dragging (redrawing the element) for you, this responsibility is left to
/// the client code. This behavior contains a work around for a mobile
/// safari bug where the 'touchend' event is not dispatched when the touch
/// goes past the bottom of the browser window.
/// This is intended to work well in iframes.
/// Intended to work with all webkit browsers, tested only on iPhone 3.x so
/// far.
///
/// Click Behavior:
/// Not yet implemented.
///
/// Zoom Behavior:
/// Not yet implemented.
///
/// Swipe Behavior:
/// Not yet implemented.
class TouchHandler {
Touchable _touchable;
final Touchable _touchable;
Element _element;
/** The absolute sum of all touch y deltas. */
/// The absolute sum of all touch y deltas. */
int _totalMoveY;
/** The absolute sum of all touch x deltas. */
/// The absolute sum of all touch x deltas. */
int _totalMoveX;
/**
* A list of tuples where the first item is the horizontal component of a
* recent relevant touch and the second item is the touch's time stamp. Old
* touches are removed based on the max tracking time and when direction
* changes.
*/
/// A list of tuples where the first item is the horizontal component of a
/// recent relevant touch and the second item is the touch's time stamp. Old
/// touches are removed based on the max tracking time and when direction
/// changes.
List<int> _recentTouchesX;
/**
* A list of tuples where the first item is the vertical component of a
* recent relevant touch and the second item is the touch's time stamp. Old
* touches are removed based on the max tracking time and when direction
* changes.
*/
/// A list of tuples where the first item is the vertical component of a
/// recent relevant touch and the second item is the touch's time stamp. Old
/// touches are removed based on the max tracking time and when direction
/// changes.
List<int> _recentTouchesY;
// TODO(jacobr): make customizable by passing optional parameters to the
// TouchHandler constructor.
/**
* Minimum movement of touch required to be considered a drag.
*/
/// Minimum movement of touch required to be considered a drag.
static const _MIN_TRACKING_FOR_DRAG = 2;
/**
* The maximum number of ms to track a touch event. After an event is older
* than this value, it will be ignored in velocity calculations.
*/
/// The maximum number of ms to track a touch event. After an event is older
/// than this value, it will be ignored in velocity calculations.
static const _MAX_TRACKING_TIME = 250;
/** The maximum number of touches to track. */
/// The maximum number of touches to track. */
static const _MAX_TRACKING_TOUCHES = 5;
/**
* The maximum velocity to return, in pixels per millisecond, that is used to
* guard against errors in calculating end velocity of a drag. This is a very
* fast drag velocity.
*/
/// The maximum velocity to return, in pixels per millisecond, that is used to
/// guard against errors in calculating end velocity of a drag. This is a very
/// fast drag velocity.
static const _MAXIMUM_VELOCITY = 5;
/**
* The velocity to return, in pixel per millisecond, when the time stamps on
* the events are erroneous. The browser can return bad time stamps if the
* thread is blocked for the duration of the drag. This is a low velocity to
* prevent the content from moving quickly after a slow drag. It is less
* jarring if the content moves slowly after a fast drag.
*/
/// The velocity to return, in pixel per millisecond, when the time stamps on
/// the events are erroneous. The browser can return bad time stamps if the
/// thread is blocked for the duration of the drag. This is a low velocity to
/// prevent the content from moving quickly after a slow drag. It is less
/// jarring if the content moves slowly after a fast drag.
static const _VELOCITY_FOR_INCORRECT_EVENTS = 1;
Draggable _draggable;
@ -116,30 +102,26 @@ class TouchHandler {
int _endTouchX;
int _endTouchY;
TouchHandler(Touchable touchable, [Element element = null])
TouchHandler(Touchable touchable, [Element element])
: _touchable = touchable,
_totalMoveY = 0,
_totalMoveX = 0,
_recentTouchesX = new List<int>(),
_recentTouchesY = new List<int>(),
_recentTouchesX = <int>[],
_recentTouchesY = <int>[],
// TODO(jmesserly): I don't like having to initialize all booleans here
// See b/5045736
_dragging = false,
_tracking = false,
_touching = false {
_element = element != null ? element : touchable.getElement();
_element = element ?? touchable.getElement();
}
/**
* Begin tracking the touchable element, it is eligible for dragging.
*/
/// Begin tracking the touchable element, it is eligible for dragging.
void _beginTracking() {
_tracking = true;
}
/**
* Stop tracking the touchable element, it is no longer dragging.
*/
/// Stop tracking the touchable element, it is no longer dragging.
void _endTracking() {
_tracking = false;
_dragging = false;
@ -147,11 +129,9 @@ class TouchHandler {
_totalMoveX = 0;
}
/**
* Correct erroneous velocities by capping the velocity if we think it's too
* high, or setting it to a default velocity if know that the event data is
* bad. Returns the corrected velocity.
*/
/// Correct erroneous velocities by capping the velocity if we think it's too
/// high, or setting it to a default velocity if know that the event data is
/// bad. Returns the corrected velocity.
num _correctVelocity(num velocity) {
num absVelocity = velocity.abs();
if (absVelocity > _MAXIMUM_VELOCITY) {
@ -162,15 +142,14 @@ class TouchHandler {
return absVelocity * (velocity < 0 ? -1 : 1);
}
/**
* Start listenting for events.
* If [capture] is True the TouchHandler should listen during the capture
* phase.
*/
/// Start listenting for events.
/// If [capture] is True the TouchHandler should listen during the capture
/// phase.
void enable([bool capture = false]) {
Function onEnd = (e) {
onEnd(e) {
_onEnd(e.timeStamp.toInt(), e);
};
}
_addEventListeners(_element, (e) {
_onStart(e);
}, (e) {
@ -178,67 +157,55 @@ class TouchHandler {
}, onEnd, onEnd, capture);
}
/**
* Get the current horizontal drag delta. Drag delta is defined as the deltaX
* of the start touch position and the last touch position.
*/
/// Get the current horizontal drag delta. Drag delta is defined as the deltaX
/// of the start touch position and the last touch position.
int getDragDeltaX() {
return _lastTouchX - _startTouchX;
}
/**
* Get the current vertical drag delta. Drag delta is defined as the deltaY of
* the start touch position and the last touch position.
*/
/// Get the current vertical drag delta. Drag delta is defined as the deltaY of
/// the start touch position and the last touch position.
int getDragDeltaY() {
return _lastTouchY - _startTouchY;
}
/**
* Get end velocity of the drag. This method is specific to drag behavior, so
* if touch behavior and drag behavior is split then this should go with drag
* behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is
* the difference between endPosition and the oldest recent position, and
* deltaTime is the difference between endTime and the oldest recent time
* stamp.
*/
/// Get end velocity of the drag. This method is specific to drag behavior, so
/// if touch behavior and drag behavior is split then this should go with drag
/// behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is
/// the difference between endPosition and the oldest recent position, and
/// deltaTime is the difference between endTime and the oldest recent time
/// stamp.
Coordinate getEndVelocity() {
num velocityX = 0;
num velocityY = 0;
if (_recentTouchesX.length > 0) {
if (_recentTouchesX.isNotEmpty) {
num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]);
velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX;
}
if (_recentTouchesY.length > 0) {
if (_recentTouchesY.isNotEmpty) {
num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]);
velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY;
}
velocityX = _correctVelocity(velocityX);
velocityY = _correctVelocity(velocityY);
return new Coordinate(velocityX, velocityY);
return Coordinate(velocityX, velocityY);
}
/**
* Return the touch of the last event.
*/
/// Return the touch of the last event.
Touch _getLastTouch() {
assert(_lastEvent != null); // Last event not set
return _lastEvent.touches[0];
}
/**
* Is the touch manager currently tracking touch moves to detect a drag?
*/
/// Is the touch manager currently tracking touch moves to detect a drag?
bool isTracking() {
return _tracking;
}
/**
* Touch end handler.
*/
void _onEnd(int timeStamp, [TouchEvent e = null]) {
/// Touch end handler.
void _onEnd(int timeStamp, [TouchEvent e]) {
_touching = false;
_touchable.onTouchEnd();
if (!_tracking || _draggable == null) {
@ -262,9 +229,7 @@ class TouchHandler {
_endTracking();
}
/**
* Touch move handler.
*/
/// Touch move handler.
void _onMove(TouchEvent e) {
if (!_tracking || _draggable == null) {
return;
@ -311,9 +276,7 @@ class TouchHandler {
_lastMoveY = moveY;
}
/**
* Touch start handler.
*/
/// Touch start handler.
void _onStart(TouchEvent e) {
if (_touching) {
return;
@ -328,8 +291,8 @@ class TouchHandler {
int timeStamp = e.timeStamp.toInt();
_startTime = timeStamp;
// TODO(jacobr): why don't we just clear the lists?
_recentTouchesX = new List<int>();
_recentTouchesY = new List<int>();
_recentTouchesX = <int>[];
_recentTouchesY = <int>[];
_recentTouchesX.add(touch.client.x);
_recentTouchesX.add(timeStamp);
_recentTouchesY.add(touch.client.y);
@ -338,13 +301,11 @@ class TouchHandler {
_beginTracking();
}
/**
* Filters the provided recent touches list to remove all touches older than
* the max tracking time or the 5th most recent touch.
* [recentTouches] specifies a list of tuples where the first item is the x
* or y component of the recent touch and the second item is the touch time
* stamp. The time of the most recent event is specified by [recentTime].
*/
/// Filters the provided recent touches list to remove all touches older than
/// the max tracking time or the 5th most recent touch.
/// [recentTouches] specifies a list of tuples where the first item is the x
/// or y component of the recent touch and the second item is the touch time
/// stamp. The time of the most recent event is specified by [recentTime].
List<int> _removeOldTouches(List<int> recentTouches, int recentTime) {
int count = 0;
final len = recentTouches.length;
@ -361,14 +322,12 @@ class TouchHandler {
return list.sublist(n);
}
/**
* Filters the provided recent touches list to remove all touches except the
* last if the move direction has changed.
* [recentTouches] specifies a list of tuples where the first item is the x
* or y component of the recent touch and the second item is the touch time
* stamp. The x or y component of the most recent move is specified by
* [recentMove].
*/
/// Filters the provided recent touches list to remove all touches except the
/// last if the move direction has changed.
/// [recentTouches] specifies a list of tuples where the first item is the x
/// or y component of the recent touch and the second item is the touch time
/// stamp. The x or y component of the most recent move is specified by
/// [recentMove].
List<int> _removeTouchesInWrongDirection(
List<int> recentTouches, int lastMove, int recentMove) {
if (lastMove != 0 &&
@ -383,19 +342,15 @@ class TouchHandler {
// TODO(jacobr): why doesn't bool implement the xor operator directly?
static bool _xor(bool a, bool b) => a != b;
/**
* Reset the touchable element.
*/
/// Reset the touchable element.
void reset() {
_endTracking();
_touching = false;
}
/**
* Call this method to enable drag behavior on a draggable delegate.
* The [draggable] object can be the same as the [_touchable] object, they are
* assigned to different members to allow for strong typing with interfaces.
*/
/// Call this method to enable drag behavior on a draggable delegate.
/// The [draggable] object can be the same as the [_touchable] object, they are
/// assigned to different members to allow for strong typing with interfaces.
void setDraggable(Draggable draggable) {
_draggable = draggable;
}

View file

@ -6,17 +6,15 @@
part of touch;
/**
* Wraps a callback with translations of mouse events to touch events. Use
* this function to invoke your callback that expects touch events after
* touch events are created from the actual mouse events.
*/
/// Wraps a callback with translations of mouse events to touch events. Use
/// this function to invoke your callback that expects touch events after
/// touch events are created from the actual mouse events.
EventListener mouseToTouchCallback(EventListener callback) {
return (Event e) {
var touches = <Touch>[];
var targetTouches = <Touch>[];
var changedTouches = <Touch>[];
final mockTouch = new MockTouch(e);
final mockTouch = MockTouch(e);
final mockTouchList = <Touch>[mockTouch];
if (e.type == 'mouseup') {
changedTouches = mockTouchList;
@ -24,14 +22,14 @@ EventListener mouseToTouchCallback(EventListener callback) {
touches = mockTouchList;
targetTouches = mockTouchList;
}
callback(new MockTouchEvent(e, touches, targetTouches, changedTouches));
callback(MockTouchEvent(e, touches, targetTouches, changedTouches));
// Required to prevent spurious selection changes while tracking touches
// on devices that don't support touch events.
e.preventDefault();
};
}
/** Helper method to attach event listeners to a [node]. */
/// Helper method to attach event listeners to a [node]. */
void _addEventListeners(Element node, EventListener onStart,
EventListener onMove, EventListener onEnd, EventListener onCancel,
[bool capture = false]) {
@ -53,10 +51,10 @@ void _addEventListeners(Element node, EventListener onStart,
}
if (Device.supportsTouch) {
var touchMoveSub;
var touchEndSub;
var touchLeaveSub;
var touchCancelSub;
StreamSubscription<TouchEvent> touchMoveSub;
StreamSubscription<TouchEvent> touchEndSub;
StreamSubscription<TouchEvent> touchLeaveSub;
StreamSubscription<TouchEvent> touchCancelSub;
removeListeners = () {
touchMoveSub.cancel();
@ -86,9 +84,9 @@ void _addEventListeners(Element node, EventListener onStart,
onEnd = mouseToTouchCallback(onEnd);
// onLeave will never be called if the device does not support touches.
var mouseMoveSub;
var mouseUpSub;
var touchCancelSub;
StreamSubscription<MouseEvent> mouseMoveSub;
StreamSubscription<MouseEvent> mouseUpSub;
StreamSubscription<TouchEvent> touchCancelSub;
removeListeners = () {
mouseMoveSub.cancel();
@ -111,10 +109,8 @@ void _addEventListeners(Element node, EventListener onStart,
}
}
/**
* Gets whether the given touch event targets the node, or one of the node's
* children.
*/
/// Gets whether the given touch event targets the node, or one of the node's
/// children.
bool _touchEventTargetsNode(event, Node node) {
Node target = event.changedTouches[0].target;
@ -131,40 +127,28 @@ bool _touchEventTargetsNode(event, Node node) {
}
abstract class Touchable {
/**
* Provide the HTML element that should respond to touch events.
*/
/// Provide the HTML element that should respond to touch events.
Element getElement();
/**
* The object has received a touchend event.
*/
/// The object has received a touchend event.
void onTouchEnd();
/**
* The object has received a touchstart event.
* Returns return true if you want to allow a drag sequence to begin,
* false you want to disable dragging for the duration of this touch.
*/
/// The object has received a touchstart event.
/// Returns return true if you want to allow a drag sequence to begin,
/// false you want to disable dragging for the duration of this touch.
bool onTouchStart(TouchEvent e);
}
abstract class Draggable implements Touchable {
/**
* The object's drag sequence is now complete.
*/
/// The object's drag sequence is now complete.
void onDragEnd();
/**
* The object has been dragged to a new position.
*/
/// The object has been dragged to a new position.
void onDragMove();
/**
* The object has started dragging.
* Returns true to allow a drag sequence to begin (custom behavior),
* false to disable dragging for this touch duration (allow native scrolling).
*/
/// The object has started dragging.
/// Returns true to allow a drag sequence to begin (custom behavior),
/// false to disable dragging for this touch duration (allow native scrolling).
bool onDragStart(TouchEvent e);
bool get verticalEnabled;
@ -174,14 +158,16 @@ abstract class Draggable implements Touchable {
class MockTouch implements Touch {
MouseEvent wrapped;
MockTouch(MouseEvent this.wrapped) {}
MockTouch(this.wrapped);
int get clientX => wrapped.client.x;
int get clientY => wrapped.client.y;
@override
get client => wrapped.client;
@override
int get identifier => 0;
int get pageX => wrapped.page.x;
@ -194,50 +180,58 @@ class MockTouch implements Touch {
return wrapped.screen.y;
}
@override
EventTarget get target => wrapped.target;
@override
double get force {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
Point get page {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
int get radiusX {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
int get radiusY {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
String get region {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
num get rotationAngle {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
Point get screen {
throw new UnimplementedError();
throw UnimplementedError();
}
num get webkitForce {
throw new UnimplementedError();
throw UnimplementedError();
}
int get webkitRadiusX {
throw new UnimplementedError();
throw UnimplementedError();
}
int get webkitRadiusY {
throw new UnimplementedError();
throw UnimplementedError();
}
num get webkitRotationAngle {
throw new UnimplementedError();
throw UnimplementedError();
}
}
@ -250,75 +244,95 @@ class MockTouchList extends Object
static bool get supported => true;
@override
int get length => values.length;
@override
Touch operator [](int index) => values[index];
@override
void operator []=(int index, Touch value) {
throw new UnsupportedError("Cannot assign element of immutable List.");
throw UnsupportedError("Cannot assign element of immutable List.");
}
@override
set length(int value) {
throw new UnsupportedError("Cannot resize immutable List.");
throw UnsupportedError("Cannot resize immutable List.");
}
@override
Touch item(int index) => values[index];
}
class MockTouchEvent implements TouchEvent {
dynamic /*MouseEvent*/ wrapped;
@override
final TouchList touches;
@override
final TouchList targetTouches;
@override
final TouchList changedTouches;
MockTouchEvent(MouseEvent this.wrapped, List<Touch> touches,
List<Touch> targetTouches, List<Touch> changedTouches)
: touches = new MockTouchList(touches),
targetTouches = new MockTouchList(targetTouches),
changedTouches = new MockTouchList(changedTouches);
: touches = MockTouchList(touches),
targetTouches = MockTouchList(targetTouches),
changedTouches = MockTouchList(changedTouches);
@override
bool get bubbles => wrapped.bubbles;
bool get cancelBubble => wrapped.cancelBubble;
void set cancelBubble(bool value) {
set cancelBubble(bool value) {
wrapped.cancelBubble = value;
}
@override
bool get cancelable => wrapped.cancelable;
@override
EventTarget get currentTarget => wrapped.currentTarget;
@override
bool get defaultPrevented => wrapped.defaultPrevented;
@override
int get eventPhase => wrapped.eventPhase;
void set returnValue(bool value) {
set returnValue(bool value) {
wrapped.returnValue = value;
}
bool get returnValue => wrapped.returnValue;
@override
EventTarget get target => wrapped.target;
/*At different times, int, double, and String*/
@override
get timeStamp => wrapped.timeStamp;
@override
String get type => wrapped.type;
@override
void preventDefault() {
wrapped.preventDefault();
}
@override
void stopImmediatePropagation() {
wrapped.stopImmediatePropagation();
}
@override
void stopPropagation() {
wrapped.stopPropagation();
}
int get charCode => wrapped.charCode;
@override
int get detail => wrapped.detail;
// TODO(sra): keyCode is not on MouseEvent.
@ -332,67 +346,78 @@ class MockTouchEvent implements TouchEvent {
int get pageY => wrapped.page.y;
@override
Window get view => wrapped.view;
int get which => wrapped.which;
@override
bool get altKey => wrapped.altKey;
@override
bool get ctrlKey => wrapped.ctrlKey;
@override
bool get metaKey => wrapped.metaKey;
@override
bool get shiftKey => wrapped.shiftKey;
DataTransfer get clipboardData {
throw new UnimplementedError();
throw UnimplementedError();
}
List<EventTarget> deepPath() {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
bool get isTrusted {
throw new UnimplementedError();
throw UnimplementedError();
}
Point get layer {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
Element get matchingTarget {
throw new UnimplementedError();
throw UnimplementedError();
}
Point get page {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
List<EventTarget> get path {
throw new UnimplementedError();
throw UnimplementedError();
}
bool get scoped {
throw new UnimplementedError();
throw UnimplementedError();
}
Point get screen {
throw new UnimplementedError();
throw UnimplementedError();
}
/*InputDeviceCapabilities*/ get sourceCapabilities {
throw new UnimplementedError();
/*InputDeviceCapabilities*/ @override
get sourceCapabilities {
throw UnimplementedError();
}
/*InputDevice*/ get sourceDevice {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
bool get composed {
throw new UnimplementedError();
throw UnimplementedError();
}
@override
List<EventTarget> composedPath() {
throw new UnimplementedError();
throw UnimplementedError();
}
}

View file

@ -4,12 +4,10 @@
part of utilslib;
typedef int NumericValueSelector<T>(T value);
typedef NumericValueSelector<T> = int Function(T value);
/**
* General purpose collection utilities.
* TODO(jmesserly): make these top level functions?
*/
/// General purpose collection utilities.
/// TODO(jmesserly): make these top level functions?
class CollectionUtils {
static void insertAt(List arr, int pos, value) {
assert(pos >= 0);
@ -39,12 +37,10 @@ class CollectionUtils {
}
}
/**
* Finds the item in [source] that matches [test]. Returns null if
* no item matches. The typing should be:
* T find(Iterable<T> source, bool test(T item)), but we don't have generic
* functions.
*/
/// Finds the item in [source] that matches [test]. Returns null if
/// no item matches. The typing should be:
/// T find(Iterable<T> source, bool test(T item)), but we don't have generic
/// functions.
static find(Iterable source, bool test(item)) {
for (final item in source) {
if (test(item)) return item;
@ -53,7 +49,7 @@ class CollectionUtils {
return null;
}
/** Compute the minimum of an iterable. Returns null if empty. */
/// Compute the minimum of an iterable. Returns null if empty. */
static num? min(Iterable source) {
final iter = source.iterator;
if (!iter.moveNext()) {
@ -66,7 +62,7 @@ class CollectionUtils {
return best;
}
/** Compute the maximum of an iterable. Returns null if empty. */
/// Compute the maximum of an iterable. Returns null if empty. */
static num? max(Iterable source) {
final iter = source.iterator;
if (!iter.moveNext()) {
@ -79,19 +75,19 @@ class CollectionUtils {
return best;
}
/** Orders an iterable by its values, or by a key selector. */
/// Orders an iterable by its values, or by a key selector. */
static List<T> orderBy<T>(Iterable<T> source,
[NumericValueSelector? selector = null]) {
[NumericValueSelector? selector]) {
final result = List<T>.from(source);
sortBy(result, selector);
return result;
}
/** Sorts a list by its values, or by a key selector. */
/// Sorts a list by its values, or by a key selector. */
// TODO(jmesserly): we probably don't want to call the key selector more than
// once for a given element. This would improve performance and the API
// contract could be stronger.
static void sortBy(List list, [NumericValueSelector? selector = null]) {
static void sortBy(List list, [NumericValueSelector? selector]) {
if (selector != null) {
list.sort((x, y) => selector(x) - selector(y));
} else {
@ -99,8 +95,8 @@ class CollectionUtils {
}
}
/** Compute the sum of an iterable. An empty iterable is an error. */
static num sum(Iterable source, [NumericValueSelector? selector = null]) {
/// Compute the sum of an iterable. An empty iterable is an error. */
static num sum(Iterable source, [NumericValueSelector? selector]) {
final iter = source.iterator;
bool wasEmpty = true;
num total = 0;

View file

@ -4,12 +4,10 @@
part of utilslib;
/**
* General purpose date/time utilities.
*/
/// General purpose date/time utilities.
class DateUtils {
// TODO(jmesserly): localized strings
static const WEEKDAYS = const [
static const WEEKDAYS = [
'Monday',
'Tuesday',
'Wednesday',
@ -83,7 +81,7 @@ class DateUtils {
return result.toLocal();
}
/** Parse a string like: 2011-07-19T22:03:04.000Z */
/// Parse a string like: 2011-07-19T22:03:04.000Z */
// TODO(jmesserly): workaround for DateTime.fromDate, which has issues:
// * on Dart VM it doesn't handle all of ISO 8601. See b/5055106.
// * on DartC it doesn't work on Safari. See b/5062557.
@ -110,7 +108,7 @@ class DateUtils {
ensure(time.length == 3);
final seconds = time[2].split('.');
ensure(seconds.length >= 1 && seconds.length <= 2);
ensure(seconds.isNotEmpty && seconds.length <= 2);
int milliseconds = 0;
if (seconds.length == 2) {
milliseconds = int.parse(seconds[1]);
@ -126,13 +124,11 @@ class DateUtils {
milliseconds);
}
/**
* A date/time formatter that takes into account the current date/time:
* - if it's from today, just show the time
* - if it's from yesterday, just show 'Yesterday'
* - if it's from the same week, just show the weekday
* - otherwise, show just the date
*/
/// A date/time formatter that takes into account the current date/time:
/// - if it's from today, just show the time
/// - if it's from yesterday, just show 'Yesterday'
/// - if it's from the same week, just show the weekday
/// - otherwise, show just the date
static String toRecentTimeString(DateTime then) {
bool datesAreEqual(DateTime d1, DateTime d2) {
return (d1.year == d2.year) &&
@ -159,13 +155,13 @@ class DateUtils {
} else {
// TODO(jmesserly): locale specific date format
String twoDigits(int n) {
if (n >= 10) return "${n}";
return "0${n}";
if (n >= 10) return "$n";
return "0$n";
}
String twoDigitMonth = twoDigits(then.month);
String twoDigitDay = twoDigits(then.day);
return "${then.year}-${twoDigitMonth}-${twoDigitDay}";
return "${then.year}-$twoDigitMonth-$twoDigitDay";
}
}
@ -179,7 +175,7 @@ class DateUtils {
return ((daysSince1970 + DateTime.thursday) % DateTime.daysPerWeek);
}
/** Formats a time in H:MM A format */
/// Formats a time in H:MM A format */
// TODO(jmesserly): should get 12 vs 24 hour clock setting from the locale
static String toHourMinutesString(Duration duration) {
assert(duration.inDays == 0);
@ -197,12 +193,12 @@ class DateUtils {
}
}
String twoDigits(int n) {
if (n >= 10) return "${n}";
return "0${n}";
if (n >= 10) return "$n";
return "0$n";
}
String mm =
twoDigits(duration.inMinutes.remainder(Duration.minutesPerHour));
return "${hours}:${mm} ${a}";
return "$hours:$mm $a";
}
}

View file

@ -4,30 +4,26 @@
part of utilslib;
/**
* General purpose string manipulation utilities.
*/
/// General purpose string manipulation utilities.
class StringUtils {
/**
* Returns either [str], or if [str] is null, the value of [defaultStr].
*/
/// Returns either [str], or if [str] is null, the value of [defaultStr].
static String defaultString(String? str, [String defaultStr = '']) {
return str == null ? defaultStr : str;
return str ?? defaultStr;
}
/** Parse string to a double, and handle null intelligently */
static double? parseDouble(String? str, [double? ifNull = null]) {
/// Parse string to a double, and handle null intelligently */
static double? parseDouble(String? str, [double? ifNull]) {
return (str == null) ? ifNull : double.parse(str);
}
/** Parse string to a int, and handle null intelligently */
static int? parseInt(String? str, [int? ifNull = null]) {
/// Parse string to a int, and handle null intelligently */
static int? parseInt(String? str, [int? ifNull]) {
return (str == null) ? ifNull : int.parse(str);
}
/** Parse bool to a double, and handle null intelligently */
/// Parse bool to a double, and handle null intelligently */
// TODO(jacobr): corelib should have a boolean parsing method
static bool? parseBool(String? str, [bool? ifNull = null]) {
static bool? parseBool(String? str, [bool? ifNull]) {
assert(str == null || str == 'true' || str == 'false');
return (str == null) ? ifNull : (str == 'true');
}

View file

@ -4,20 +4,16 @@
part of utilslib;
/**
* A parsed URI, inspired by:
* https://github.com/google/closure-library/blob/master/closure/goog/uri/uri.js
*/
/// A parsed URI, inspired by:
/// https://github.com/google/closure-library/blob/master/closure/goog/uri/uri.js
class SwarmUri {
/**
* Parses a URL query string into a map. Because you can have multiple values
* for the same parameter name, each parameter name maps to a list of
* values. For example, '?a=b&c=d&a=e' would be parsed as
* [{'a':['b','e'],'c':['d']}].
*/
/// Parses a URL query string into a map. Because you can have multiple values
/// for the same parameter name, each parameter name maps to a list of
/// values. For example, '?a=b&c=d&a=e' would be parsed as
/// [{'a':['b','e'],'c':['d']}].
// TODO(jmesserly): consolidate with Uri.parse(...)
static Map<String, List<String>> parseQuery(String queryString) {
final queryParams = Map<String, List<String>>();
final queryParams = <String, List<String>>{};
if (queryString.startsWith('?')) {
final params = queryString.substring(1, queryString.length).split('&');
for (final param in params) {
@ -41,9 +37,7 @@ class SwarmUri {
return queryParams;
}
/**
* Percent-encodes a string for use as a query parameter in a URI.
*/
/// Percent-encodes a string for use as a query parameter in a URI.
// TODO(rnystrom): Get rid of this when the real encodeURIComponent()
// function is available within Dart.
static String? encodeComponent(String? component) {
@ -60,10 +54,8 @@ class SwarmUri {
.replaceAll(' ', '%20');
}
/**
* Decodes a string used a query parameter by replacing percent-encoded
* sequences with their original characters.
*/
/// Decodes a string used a query parameter by replacing percent-encoded
/// sequences with their original characters.
// TODO(jmesserly): replace this with a better implementation
static String? decodeComponent(String? component) {
if (component == null) return component;

View file

@ -6,10 +6,9 @@
part of view;
/**
* A View that is composed of child views.
*/
/// A View that is composed of child views.
class CompositeView extends View {
@override
List<View> childViews;
// TODO(rnystrom): Allowing this to be public is gross. CompositeView should
@ -26,7 +25,7 @@ class CompositeView extends View {
final bool _nestedContainer;
final bool _showScrollbar;
CompositeView(String this._cssName,
CompositeView(this._cssName,
[nestedContainer = false,
scrollable = false,
vertical = false,
@ -35,26 +34,27 @@ class CompositeView extends View {
_scrollable = scrollable,
_vertical = vertical,
_showScrollbar = showScrollbar,
childViews = new List<View>() {}
childViews = <View>[];
@override
Element render() {
Element node = new Element.html('<div class="$_cssName"></div>');
Element node = Element.html('<div class="$_cssName"></div>');
if (_nestedContainer) {
container = new Element.html('<div class="scroll-container"></div>');
container = Element.html('<div class="scroll-container"></div>');
node.nodes.add(container);
} else {
container = node;
}
if (_scrollable) {
scroller = new Scroller(
scroller = Scroller(
container,
_vertical /* verticalScrollEnabled */,
!_vertical /* horizontalScrollEnabled */,
true /* momementumEnabled */);
if (_showScrollbar) {
_scrollbar = new Scrollbar(scroller);
_scrollbar = Scrollbar(scroller);
}
}
@ -65,6 +65,7 @@ class CompositeView extends View {
return node;
}
@override
void afterRender(Element node) {
if (_scrollbar != null) {
_scrollbar.initialize();

View file

@ -6,11 +6,9 @@
part of view;
/**
* Holds a number of child views. As you switch between views, the old
* view is pushed off to the side and the new view slides in from the other
* side.
*/
/// Holds a number of child views. As you switch between views, the old
/// view is pushed off to the side and the new view slides in from the other
/// side.
class ConveyorView extends CompositeView {
// TODO(jmesserly): some places use this property to know when the slide
// transition is finished. It would be better to have an event that fires
@ -29,8 +27,9 @@ class ConveyorView extends CompositeView {
ConveyorView()
: animationTimer = null,
super('conveyor-view', true) {}
super('conveyor-view', true);
@override
Element render() {
final result = super.render();
// TODO(rnystrom): Have to do this in render() because container doesn't
@ -60,11 +59,11 @@ class ConveyorView extends CompositeView {
// specified in miliseconds rather than accepting a string.
style.transitionDuration = '${durationSeconds}s';
final xTranslationPercent = -index * 100;
style.transform = 'translate3d(${xTranslationPercent}%, 0px, 0px)';
style.transform = 'translate3d($xTranslationPercent%, 0px, 0px)';
if (animate) {
animationTimer = new Timer(
new Duration(milliseconds: ((durationSeconds * 1000).toInt())), () {
animationTimer =
Timer(Duration(milliseconds: ((durationSeconds * 1000).toInt())), () {
_onAnimationEnd();
});
}
@ -81,10 +80,9 @@ class ConveyorView extends CompositeView {
throw "view not found";
}
/**
* Adds a child view to the ConveyorView. The views are stacked horizontally
* in the order they are added.
*/
/// Adds a child view to the ConveyorView. The views are stacked horizontally
/// in the order they are added.
@override
View addChild(View view) {
view.addClass('conveyor-item');
view.transform = 'translate3d(${(childViews.length * 100)}%, 0, 0)';

View file

@ -8,10 +8,8 @@ part of view;
// TODO(jacobr): handle splitting lines on symbols such as '-' that aren't
// whitespace but are valid word breaking points.
/**
* Utility class to efficiently word break and measure text without requiring
* access to the DOM.
*/
/// Utility class to efficiently word break and measure text without requiring
/// access to the DOM.
class MeasureText {
static CanvasRenderingContext2D _context;
@ -23,7 +21,7 @@ class MeasureText {
MeasureText(this.font) {
if (_context == null) {
CanvasElement canvas = new Element.tag('canvas');
CanvasElement canvas = Element.tag('canvas');
_context = canvas.getContext('2d');
}
if (_spaceLength == null) {
@ -60,12 +58,10 @@ class MeasureText {
}
}
/**
* Add line broken text as html separated by <br> elements.
* Returns the number of lines in the output.
* This function is safe to call with [:sb == null:] in which case just the
* line count is returned.
*/
/// Add line broken text as html separated by <br> elements.
/// Returns the number of lines in the output.
/// This function is safe to call with [:sb == null:] in which case just the
/// line count is returned.
int addLineBrokenText(
StringBuffer sb, String text, num lineWidth, int maxLines) {
// Strip surrounding whitespace. This ensures we create zero lines if there
@ -119,8 +115,8 @@ class MeasureText {
int lines = 0;
num currentLength = 0;
int startIndex = 0;
int wordStartIndex = null;
int lastWordEndIndex = null;
int wordStartIndex;
int lastWordEndIndex;
bool lastWhitespace = true;
// TODO(jacobr): optimize this further.
// To simplify the logic, we simulate injecting a whitespace character

View file

@ -11,25 +11,25 @@ class PageState {
final ObservableValue<int> target;
final ObservableValue<int> length;
PageState()
: current = new ObservableValue<int>(0),
target = new ObservableValue<int>(0),
length = new ObservableValue<int>(1);
: current = ObservableValue<int>(0),
target = ObservableValue<int>(0),
length = ObservableValue<int>(1);
}
/** Simplifies using a PageNumberView and PagedColumnView together. */
/// Simplifies using a PageNumberView and PagedColumnView together. */
class PagedContentView extends CompositeView {
final View content;
final PageState pages;
PagedContentView(this.content)
: pages = new PageState(),
: pages = PageState(),
super('paged-content') {
addChild(new PagedColumnView(pages, content));
addChild(new PageNumberView(pages));
addChild(PagedColumnView(pages, content));
addChild(PageNumberView(pages));
}
}
/** Displays current page and a left/right arrow. Used with [PagedColumnView] */
/// Displays current page and a left/right arrow. Used with [PagedColumnView] */
class PageNumberView extends View {
final PageState pages;
Element _label;
@ -37,11 +37,12 @@ class PageNumberView extends View {
PageNumberView(this.pages);
@override
Element render() {
// TODO(jmesserly): this was supposed to use the somewhat flatter unicode
// glyphs that Chrome uses on the new tab page, but the text is getting
// corrupted.
final node = new Element.html('''
final node = Element.html('''
<div class="page-number">
<div class="page-number-left">&lsaquo;</div>
<div class="page-number-label"></div>
@ -54,6 +55,7 @@ class PageNumberView extends View {
return node;
}
@override
void enterDocument() {
watch(pages.current, (s) => _update());
watch(pages.length, (s) => _update());
@ -76,16 +78,14 @@ class PageNumberView extends View {
}
}
/**
* A horizontal scrolling view that snaps to items like [ConveyorView], but only
* has one child. Instead of scrolling between views, it scrolls between content
* that flows horizontally in columns. Supports left/right swipe to switch
* between pages. Can also be used with [PageNumberView].
*
* This control assumes that it is styled with fixed or percent width and
* height, so the content will flow out horizontally. This allows it to compute
* the number of pages using [:scrollWidth:] and [:offsetWidth:].
*/
/// A horizontal scrolling view that snaps to items like [ConveyorView], but only
/// has one child. Instead of scrolling between views, it scrolls between content
/// that flows horizontally in columns. Supports left/right swipe to switch
/// between pages. Can also be used with [PageNumberView].
///
/// This control assumes that it is styled with fixed or percent width and
/// height, so the content will flow out horizontally. This allows it to compute
/// the number of pages using [:scrollWidth:] and [:offsetWidth:].
class PagedColumnView extends View {
static const MIN_THROW_PAGE_FRACTION = 0.01;
final View contentView;
@ -99,8 +99,9 @@ class PagedColumnView extends View {
PagedColumnView(this.pages, this.contentView);
@override
Element render() {
final node = new Element.html('''
final node = Element.html('''
<div class="paged-column">
<div class="paged-column-container"></div>
</div>''');
@ -113,9 +114,9 @@ class PagedColumnView extends View {
// the scroller configured the default way.
// TODO(jacobr): use named arguments when available.
scroller = new Scroller(_container, false /* verticalScrollEnabled */,
scroller = Scroller(_container, false /* verticalScrollEnabled */,
true /* horizontalScrollEnabled */, true /* momementumEnabled */, () {
return new Size(_getViewLength(_container), 1);
return Size(_getViewLength(_container), 1);
}, Scroller.FAST_SNAP_DECELERATION_FACTOR);
scroller.onDecelStart.listen(_snapToPage);
@ -130,6 +131,7 @@ class PagedColumnView extends View {
// TODO(jmesserly): would be better to not have this code in enterDocument.
// But we need computedStyle to read our CSS properties.
@override
void enterDocument() {
scheduleMicrotask(() {
var style = contentView.node.getComputedStyle();
@ -153,7 +155,7 @@ class PagedColumnView extends View {
});
}
/** Read the column-gap setting so we know how far to translate the child. */
/// Read the column-gap setting so we know how far to translate the child. */
void _computeColumnGap(CssStyleDeclaration style) {
String gap = style.columnGap;
if (gap == 'normal') {
@ -172,7 +174,8 @@ class PagedColumnView extends View {
return double.parse(value).round();
}
/** Watch for resize and update page count. */
/// Watch for resize and update page count. */
@override
void windowResized() {
// TODO(jmesserly): verify we aren't triggering unnecessary layouts.

View file

@ -6,12 +6,10 @@
part of view;
typedef void SelectHandler(String menuText);
typedef SelectHandler = void Function(String menuText);
/**
* This implements a horizontal menu bar with a sliding triangle arrow
* that points at the currently selected item.
*/
/// This implements a horizontal menu bar with a sliding triangle arrow
/// that points at the currently selected item.
class SliderMenu extends View {
static const int TRIANGLE_WIDTH = 24;
@ -24,25 +22,24 @@ class SliderMenu extends View {
// TODO(mattsh) - move this to a touch mixin
Element touchItem;
/**
* Callback function that we call when the user chooses something from
* the menu. This is passed the menu item text.
*/
/// Callback function that we call when the user chooses something from
/// the menu. This is passed the menu item text.
SelectHandler onSelect;
List<String> _menuItems;
final List<String> _menuItems;
SliderMenu(this._menuItems, this.onSelect);
@override
Element render() {
// Create a div for each menu item.
final items = new StringBuffer();
final items = StringBuffer();
for (final item in _menuItems) {
items.write('<div class="sm-item">$item</div>');
}
// Create a root node to hold this view.
return new Element.html('''
return Element.html('''
<div class="sm-root">
<div class="sm-item-box">
<div class="sm-item-filler"></div>
@ -56,6 +53,7 @@ class SliderMenu extends View {
''');
}
@override
void enterDocument() {
// select the first item
// todo(jacobr): too much actual work is performed in enterDocument.
@ -95,10 +93,8 @@ class SliderMenu extends View {
window.onResize.listen((Event event) => updateIndicator(false));
}
/**
* Walks the parent chain of the first Touch target to find the first ancestor
* that has sm-item class.
*/
/// Walks the parent chain of the first Touch target to find the first ancestor
/// that has sm-item class.
Element itemOfTouchEvent(event) {
Node node = event.changedTouches[0].target;
return itemOfNode(node);
@ -153,9 +149,7 @@ class SliderMenu extends View {
}
}
/**
* animate - if true, then animate the movement of the triangle slider
*/
/// animate - if true, then animate the movement of the triangle slider
void updateIndicator(bool animate) {
if (selectedItem != null) {
// calculate where we want to put the triangle

View file

@ -26,7 +26,7 @@ part 'SliderMenu.dart';
// subclasses are refactored to use the new way. There will be some scaffolding
// and construction cones laying around. Try not to freak out.
/** A generic view. */
/// A generic view. */
class View implements Positionable {
Element _node;
ViewLayout _layout;
@ -35,25 +35,25 @@ class View implements Positionable {
// App track the views that want to be notified of resize()
StreamSubscription _resizeSubscription;
/**
* Style properties configured for this view.
*/
/// Style properties configured for this view.
// TODO(jmesserly): We should be getting these from our CSS preprocessor.
// I'm not sure if this will stay as a Map, or just be a get method.
// TODO(jacobr): Consider returning a somewhat typed base.Style wrapper
// object instead, and integrating with built in CSS properties.
@override
final Map<String, String> customStyle;
View() : customStyle = new Map<String, String>();
View() : customStyle = <String, String>{};
View.fromNode(Element this._node) : customStyle = new Map<String, String>();
View.fromNode(this._node) : customStyle = <String, String>{};
View.html(String html)
: customStyle = new Map<String, String>(),
_node = new Element.html(html);
: customStyle = <String, String>{},
_node = Element.html(html);
// TODO(rnystrom): Get rid of this when all views are refactored to not use
// it.
@override
Element get node {
// Lazy render.
if (_node == null) {
@ -63,22 +63,19 @@ class View implements Positionable {
return _node;
}
/**
* A subclass that contains child views should override this to return those
* views. View uses this to ensure that child views are properly rendered
* and initialized when their parent view is without the parent having to
* manually handle that traversal.
*/
/// A subclass that contains child views should override this to return those
/// views. View uses this to ensure that child views are properly rendered
/// and initialized when their parent view is without the parent having to
/// manually handle that traversal.
@override
Iterable<View> get childViews {
return const [];
}
/**
* View presumes the collection of views returned by childViews is more or
* less static after the view is first created. Subclasses should call this
* when that invariant doesn't hold to let View know that a new childView has
* appeared.
*/
/// View presumes the collection of views returned by childViews is more or
/// less static after the view is first created. Subclasses should call this
/// when that invariant doesn't hold to let View know that a new childView has
/// appeared.
void childViewAdded(View child) {
if (isInDocument) {
child._enterDocument();
@ -88,37 +85,31 @@ class View implements Positionable {
}
}
/**
* View presumes the collection of views returned by childViews is more or
* less static after the view is first created. Subclasses should call this
* when that invariant doesn't hold to let View know that a childView has
* been removed.
*/
/// View presumes the collection of views returned by childViews is more or
/// less static after the view is first created. Subclasses should call this
/// when that invariant doesn't hold to let View know that a childView has
/// been removed.
void childViewRemoved(View child) {
if (isInDocument) {
child._exitDocument();
}
}
/** Gets whether this View has already been rendered or not. */
/// Gets whether this View has already been rendered or not. */
bool get isRendered {
return _node != null;
}
/**
* Gets whether this View (or one of its parents) has been added to the
* document or not.
*/
/// Gets whether this View (or one of its parents) has been added to the
/// document or not.
bool get isInDocument {
return _node != null &&
node.ownerDocument is HtmlDocument &&
(node.ownerDocument as HtmlDocument).body.contains(node);
}
/**
* Adds this view to the document as a child of the given node. This should
* generally only be called once for the top-level view.
*/
/// Adds this view to the document as a child of the given node. This should
/// generally only be called once for the top-level view.
void addToDocument(Element parentNode) {
assert(!isInDocument);
@ -137,66 +128,58 @@ class View implements Positionable {
_node.remove();
}
/**
* Override this to generate the DOM structure for the view.
*/
/// Override this to generate the DOM structure for the view.
// TODO(rnystrom): make this method abstract, see b/5015671
Element render() {
throw 'abstract';
}
/**
* Override this to perform initialization behavior that requires access to
* the DOM associated with the View, such as event wiring.
*/
/// Override this to perform initialization behavior that requires access to
/// the DOM associated with the View, such as event wiring.
void afterRender(Element node) {
// Do nothing by default.
}
/**
* Override this to perform behavior after this View has been added to the
* document. This is appropriate if you need access to state (such as the
* calculated size of an element) that's only available when the View is in
* the document.
*
* This will be called each time the View is added to the document, if it is
* added and removed multiple times.
*/
/// Override this to perform behavior after this View has been added to the
/// document. This is appropriate if you need access to state (such as the
/// calculated size of an element) that's only available when the View is in
/// the document.
///
/// This will be called each time the View is added to the document, if it is
/// added and removed multiple times.
void enterDocument() {}
/**
* Override this to perform behavior after this View has been removed from the
* document. This can be a convenient time to unregister event handlers bound
* in enterDocument().
*
* This will be called each time the View is removed from the document, if it
* is added and removed multiple times.
*/
/// Override this to perform behavior after this View has been removed from the
/// document. This can be a convenient time to unregister event handlers bound
/// in enterDocument().
///
/// This will be called each time the View is removed from the document, if it
/// is added and removed multiple times.
void exitDocument() {}
/** Override this to perform behavior after the window is resized. */
/// Override this to perform behavior after the window is resized. */
// TODO(jmesserly): this isn't really the event we want. Ideally we want to
// fire the event only if this particular View changed size. Also we should
// give a view the ability to measure itself when added to the document.
void windowResized() {}
/**
* Registers the given listener callback to the given observable. Also
* immediately invokes the callback once as if a change has just come in.
* This lets you define a render() method that renders the skeleton of a
* view, then register a bunch of listeners which all fire to populate the
* view with model data.
*/
void watch(Observable observable, void watcher(EventSummary summary)) {
/// Registers the given listener callback to the given observable. Also
/// immediately invokes the callback once as if a change has just come in.
/// This lets you define a render() method that renders the skeleton of a
/// view, then register a bunch of listeners which all fire to populate the
/// view with model data.
void watch(
Observable observable, void Function(EventSummary summary) watcher) {
// Make a fake summary for the initial watch.
final summary = new EventSummary(observable);
final summary = EventSummary(observable);
watcher(summary);
attachWatch(observable, watcher);
}
/** Registers the given listener callback to the given observable. */
void attachWatch(Observable observable, void watcher(EventSummary summary)) {
/// Registers the given listener callback to the given observable. */
void attachWatch(
Observable observable, void Function(EventSummary summary) watcher) {
observable.addChangeListener(watcher);
// TODO(rnystrom): Should keep track of this and unregister when the view
@ -207,15 +190,11 @@ class View implements Positionable {
_node.onClick.listen(handler);
}
/**
* Gets whether the view is hidden.
*/
/// Gets whether the view is hidden.
bool get hidden => _node.style.display == 'none';
/**
* Sets whether the view is hidden.
*/
void set hidden(bool hidden) {
/// Sets whether the view is hidden.
set hidden(bool hidden) {
if (hidden) {
node.style.display = 'none';
} else {
@ -231,43 +210,35 @@ class View implements Positionable {
node.classes.remove(className);
}
/** Sets the CSS3 transform applied to the view. */
/// Sets the CSS3 transform applied to the view. */
set transform(String transform) {
node.style.transform = transform;
}
// TODO(rnystrom): Get rid of this, or move into a separate class?
/** Creates a View whose node is a <div> with the given class(es). */
static View div(String cssClass, [String body = null]) {
if (body == null) {
body = '';
}
return new View.html('<div class="$cssClass">$body</div>');
/// Creates a View whose node is a <div> with the given class(es). */
static View div(String cssClass, [String body]) {
body ??= '';
return View.html('<div class="$cssClass">$body</div>');
}
/**
* Internal render method that deals with traversing child views. Should not
* be overridden.
*/
/// Internal render method that deals with traversing child views. Should not
/// be overridden.
void _render() {
// TODO(rnystrom): Should render child views here. Not implemented yet.
// Instead, we rely on the parent accessing .node to implicitly cause the
// child to be rendered.
// Render this view.
if (_node == null) {
_node = render();
}
_node ??= render();
// Pass the node back to the derived view so it can register event
// handlers on it.
afterRender(_node);
}
/**
* Internal method that deals with traversing child views. Should not be
* overridden.
*/
/// Internal method that deals with traversing child views. Should not be
/// overridden.
void _enterDocument() {
// Notify the children first.
for (final child in childViews) {
@ -279,17 +250,14 @@ class View implements Positionable {
// Layout related methods
@override
ViewLayout get layout {
if (_layout == null) {
_layout = new ViewLayout.fromView(this);
}
_layout ??= ViewLayout.fromView(this);
return _layout;
}
/**
* Internal method that deals with traversing child views. Should not be
* overridden.
*/
/// Internal method that deals with traversing child views. Should not be
/// overridden.
void _exitDocument() {
// Notify this View first so that it's children are still valid.
exitDocument();
@ -300,11 +268,9 @@ class View implements Positionable {
}
}
/**
* If needed, starts a layout computation from the top level.
* Also hooks the relevant events like window resize, so we can layout on too
* demand.
*/
/// If needed, starts a layout computation from the top level.
/// Also hooks the relevant events like window resize, so we can layout on too
/// demand.
void _hookGlobalLayoutEvents() {
if (_resizeSubscription == null) {
var handler = EventBatch.wrap((e) => doLayout());
@ -322,6 +288,7 @@ class View implements Positionable {
}
}
@override
void doLayout() {
_measureLayout().then((changed) {
if (changed) {
@ -332,7 +299,7 @@ class View implements Positionable {
Future<bool> _measureLayout() {
// TODO(10459): code should not use Completer.sync.
final changed = new Completer<bool>.sync();
final changed = Completer<bool>.sync();
_measureLayoutHelper(changed);
var changedComplete = false;
@ -356,10 +323,9 @@ class View implements Positionable {
// a good tradeoff?
if (ViewLayout.hasCustomLayout(this)) {
// TODO(10459): code should not use Completer.sync.
Completer sizeCompleter = new Completer<Size>.sync();
Completer sizeCompleter = Completer<Size>.sync();
scheduleMicrotask(() {
sizeCompleter
.complete(new Size(_node.client.width, _node.client.height));
sizeCompleter.complete(Size(_node.client.width, _node.client.height));
});
layout.measureLayout(sizeCompleter.future, changed);
} else {