745f112ef6
Opt an initial batch of files under samples, samples-dev, utils, and runtime/tools/dartfuzz out of null safety in preparation for switching the flag on by default. Change-Id: Icdfd52a5a969e678a7205903332f73fe3841c223 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/166960 Reviewed-by: Daco Harkes <dacoharkes@google.com> Commit-Queue: Leaf Petersen <leafp@google.com>
385 lines
10 KiB
Dart
385 lines
10 KiB
Dart
// 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();
|
|
}
|
|
}
|