// 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 customStyle; View() : customStyle = new Map(); View.fromNode(Element this._node) : customStyle = new Map(); View.html(String html) : customStyle = new Map(), _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 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
with the given class(es). */ static View div(String cssClass, [String body = null]) { if (body == null) { body = ''; } return new View.html('
$body
'); } /** * 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 _measureLayout() { // TODO(10459): code should not use Completer.sync. final changed = new Completer.sync(); _measureLayoutHelper(changed); var changedComplete = false; changed.future.then((_) { changedComplete = true; }); scheduleMicrotask(() { if (!changedComplete) { changed.complete(false); } }); return changed.future; } void _measureLayoutHelper(Completer 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.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(); } }