| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 #library("view"); | |
| 6 | |
| 7 #import('dart:html'); | |
| 8 #import('../base/base.dart'); | |
| 9 #import('../observable/observable.dart'); | |
| 10 #import('../touch/touch.dart'); | |
| 11 #import('../layout/layout.dart'); | |
| 12 | |
| 13 #source('CompositeView.dart'); | |
| 14 #source('ConveyorView.dart'); | |
| 15 #source('MeasureText.dart'); | |
| 16 #source('PagedViews.dart'); | |
| 17 #source('SliderMenu.dart'); | |
| 18 | |
| 19 | |
| 20 // TODO(rnystrom): Note! This class is undergoing heavy construction. It will | |
| 21 // temporary support both some old and some new ways of doing things until all | |
| 22 // subclasses are refactored to use the new way. There will be some scaffolding | |
| 23 // and construction cones laying around. Try not to freak out. | |
| 24 | |
| 25 /** A generic view. */ | |
| 26 class View implements Positionable { | |
| 27 Element _node; | |
| 28 ViewLayout _layout; | |
| 29 | |
| 30 // TODO(jmesserly): instead of tracking this on every View, we could have the | |
| 31 // App track the views that want to be notified of resize() | |
| 32 EventListener _resizeHandler; | |
| 33 | |
| 34 /** | |
| 35 * Style properties configured for this view. | |
| 36 */ | |
| 37 // TODO(jmesserly): We should be getting these from our CSS preprocessor. | |
| 38 // I'm not sure if this will stay as a Map, or just be a get method. | |
| 39 // TODO(jacobr): Consider returning a somewhat typed base.Style wrapper | |
| 40 // object instead, and integrating with built in CSS properties. | |
| 41 final Map<String, String> customStyle; | |
| 42 | |
| 43 View() | |
| 44 : customStyle = new Map<String, String>(); | |
| 45 | |
| 46 View.fromNode(Element this._node) | |
| 47 : customStyle = new Map<String, String>(); | |
| 48 | |
| 49 View.html(String html) | |
| 50 : customStyle = new Map<String, String>(), | |
| 51 _node = new Element.html(html); | |
| 52 | |
| 53 // TODO(rnystrom): Get rid of this when all views are refactored to not use | |
| 54 // it. | |
| 55 Element get node() { | |
| 56 // Lazy render. | |
| 57 if (_node === null) { | |
| 58 _render(); | |
| 59 } | |
| 60 | |
| 61 return _node; | |
| 62 } | |
| 63 | |
| 64 /** | |
| 65 * A subclass that contains child views should override this to return those | |
| 66 * views. View uses this to ensure that child views are properly rendered | |
| 67 * and initialized when their parent view is without the parent having to | |
| 68 * manually handle that traversal. | |
| 69 */ | |
| 70 Collection<View> get childViews() { | |
| 71 return const []; | |
| 72 } | |
| 73 | |
| 74 /** | |
| 75 * View presumes the collection of views returned by childViews is more or | |
| 76 * less static after the view is first created. Subclasses should call this | |
| 77 * when that invariant doesn't hold to let View know that a new childView has | |
| 78 * appeared. | |
| 79 */ | |
| 80 void childViewAdded(View child) { | |
| 81 if (isInDocument) { | |
| 82 child._enterDocument(); | |
| 83 | |
| 84 // TODO(jmesserly): is this too expensive? | |
| 85 doLayout(); | |
| 86 } | |
| 87 } | |
| 88 | |
| 89 /** | |
| 90 * View presumes the collection of views returned by childViews is more or | |
| 91 * less static after the view is first created. Subclasses should call this | |
| 92 * when that invariant doesn't hold to let View know that a childView has | |
| 93 * been removed. | |
| 94 */ | |
| 95 void childViewRemoved(View child) { | |
| 96 if (isInDocument) { | |
| 97 child._exitDocument(); | |
| 98 } | |
| 99 } | |
| 100 | |
| 101 /** Gets whether this View has already been rendered or not. */ | |
| 102 bool get isRendered() { | |
| 103 return _node !== null; | |
| 104 } | |
| 105 | |
| 106 /** | |
| 107 * Gets whether this View (or one of its parents) has been added to the | |
| 108 * document or not. | |
| 109 */ | |
| 110 bool get isInDocument() { | |
| 111 return _node !== null && node.document.contains(node); | |
| 112 } | |
| 113 | |
| 114 /** | |
| 115 * Adds this view to the document as a child of the given node. This should | |
| 116 * generally only be called once for the top-level view. | |
| 117 */ | |
| 118 void addToDocument(Element parentNode) { | |
| 119 assert(!isInDocument); | |
| 120 | |
| 121 _render(); | |
| 122 parentNode.nodes.add(_node); | |
| 123 _hookGlobalLayoutEvents(); | |
| 124 _enterDocument(); | |
| 125 } | |
| 126 | |
| 127 void removeFromDocument() { | |
| 128 assert(isInDocument); | |
| 129 | |
| 130 // Remove runs in reverse order of how we entered. | |
| 131 _exitDocument(); | |
| 132 _unhookGlobalLayoutEvents(); | |
| 133 _node.remove(); | |
| 134 } | |
| 135 | |
| 136 /** | |
| 137 * Override this to generate the DOM structure for the view. | |
| 138 */ | |
| 139 // TODO(rnystrom): make this method abstract, see b/5015671 | |
| 140 Element render() { throw 'abstract'; } | |
| 141 | |
| 142 /** | |
| 143 * Override this to perform initialization behavior that requires access to | |
| 144 * the DOM associated with the View, such as event wiring. | |
| 145 */ | |
| 146 void afterRender(Element node) { | |
| 147 // Do nothing by default. | |
| 148 } | |
| 149 | |
| 150 /** | |
| 151 * Override this to perform behavior after this View has been added to the | |
| 152 * document. This is appropriate if you need access to state (such as the | |
| 153 * calculated size of an element) that's only available when the View is in | |
| 154 * the document. | |
| 155 * | |
| 156 * This will be called each time the View is added to the document, if it is | |
| 157 * added and removed multiple times. | |
| 158 */ | |
| 159 void enterDocument() {} | |
| 160 | |
| 161 /** | |
| 162 * Override this to perform behavior after this View has been removed from the | |
| 163 * document. This can be a convenient time to unregister event handlers bound | |
| 164 * in enterDocument(). | |
| 165 * | |
| 166 * This will be called each time the View is removed from the document, if it | |
| 167 * is added and removed multiple times. | |
| 168 */ | |
| 169 void exitDocument() {} | |
| 170 | |
| 171 /** Override this to perform behavior after the window is resized. */ | |
| 172 // TODO(jmesserly): this isn't really the event we want. Ideally we want to | |
| 173 // fire the event only if this particular View changed size. Also we should | |
| 174 // give a view the ability to measure itself when added to the document. | |
| 175 void windowResized() {} | |
| 176 | |
| 177 /** | |
| 178 * Registers the given listener callback to the given observable. Also | |
| 179 * immedially invokes the callback once as if a change has just come in. This | |
| 180 * lets you define a render() method that renders the skeleton of a view, then | |
| 181 * register a bunch of listeners which all fire to populate the view with | |
| 182 * model data. | |
| 183 */ | |
| 184 void watch(Observable observable, void watcher(EventSummary summary)) { | |
| 185 // Make a fake summary for the initial watch. | |
| 186 final summary = new EventSummary(observable); | |
| 187 watcher(summary); | |
| 188 | |
| 189 attachWatch(observable, watcher); | |
| 190 } | |
| 191 | |
| 192 /** Registers the given listener callback to the given observable. */ | |
| 193 void attachWatch(Observable observable, void watcher(EventSummary summary)) { | |
| 194 observable.addChangeListener(watcher); | |
| 195 | |
| 196 // TODO(rnystrom): Should keep track of this and unregister when the view | |
| 197 // is discarded. | |
| 198 } | |
| 199 | |
| 200 void addOnClick(EventListener handler) { | |
| 201 _node.on.click.add(handler); | |
| 202 } | |
| 203 | |
| 204 /** | |
| 205 * Gets whether the view is hidden. | |
| 206 */ | |
| 207 bool get hidden() => _node.style.display == 'none'; | |
| 208 | |
| 209 /** | |
| 210 * Sets whether the view is hidden. | |
| 211 */ | |
| 212 void set hidden(bool hidden) { | |
| 213 if (hidden) { | |
| 214 node.style.display = 'none'; | |
| 215 } else { | |
| 216 node.style.display = ''; | |
| 217 } | |
| 218 } | |
| 219 | |
| 220 void addClass(String className) { | |
| 221 node.classes.add(className); | |
| 222 } | |
| 223 | |
| 224 void removeClass(String className) { | |
| 225 node.classes.remove(className); | |
| 226 } | |
| 227 | |
| 228 /** Sets the CSS3 transform applied to the view. */ | |
| 229 set transform(String transform) { | |
| 230 node.style.transform = transform; | |
| 231 } | |
| 232 | |
| 233 // TODO(rnystrom): Get rid of this, or move into a separate class? | |
| 234 /** Creates a View whose node is a <div> with the given class(es). */ | |
| 235 static View div(String cssClass, [String body = null]) { | |
| 236 if (body == null) { | |
| 237 body = ''; | |
| 238 } | |
| 239 return new View.html('<div class="$cssClass">$body</div>'); | |
| 240 } | |
| 241 | |
| 242 /** | |
| 243 * Internal render method that deals with traversing child views. Should not | |
| 244 * be overridden. | |
| 245 */ | |
| 246 void _render() { | |
| 247 // TODO(rnystrom): Should render child views here. Not implemented yet. | |
| 248 // Instead, we rely on the parent accessing .node to implicitly cause the | |
| 249 // child to be rendered. | |
| 250 | |
| 251 // Render this view. | |
| 252 if (_node == null) { | |
| 253 _node = render(); | |
| 254 } | |
| 255 | |
| 256 // Pass the node back to the derived view so it can register event | |
| 257 // handlers on it. | |
| 258 afterRender(_node); | |
| 259 } | |
| 260 | |
| 261 /** | |
| 262 * Internal method that deals with traversing child views. Should not be | |
| 263 * overridden. | |
| 264 */ | |
| 265 void _enterDocument() { | |
| 266 // Notify the children first. | |
| 267 for (final child in childViews) { | |
| 268 child._enterDocument(); | |
| 269 } | |
| 270 | |
| 271 enterDocument(); | |
| 272 } | |
| 273 | |
| 274 // Layout related methods | |
| 275 | |
| 276 ViewLayout get layout() { | |
| 277 if (_layout == null) { | |
| 278 _layout = new ViewLayout.fromView(this); | |
| 279 } | |
| 280 return _layout; | |
| 281 } | |
| 282 | |
| 283 /** | |
| 284 * Internal method that deals with traversing child views. Should not be | |
| 285 * overridden. | |
| 286 */ | |
| 287 void _exitDocument() { | |
| 288 // Notify this View first so that it's children are still valid. | |
| 289 exitDocument(); | |
| 290 | |
| 291 // Notify the children. | |
| 292 for (final child in childViews) { | |
| 293 child._exitDocument(); | |
| 294 } | |
| 295 } | |
| 296 | |
| 297 /** | |
| 298 * If needed, starts a layout computation from the top level. | |
| 299 * Also hooks the relevant events like window resize, so we can layout on too | |
| 300 * demand. | |
| 301 */ | |
| 302 void _hookGlobalLayoutEvents() { | |
| 303 if (_resizeHandler == null) { | |
| 304 _resizeHandler = EventBatch.wrap((e) => doLayout()); | |
| 305 } | |
| 306 window.on.resize.add(_resizeHandler); | |
| 307 | |
| 308 // Trigger the initial layout. | |
| 309 doLayout(); | |
| 310 } | |
| 311 | |
| 312 void _unhookGlobalLayoutEvents() { | |
| 313 if (_resizeHandler != null) { | |
| 314 window.on.resize.remove(_resizeHandler); | |
| 315 _resizeHandler = null; | |
| 316 } | |
| 317 } | |
| 318 | |
| 319 void doLayout() { | |
| 320 _measureLayout().then((bool changed) { | |
| 321 if (changed) { | |
| 322 _applyLayoutToChildren(); | |
| 323 } | |
| 324 }); | |
| 325 } | |
| 326 | |
| 327 Future<bool> _measureLayout() { | |
| 328 final changed = new Completer<bool>(); | |
| 329 _measureLayoutHelper(changed); | |
| 330 | |
| 331 window.requestLayoutFrame(() { | |
| 332 if (!changed.future.isComplete) { | |
| 333 changed.complete(false); | |
| 334 } | |
| 335 }); | |
| 336 return changed.future; | |
| 337 } | |
| 338 | |
| 339 void _measureLayoutHelper(Completer<bool> changed) { | |
| 340 windowResized(); | |
| 341 | |
| 342 // TODO(jmesserly): this logic is more complex than it needs to be because | |
| 343 // we're taking pains to not initialize _layout if it's not needed. Is that | |
| 344 // a good tradeoff? | |
| 345 if (ViewLayout.hasCustomLayout(this)) { | |
| 346 Completer sizeCompleter = new Completer<Size>(); | |
| 347 _node.rect.then((ElementRect rect) { | |
| 348 sizeCompleter.complete( | |
| 349 new Size(rect.client.width, rect.client.height)); | |
| 350 }); | |
| 351 layout.measureLayout(sizeCompleter.future, changed); | |
| 352 } else { | |
| 353 for (final child in childViews) { | |
| 354 child._measureLayoutHelper(changed); | |
| 355 } | |
| 356 } | |
| 357 } | |
| 358 | |
| 359 void _applyLayoutToChildren() { | |
| 360 for (final child in childViews) { | |
| 361 child._applyLayout(); | |
| 362 } | |
| 363 } | |
| 364 | |
| 365 void _applyLayout() { | |
| 366 if (_layout != null) { | |
| 367 _layout.applyLayout(); | |
| 368 } | |
| 369 _applyLayoutToChildren(); | |
| 370 } | |
| 371 } | |
| OLD | NEW |