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 |