blob: 30f5e469924cd053dc1fe0f584320a8dde7dab75 [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
library view;
import 'dart:async';
import 'dart:html';
import 'dart:math' as Math;
import '../base/base.dart';
import '../observable/observable.dart';
import '../touch/touch.dart';
import '../layout/layout.dart';
part 'CompositeView.dart';
part 'ConveyorView.dart';
part 'MeasureText.dart';
part 'PagedViews.dart';
part 'SliderMenu.dart';
// TODO(rnystrom): Note! This class is undergoing heavy construction. It will
// temporary support both some old and some new ways of doing things until all
// 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. */
class View implements Positionable {
Element _node;
ViewLayout _layout;
// TODO(jmesserly): instead of tracking this on every View, we could have the
// App track the views that want to be notified of resize()
StreamSubscription _resizeSubscription;
/**
* 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.
final Map<String, String> customStyle;
View() : customStyle = new Map<String, String>();
View.fromNode(Element this._node) : customStyle = new Map<String, String>();
View.html(String html)
: customStyle = new Map<String, String>(),
_node = new Element.html(html);
// TODO(rnystrom): Get rid of this when all views are refactored to not use
// it.
Element get node {
// Lazy render.
if (_node == null) {
_render();
}
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.
*/
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.
*/
void childViewAdded(View child) {
if (isInDocument) {
child._enterDocument();
// TODO(jmesserly): is this too expensive?
doLayout();
}
}
/**
* 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. */
bool get isRendered {
return _node != null;
}
/**
* 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.
*/
void addToDocument(Element parentNode) {
assert(!isInDocument);
_render();
parentNode.nodes.add(_node);
_hookGlobalLayoutEvents();
_enterDocument();
}
void removeFromDocument() {
assert(isInDocument);
// Remove runs in reverse order of how we entered.
_exitDocument();
_unhookGlobalLayoutEvents();
_node.remove();
}
/**
* 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.
*/
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.
*/
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.
*/
void exitDocument() {}
/** 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)) {
// Make a fake summary for the initial watch.
final summary = new EventSummary(observable);
watcher(summary);
attachWatch(observable, watcher);
}
/** Registers the given listener callback to the given observable. */
void attachWatch(Observable observable, void watcher(EventSummary summary)) {
observable.addChangeListener(watcher);
// TODO(rnystrom): Should keep track of this and unregister when the view
// is discarded.
}
void addOnClick(EventListener handler) {
_node.onClick.listen(handler);
}
/**
* Gets whether the view is hidden.
*/
bool get hidden => _node.style.display == 'none';
/**
* Sets whether the view is hidden.
*/
void set hidden(bool hidden) {
if (hidden) {
node.style.display = 'none';
} else {
node.style.display = '';
}
}
void addClass(String className) {
node.classes.add(className);
}
void removeClass(String className) {
node.classes.remove(className);
}
/** 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>');
}
/**
* 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();
}
// 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.
*/
void _enterDocument() {
// Notify the children first.
for (final child in childViews) {
child._enterDocument();
}
enterDocument();
}
// Layout related methods
ViewLayout get layout {
if (_layout == null) {
_layout = new ViewLayout.fromView(this);
}
return _layout;
}
/**
* 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();
// Notify the children.
for (final child in childViews) {
child._exitDocument();
}
}
/**
* 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());
_resizeSubscription = window.onResize.listen(handler);
}
// Trigger the initial layout.
doLayout();
}
void _unhookGlobalLayoutEvents() {
if (_resizeSubscription != null) {
_resizeSubscription.cancel();
_resizeSubscription = null;
}
}
void doLayout() {
_measureLayout().then((changed) {
if (changed) {
_applyLayoutToChildren();
}
});
}
Future<bool> _measureLayout() {
// TODO(10459): code should not use Completer.sync.
final changed = new Completer<bool>.sync();
_measureLayoutHelper(changed);
var changedComplete = false;
changed.future.then((_) {
changedComplete = true;
});
scheduleMicrotask(() {
if (!changedComplete) {
changed.complete(false);
}
});
return changed.future;
}
void _measureLayoutHelper(Completer<bool> changed) {
windowResized();
// TODO(jmesserly): this logic is more complex than it needs to be because
// we're taking pains to not initialize _layout if it's not needed. Is that
// a good tradeoff?
if (ViewLayout.hasCustomLayout(this)) {
// TODO(10459): code should not use Completer.sync.
Completer sizeCompleter = new Completer<Size>.sync();
scheduleMicrotask(() {
sizeCompleter
.complete(new Size(_node.client.width, _node.client.height));
});
layout.measureLayout(sizeCompleter.future, changed);
} else {
for (final child in childViews) {
child._measureLayoutHelper(changed);
}
}
}
void _applyLayoutToChildren() {
for (final child in childViews) {
child._applyLayout();
}
}
void _applyLayout() {
if (_layout != null) {
_layout.applyLayout();
}
_applyLayoutToChildren();
}
}