| // 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(); |
| } |
| } |