· 8 years ago · Feb 20, 2018, 07:56 PM
1// ========================================================================
2// SproutCore
3// copyright 2006-2008 Sprout Systems, Inc.
4// ========================================================================
5
6(function() {
7
8var SC = SC.Deprecated;
9
10require('foundation/object') ;
11require('foundation/responder') ;
12require('foundation/node_descriptor') ;
13require('foundation/binding');
14require('foundation/path_module');
15
16require('mixins/delegate_support') ;
17require('mixins/array') ;
18require('mixins/tree') ;
19
20SC.BENCHMARK_OUTLETS = NO ;
21SC.BENCHMARK_CONFIGURE_OUTLETS = NO ;
22
23SC.AUTORESIZE_BORDERS = 'borders';
24SC.AUTORESIZE_WIDTH = 'width';
25SC.AUTORESIZE_RIGHT = 'right';
26SC.AUTORESIZE_LEFT = 'left';
27SC.AUTORESIZE_HEIGHT = 'height';
28SC.AUTORESIZE_TOP = 'top';
29SC.AUTORESIZE_BOTTOM = 'bottom';
30SC.AUTORESIZE_WIDTH_HEIGHT = 'width+height';
31SC.AUTORESIZE_WIDTH_TOP = 'width+top';
32SC.AUTORESIZE_WIDTH_BOTTOM = 'width+bottom';
33SC.AUTORESIZE_RIGHT_HEIGHT = 'right+height';
34SC.AUTORESIZE_RIGHT_TOP = 'right+top';
35SC.AUTORESIZE_RIGHT_BOTTOM = 'right+bottom';
36SC.AUTORESIZE_LEFT_TOP = 'left+top';
37SC.AUTORESIZE_LEFT_BOTTOM = 'left+bottom';
38SC.AUTORESIZE_LEFT_HEIGHT = 'left+height';
39
40/**
41 @class
42
43 A view is the root class you use to manage the web page DOM in your
44 application. You can use views to render visible content on your page,
45 provide animations, and to capture and respond to events.
46
47 You can use SC.View directly to manage DOM elements or you can extend one
48 of the many subclasses provided by SproutCore. This documentation describes
49 the general concepts you need to understand when working with views, though
50 most often you will want to work with one of the subclasses instead.
51
52 h2. Working with DOM Elements
53
54 h2. Handling Events
55
56 @extends SC.Responder
57 @extends SC.PathModule
58 @extends SC.DelegateSupport
59 @since SproutCore 1.0
60*/
61
62
63SC.View = SC.Responder.extend( SC.PathModule, SC.DelegateSupport, SC.Tree,
64/** @scope SC.View.prototype */ {
65
66 isAwake: false,
67
68 awake: function() {
69 if ( !this.isAwake ) {
70 var outlets = this.outlets;
71
72 // ...and then appends outlets to the view tree...
73 for ( var i = 0, e = outlets.length; i < e; ++i ) {
74 var key = outlets[i];
75 var view = this[key];
76 if (view) this.insertBefore(view, null);
77 }
78
79 // ...and then calls awake() recursively...
80 for ( var i = 0, e = outlets.length; i < e; ++i ) {
81 var key = outlets[i];
82 var view = this[key];
83 if (view) view.awake();
84 }
85
86 // ... and then configures any bindings.
87 // for ( NSString *key in _bindings ) {
88 // SCBinding *binding = [self bindTo: [key substringWithRange: NSMakeRange( 0, [key length] - 7)] from: [_bindings objectForKey: key]];
89 // [bindings addObject: binding];
90 // }
91 // sc_super();
92
93 // _outlets = nil; // don't need to keep this around anymore
94 // _bindings = nil; // don't need to keep this around anymore
95 this.isAwake = true;
96 }
97 },
98
99 // ..........................................
100 // VIEW API
101 //
102 // The methods in this section are used to manage actual views. You can
103 // basically interact with child elements in two ways. One using an API
104 // similar to the DOM API. Alternatively, you can treat the view like an
105 // array and use standard iterators.
106 //
107
108 /**
109 Insert the view into the the receiver's childNodes array.
110
111 The view will be added to the childNodes array before the beforeView. If
112 beforeView is null, then the view will be added to the end of the array.
113 This will also add the view's rootElement DOM node to the receivers
114 containerElement DOM node as a child.
115
116 If the specified view already belongs to another parent, it will be
117 removed from that view first.
118
119 @param view {SC.View} the view to insert as a child node.
120 @param beforeView {SC.View} view to insert before, or null to insert at
121 end
122 @returns {SC.View} the receiver
123 */
124 insertBefore: function(view, beforeView) {
125 this._insertBefore(view,beforeView,true);
126 },
127
128 /** @private */
129 _insertBefore: function(view, beforeView, updateDom) {
130 // verify that beforeView is a child.
131 if (beforeView) {
132 if (beforeView.parentNode != this) throw "insertBefore() beforeView must belong to the receiver" ;
133 if (beforeView == view) throw "insertBefore() views cannot be the same";
134 }
135
136 if (view.parentNode) view.removeFromParent() ;
137 this.willAddChild(this, beforeView) ;
138 view.willAddToParent(this, beforeView) ;
139
140 // patch in the view.
141 if (beforeView) {
142 view.set('previousSibling', beforeView.previousSibling) ;
143 view.set('nextSibling', beforeView) ;
144 beforeView.set('previousSibling', view) ;
145 } else {
146 view.set('previousSibling', this.lastChild) ;
147 view.set('nextSibling', null) ;
148 this.set('lastChild', view) ;
149 }
150
151 if (view.previousSibling) view.previousSibling.set('nextSibling',view);
152 if (view.previousSibling == null) this.set('firstChild',view) ;
153 view.set('parentNode', this) ;
154
155 // Update DOM. -- ANIMATE
156 // Note that this code is not called when outlets are first configured.
157 // The assumption is that the created view already belongs to the
158 // document somwhere.
159 if (updateDom) {
160 var beforeElement = (beforeView) ? beforeView.rootElement : null;
161
162 (this.containerElement || this.rootElement).insertBefore(view.rootElement,beforeElement);
163
164 // regenerate the childNodes array.
165 this._rebuildChildNodes();
166 }
167
168 // update cached states.
169 view._updateIsVisibleInWindow() ;
170 view._flushInternalCaches() ;
171 view._invalidateClippingFrame() ;
172
173 // call notices.
174 view.didAddToParent(this, beforeView) ;
175 this.didAddChild(view, beforeView) ;
176 try{
177 return this ;
178 }finally{
179 if(beforeElement)
180 beforeElement=null;
181 }
182 },
183
184 /**
185 Remove the view from the receiver's childNodes array.
186
187 This will also remove the view's DOM element from the recievers DOM.
188
189 @param view {SC.View} the view to remove
190 @returns {SC.View} the receiver
191 */
192 removeChild: function(view) {
193 if (!view) return ;
194 if (view.parentNode != this) throw "removeChild: view must belong to parent";
195
196 view.willRemoveFromParent() ;
197 this.willRemoveChild(view) ;
198
199 // unpatch.
200 if (view.previousSibling) {
201 view.previousSibling.set('nextSibling', view.nextSibling);
202 } else this.set('firstChild', view.nextSibling) ;
203
204 if (view.nextSibling) {
205 view.nextSibling.set('previousSibling', view.previousSibling) ;
206 } else this.set('lastChild', view.previousSibling) ;
207
208 // Update DOM -- ANIMATE
209 var el = (this.containerElement || this.rootElement);
210 if (el && (view.rootElement.parentNode == el) && (el != document)) {
211 el.removeChild(view.rootElement);
212 }
213
214 // regenerate the childNodes array.
215 this._rebuildChildNodes();
216
217 view.set('nextSibling', null);
218 view.set('previousSibling', null);
219 view.set('parentNode', null) ;
220
221 // update parent state.
222 view._updateIsVisibleInWindow() ;
223 view._flushInternalCaches();
224 view._invalidateClippingFrame() ;
225
226 view.didRemoveFromParent(this) ;
227 this.didRemoveChild(view);
228 try{
229 return this;
230}finally{
231 el=null;
232}
233 },
234
235 /**
236 Replace the oldView with the specified view in the receivers childNodes
237 array. This will also replace the DOM node of the oldView with the DOM
238 node of the new view in the receivers DOM.
239
240 If the specified view already belongs to another parent, it will be
241 removed from that view first.
242
243 @param view {SC.View} the view to insert in the DOM
244 @param view {SC.View} the view to remove from the DOM.
245 @returns {SC.View} the receiver
246 */
247 replaceChild: function(view, oldView) {
248 this.insertBefore(view,oldView) ; this.removeChild(oldView) ;
249 return this;
250 },
251
252 /**
253 Removes the receiver from its parentNode. If the receiver does not belong
254 to a parentNode, this method does nothing.
255
256 @returns {null}
257 */
258 removeFromParent: function() {
259 if (this.parentNode) this.parentNode.removeChild(this) ;
260 return null ;
261 },
262
263 /**
264 Works just like removeFromParent but also removes the view from internal
265 caches and sets the rootElement to null so that the view and its DOM can
266 be garbage collected.
267
268 SproutCore includes special gaurds that ensure views and their related
269 DOM elements will be garbage collected whenever your web page unloads.
270 However, if you create and destroy views frequently while your application
271 is running, you should call this method when views are no longer needed
272 to ensure they will be garbage collected even while your application is
273 still running.
274
275 @returns {null}
276 */
277 destroy: function() {
278 this.removeFromParent() ;
279 delete SC.View._view[SC.guidFor(this)];
280 return null ;
281 },
282
283 /**
284 Appends the specified view to the end of the receivers childNodes array.
285 This is equivalent to calling insertBefore(view, null);
286
287 @param view {SC.View} the view to insert
288 @returns {SC.View} the receiver
289 */
290 appendChild: function(view) {
291 this.insertBefore(view,null) ;
292 return this ;
293 },
294
295 /**
296 The array of views that are direct children of the receiver view. The DOM
297 elements managed by the views are also directl children of the
298 containerElement for the receiver.
299
300 @field
301 @type Array
302 */
303 childNodes: [],
304
305 /**
306 The first child view in the childNodes array. If the view does not have
307 any children, this property will be null.
308
309 @field
310 @type SC.View
311 */
312 firstChild: null,
313
314 /**
315 The last child view in the childNodes array. If the view does not have any children,
316 this property will be null.
317
318 @field
319 @type SC.View
320 */
321 lastChild: null,
322
323 /**
324 The next sibling view in the childNodes array of the receivers parentNode.
325 If the receiver is the last view in the array or if the receiver does not
326 belong to a parent view this property will be null.
327
328 @field
329 @type SC.View
330 */
331 nextSibling: null,
332
333 /**
334 The previous sibling view in the childNodes array of the receivers
335 parentNode. If the receiver is the first view in the array or if the
336 receiver does not belong to a parent view this property will be null.
337
338 @field
339 @type SC.View
340 */
341 previousSibling: null,
342
343 /**
344 The parent view this view belongs to. If the receiver does not belong to a parent view
345 then this property is null.
346
347 @field
348 @type SC.View
349 */
350 parentNode: null,
351
352
353 /**
354 The pane this view belongs to. The pane is the root of the responder
355 chain that this view belongs to. Typically a view's pane will be the
356 SC.window object. However, if you have added the view to a dialog, panel,
357 popup or other pane, this property will point to that pane instead.
358
359 If the view does not belong to a parentNode or if the view is not
360 onscreen, this property will be null.
361
362 @field
363 @type SC.View
364 */
365 pane: function()
366 {
367 var view = this;
368 while(view = view.get('parentNode'))
369 {
370 if (view.get('isPane') ) break;
371 }
372 return view;
373 }.property(),
374
375
376 /**
377 Removes all child views from the receiver.
378
379 @returns {void}
380 */
381 clear: function() {
382 while(this.firstChild) this.removeChild(this.firstChild) ;
383 },
384
385 /**
386 This method is called on the view just before it is added to a new parent
387 view.
388
389 You can override this method to do any setup you need on your view or to
390 reset any cached values that are impacted by being added to a view. The
391 default implementation does nothing.
392
393 @param parent {SC.View} the new parent
394 @paran beforeView {SC.View} the view in the parent's childNodes array that
395 will follow this view once it is added. If the view is being added to
396 the end of the array, this will be null.
397 @returns {void}
398 */
399 willAddToParent: function(parent, beforeView) {},
400
401 /**
402 This method is called on the view just after it is added to a new parent
403 view.
404
405 You can override this method to do any setup you need on your view or to
406 reset any cached values that are impacted by being added to a view. The
407 default implementation does nothing.
408
409 @param parent {SC.View} the new parent
410 @paran beforeView {SC.View} the view in the parent's childNodes array that
411 will follow this view once it is added. If the view is being added to
412 the end of the array, this will be null.
413 @returns {void}
414 */
415 didAddToParent: function(parent, beforeView) {},
416
417 /**
418 This method is called on the view just before it is removed from a parent
419 view.
420
421 You can override this method to clear out any values that depend on the
422 view belonging to the current parentNode. The default implementation does
423 nothing.
424
425 @returns {void}
426 */
427 willRemoveFromParent: function() {},
428
429 /**
430 This method is called on the view just after it is removed from a parent
431 view.
432
433 You can override this method to clear out any values that depend on the
434 view belonging to the current parentNode. The default implementation does
435 nothing.
436
437 @param oldParent {SC.View} the old parent view
438 @returns {void}
439 */
440 didRemoveFromParent: function(oldParent) {},
441
442 /**
443 This method is called just before a new child view is added to the
444 receiver's childNodes array. You can use this to prepare for any layout
445 or other cleanup you might need to do.
446
447 The default implementation does nothing.
448
449 @param child {SC.View} the view to be added
450 @param beforeView {SC.View} and existing child view that will follow the
451 child view in the array once it is added. If adding to the end of the
452 array, this param will be null.
453 @returns {void}
454 */
455 willAddChild: function(child, beforeView) {},
456
457 /**
458 This method is called just after a new child view is added to the
459 receiver's childNodes array. You can use this to prepare for any layout
460 or other cleanup you might need to do.
461
462 The default implementation does nothing.
463
464 @param child {SC.View} the view that was added
465 @param beforeView {SC.View} and existing child view that will follow the
466 child view in the array once it is added. If adding to the end of the
467 array, this param will be null.
468 @returns {void}
469 */
470 didAddChild: function(child, beforeView) {},
471
472 /**
473 This method is called just before a child view is removed from the
474 receiver's childNodes array. You can use this to prepare for any layout
475 or other cleanup you might need to do.
476
477 The default implementation does nothing.
478
479 @param child {SC.View} the view to be removed
480 @returns {void}
481 */
482 willRemoveChild: function(child) {},
483
484 /**
485 This method is called just after a child view is removed from the
486 receiver's childNodes array. You can use this to prepare for any layout
487 or other cleanup you might need to do.
488
489 The default implementation does nothing.
490
491 @param child {SC.View} the view that was removed
492 @returns {void}
493 */
494 didRemoveChild: function(child) {},
495
496
497 nextKeyView: null,
498 previousKeyView: null,
499
500 nextValidKeyView: function()
501 {
502 var view = this;
503 while (view = view.get('nextKeyView'))
504 {
505 if (view.get('isVisible') && view.get('acceptsFirstResponder')) {
506 return view;
507 }
508 }
509 return null;
510 },
511
512 previousValidKeyView: function()
513 {
514 var view = this;
515 while (view = view.get('previousKeyView'))
516 {
517 if (view.get('isVisible') && view.get('acceptsFirstResponder')) {
518 return view;
519 }
520 }
521 return null;
522 },
523
524 /** @private
525 Invoked whenever the child hierarchy changes and any internally cached
526 values might need to be recalculated.
527 */
528 _flushInternalCaches: function() {
529 // only flush cache for parent if this item was cached since the top level
530 // cached can only be populated if this one is populated also...
531 if ((this._needsClippingFrame != null) || (this._needsFrameChanges != null)) {
532 this._needsClippingFrame = this._needsFrameChanges = null ;
533 if (this.parentNode) this.parentNode._flushInternalCaches() ;
534 }
535 },
536
537 // ..........................................
538 // SC.Responder implementation
539 //
540
541 nextResponder: function()
542 {
543 return this.parentNode;
544 }.property('parentNode'),
545
546 // recursively travels down the view hierarchy looking for a view that returns true to performKeyEquivalent
547 performKeyEquivalent: function(keystring, evt)
548 {
549 var child = this.get('firstChild');
550 while (child)
551 {
552 if (child.performKeyEquivalent(keystring, evt)) return true;
553 child = child.get('nextSibling');
554 }
555 return false;
556 },
557
558 // ..........................................
559 // ELEMENT API
560 //
561
562 /**
563 An array of currently applied classNames.
564
565 @field
566 @type {Array}
567 @param value {Array} Array of class names to apply to the element
568 */
569 classNames: function(key, value) {
570 if (value !== undefined) {
571 value = Array.from(value) ;
572 if (this.rootElement) this.rootElement.className = value.join(' ') ;
573 this._classNames = value.slice() ;
574 }
575
576 if (!this._classNames) {
577 var classNames = this.rootElement.className;
578 this._classNames = (classNames && classNames.length > 0) ? classNames.split(' ') : [] ;
579 }
580 return this._classNames ;
581 }.property(),
582
583 /**
584 Detects the presence of the class name on the root element.
585
586 @param className {String} the class name
587 @returns {Boolean} YES if class name is currently applied, NO otherwise
588 */
589 hasClassName: function(className) {
590 return (this._classNames || this.get('classNames')).indexOf(className) >= 0 ;
591 },
592
593 /**
594 Adds the class name to the element.
595
596 @param className {String} the class name to add.
597 @returns {String} the class name
598 */
599 addClassName: function(className) {
600 if (this.hasClassName(className)) return ; // nothing to do
601
602 var classNames = this._classNames || this.get('classNames') ;
603 classNames.push(className) ;
604 this.set('classNames', classNames) ;
605 return className ;
606 },
607
608 /**
609 Removes the specified class name from the element.
610
611 @param className {String} the class name to remove
612 @returns {String} the class name
613 */
614 removeClassName: function(className) {
615 if (!this.hasClassName(className)) return ; // nothing to do
616
617 var classNames = this._classNames || this.get('classNames') ;
618 classNames = this._classNames = classNames.without(className) ;
619 this.set('classNames', classNames) ;
620 return className ;
621 },
622
623 /**
624 Adds or removes the class name according to flag.
625
626 This is a simple way to add or remove a class from the root element.
627
628 @param className {String} the class name
629 @param flag {Boolean} YES to add class name, NO to remove it.
630 @returns {String} The class Name.
631 */
632 setClassName: function(className, flag) {
633 return (!!flag) ? this.addClassName(className) : this.removeClassName(className);
634 },
635
636 /**
637 Toggles the presence of the class name.
638
639 If the specified CSS class is applied, it will be removed. If it is not
640 present, it will be added. Note that if this changes the potential
641 layout of the view, you must wrap calls to this in viewFrameDidChange()
642 and viewFrameWillChange().
643
644 @param className {String} the class name
645 @returns {Boolean} YES if classname is now applied
646 */
647 toggleClassName: function(className) {
648 return this.setClassName(className, !this.hasClassName(className)) ;
649 },
650
651 /**
652 Retrieves the current value of the named CSS style.
653
654 This method is designed to work cross platform and uses the current
655 computed style, which is the combination of all applied CSS class names
656 and inline styles.
657
658 @param style {String} the style key.
659 @returns {Object} the style value or null if not-applied/auto
660 */
661 getStyle: function(style) {
662 var element = this.rootElement ;
663 if (!this._computedStyle) {
664 this._computedStyle = document.defaultView.getComputedStyle(element, null) ;
665 }
666
667 //if (style == 'float') style = 'cssFloat' ;
668 style = (style === 'float') ? 'cssFloat' : style.camelize() ;
669 var value = element.style[style];
670 if (!value) {
671 value = this._computedStyle ? this._computedStyle[style] : null ;
672 }
673
674 if (style === 'opacity') {
675 value = value ? parseFloat(value) : 1.0;
676 }
677 if (value === 'auto') value = null ;
678
679 return value ;
680 },
681
682
683 /**
684 Sets the passed hash of CSS styles and values on the element. You should
685 pass your properties pre-camelized.
686
687 @param styles {Hash} hash of keys and values
688 @param camelized {Boolean} optional bool set to NO if you did not camelize.
689 @returns {Boolean} YES if set succeeded.
690 */
691 setStyle: function(styles, camelized) {
692 return Element.setStyle(this.rootElement, styles, camelized) ;
693 },
694
695/**
696 Updates the HTML of an element.
697
698 This method takes care of nasties like processing scripts and inserting
699 HTML into a table. It is also somewhat slow. If you control the HTML
700 being inserted and you are not working with table elements, you should use
701 the innerHTML property instead. If you are setting content generated by
702 users, this method can insert the content safely.
703
704 @param html {String} the html to insert.
705*/
706 update: function(html) {
707 Element.update((this.containerElement || this.rootElement),html) ;
708 this.propertyDidChange('innerHTML') ;
709 },
710
711 /**
712 Retrieves the value for an attribute on the DOM element
713
714 @param attrName {String} the attribute name
715 @returns {String} attribute value
716 */
717 getAttribute: function(attrName) {
718 return Element.readAttribute(this.rootElement,attrName) ;
719 },
720
721 /**
722 Sets an attribute on the root DOM element.
723
724 @param attrName {String} the attribute name
725 @param value {String} the new attribute value
726 @returns {String} the set attribute name
727 */
728 setAttribute: function(attrName, value) {
729 this.rootElement.setAttribute(attrName, value) ;
730 },
731
732 /**
733 Returns true if the named attributes is defined on the views root element.
734
735 @param attrName {String} the attribute name
736 @returns {Boolean} YES if attribute is present.
737 */
738 hasAttribute: function(attrName) {
739 return Element.hasAttribute(this.rootElement, attrName) ;
740 },
741
742 // ..........................................
743 // STYLE API
744 //
745 // These properties can be used to directly manipulate various CSS
746 // styles on the view. These properties are required for animation
747 // support. Values are typically assumed to be in px.
748
749 /**
750 SC.View's unknown property is used to implement a large class of
751 properties beginning with the the world "style". You can get or set
752 any of these properties to edit individual CSS style properties.
753 */
754 unknownProperty: function(key, value) {
755 if (key && key.match && key.match(/^style/)) {
756 key = key.slice(5,key.length).replace(/^./, function(x) {
757 return x.toLowerCase();
758 });
759
760 var ret = null ;
761
762 // handle dimensional properties
763 if (key.match(/height$|width$|top$|bottom$|left$|right$/i)) {
764 if (value !== undefined) {
765 this.viewFrameWillChange() ;
766 var props = {} ;
767 props[key] = (value) ? value + 'px' : 'auto' ;
768 this.setStyle(props) ;
769 this.viewFrameDidChange() ;
770 }
771 ret = this.getStyle(key) ;
772 ret = (ret === 'auto') ? null : Math.round(parseFloat(ret)) ;
773
774 // all other properties just pass through (and do not change frame)
775 } else {
776 if (value !== undefined) {
777 var props = {} ;
778 props[key] = value ;
779 this.setStyle(props) ;
780 }
781 ret = this.getStyle(key) ;
782 }
783 return ret;
784
785 } else return sc_super();
786 },
787
788 // ..........................................
789 // DOM API
790 //
791 // The methods in this section give you some low-level control over how the
792 // view interacts with the DOM. You do not normally need to work with this.
793
794 /**
795 This is the DOM element actually managed by this view. This will be set
796 by the view when it is created. You should rarely need to access this
797 property directly. When you do access it, you should only do so from
798 within methods you write on your SC.View subclasses, never from outside
799 the view.
800
801 Unlike most properties, you do not need to use get()/set() to access this
802 property. It is not currently safe to edit this property once the view
803 has been createde.
804
805 @field
806 @type {Element}
807 */
808 rootElement: null,
809
810 /**
811 Normally when you add child views to your view, their DOM elements will
812 be set as direct children of the root element. However you can
813 choose instead to designate an alertnative child node using this
814 property. Set this to a selector string to begin with. The first time
815 it is accessed, the view will convert it to an actual element. It is not
816 currently safe to edit this property once the view has been created.
817
818 Like rootElement, you should only access this property from within
819 methods you write on an SC.View subclass, never from outside the view.
820 Unlike most properties, it is not necessary to use get()/set().
821
822 @field
823 @type {Element}
824 */
825 containerElement: null,
826
827 // ..........................................
828 // VIEW LAYOUT
829 //
830 // The following methods can be used to implement automatic resizing.
831 // The frame and bounds provides a simple way for you to compute the
832 // location and size of your views. You can then use the automatic
833 // resizing.
834
835 /**
836 Returns true if the view or any of its contained views implement the
837 clippingFrameDidChange method.
838
839 If this property returns false, then notifications about changes to the
840 clippingFrame will probably not be called on the receiver. Normally if
841 you do not need to worry about this property since implementing the
842 clippingFrameDidChange() method will change its value and cause your
843 method to be invoked.
844
845 This property is automatically updated whenever you add or remove a child
846 view.
847 */
848 needsClippingFrame: function() {
849 if (this._needsClippingFrame == null) {
850 var ret = this.clippingFrameDidChange != SC.View.prototype.clippingFrameDidChange;
851 var view = this.get('firstChild') ;
852 while(!ret && view) {
853 ret = view.get('needsClippingFrame') ;
854 view = view.get('nextSibling') ;
855 }
856 this._needsClippingFrame = ret ;
857 }
858 return this._needsClippingFrame ;
859 }.property(),
860
861 /**
862 Returns true if the view or any of its contained views implements any
863 resize methods.
864
865 If this property returns false, changes to your frame view may not be
866 relayed to child methods. This may mean that your various frame
867 properties could become stale unless you call refreshFrames() first.
868
869 If you want you make sure your frames are up to date, see hasManualLayout.
870
871 This property is automatically updated whenever you add or remove a child
872 view. It returns true if you implement any of the resize methods or if
873 hasManualLayout is true.
874 */
875 needsFrameChanges: function() {
876 if (this._needsFrameChanges == null) {
877 var ret = this.get('needsClippingFrame') || this.get('hasManualLayout') ;
878 var view = this.get('firstChild') ;
879 while(!ret && view) {
880 ret = view.get('needsFrameChanges') ;
881 view = view.get('nextSibling') ;
882 }
883 this._needsFrameChanges = ret ;
884 }
885 return this._needsFrameChanges ;
886 }.property(),
887
888
889 /**
890 Returns true if the receiver manages the layout for itself or its
891 children.
892
893 Normally this property returns true automatically if you implement
894 resizeChildrenWithOldSize() or resizeWithOldParentSize() or
895 clippingFrameDidChange().
896
897 If you do not implement these methods but need to make sure your frame is
898 always up-to-date anyway, set this property to true.
899 */
900 hasManualLayout: function() {
901 return (this.resizeChildrenWithOldSize != SC.View.prototype.resizeChildrenWithOldSize) ||
902 (this.resizeWithOldParentSize != SC.View.prototype.resizeWithOldParentSize) ||
903 (this.clippingFrameDidChange != SC.View.prototype.clippingFrameDidChange) ;
904 }.property(),
905
906 /**
907 Convert a point _from_ the offset parent of the passed view to the current
908 view.
909
910 This is a useful utility for converting points in the coordinate system of
911 another view to the coordinate system of the receiver. Pass null for
912 targetView to convert a point from a window offset. This is the inverse
913 of convertFrameToView().
914
915 Note that if your view is not visible on the screen, this may not work.
916
917 @param {Point} f The point or frame to convert
918 @param {SC.View} targetView The view to convert from. Pass null to convert from window coordinates.
919
920 @returns {Point} The converted point or frame
921 */
922 convertFrameFromView: function(f, targetView) {
923
924 // first, convert to root level offset.
925 var thisOffset = SC.viewportOffset(this.get('offsetParent')) ;
926 var thatOffset = (targetView) ? SC.viewportOffset(targetView.get('offsetParent')) : SC.ZERO_POINT;
927
928 // now get adjustment.
929 var adjustX = thatOffset.x - thisOffset.x ;
930 var adjustY = thatOffset.y - thisOffset.y ;
931 return { x: (f.x + adjustX), y: (f.y + adjustY), width: f.width, height: f.height };
932 },
933
934 /**
935 Convert a point _to_ the offset parent of the passed view from the current
936 view.
937
938 This is a useful utility for converting points in the coordinate system of
939 the receiver to the coordinate system of another view. Pass null for
940 targetView to convert a point to a window offset. This is the inverse of
941 convertFrameFromView().
942
943 Note that if your view is not visible on the screen, this may not work.
944
945 @param {Point} f The point or frame to convert
946 @param {SC.View} targetView The view to convert to. Pass null to convert to window coordinates.
947
948 @returns {Point} The converted point or frame
949 */
950 convertFrameToView: function(f, sourceView) {
951 // first, convert to root level offset.
952 var thisOffset = SC.viewportOffset(this.get('offsetParent')) ;
953 var thatOffset = (sourceView) ? SC.viewportOffset(sourceView.get('offsetParent')) : SC.ZERO_POINT ;
954
955 // now get adjustment.
956 var adjustX = thisOffset.x - thatOffset.x ;
957 var adjustY = thisOffset.y - thatOffset.y ;
958 return { x: (f.x + adjustX), y: (f.y + adjustY), width: f.width, height: f.height };
959 },
960
961 /**
962 This property returns a DOM ELEMENT that is the offset parent for
963 this view's frame coordinates. Depending on your CSS, this parent
964 may or may not match with the parent view.
965
966 @example
967 offsetView = $view(this.get('offsetParent')) ;
968
969 @field
970 @type {Element}
971 */
972 offsetParent: function() {
973
974 // handle simple cases.
975 var el = this.rootElement ;
976 if (!el || el === document.body) return el;
977 if (el.offsetParent) return el.offsetParent ;
978
979 // in some cases, we can't find the offset parent so we walk up the
980 // chain until an element is found with a position other than
981 // 'static'
982 //
983 // Note that IE places DOM elements not in the main body inside of a
984 // document-fragment root. We need to treat document-fragments (i.e.
985 // nodeType === 11) as null values
986 var ret = null ;
987 while(!ret && (el = el.parentNode) && (el.nodeType !== 11) && (el !== document.body)) {
988 if (Element.getStyle(el, 'position') !== 'static') ret = el;
989 }
990 if (!ret && (el === document.body)) ret = el ;
991 return ret ;
992 }.property(),
993
994 /**
995 The inner bounds for the content shown inside of this frame. Reflects
996 scroll position and other properties.
997
998 The inner frame returns the actual available frame for child elements,
999 less any borders or scroll bars.
1000
1001 This value can change when:
1002 - the receiver's frame changes
1003 - the receiver's child views change, adding or removing scrollbars
1004 - You can the CSS or applied style that effects the borders or scrollbar visibility
1005 */
1006 innerFrame: function(key, value) {
1007
1008 var f ;
1009 if (this._innerFrame == null) {
1010
1011 // get the base frame
1012 // The _collectInnerFrame function is set at the bottom of this file
1013 // based on the browser type.
1014 var el = this.rootElement ;
1015 f = this._collectFrame(SC.View._collectInnerFrame) ;
1016
1017 // bizarely for FireFox if your offsetParent has a border, then it can
1018 // impact the offset
1019 if (SC.Platform.Firefox) {
1020 var parent = el.offsetParent ;
1021 var overflow = (parent) ? Element.getStyle(parent, 'overflow') : 'visible' ;
1022 if (overflow && overflow !== 'visible') {
1023 var left = parseInt(Element.getStyle(parent, 'borderLeftWidth'),0) || 0 ;
1024 var top = parseInt(Element.getStyle(parent, 'borderTopWidth'),0) || 0 ;
1025 f.x += left; f.y += top ;
1026 }
1027 }
1028
1029 // fix the x & y with the clientTop/clientLeft
1030 var clientLeft, clientTop ;
1031
1032 if (SC.Platform.IE) {
1033 if (!el.width) {
1034 clientLeft = parseInt(this.getStyle('border-left-width'),0) || 0 ;
1035 } else clientLeft = el.clientLeft ;
1036
1037 if (!el.height) {
1038 clientTop = parseInt(this.getStyle('border-top-width'),0) || 0 ;
1039 } else clientTop = el.clientTop ;
1040
1041 f.x += clientLeft; f.y += clientTop;
1042 }
1043 else {
1044 if (el.clientLeft == null) {
1045 clientLeft = parseInt(this.getStyle('border-left-width'),0) || 0 ;
1046 } else clientLeft = el.clientLeft ;
1047
1048 if (el.clientTop == null) {
1049 clientTop = parseInt(this.getStyle('border-top-width'),0) || 0 ;
1050 } else clientTop = el.clientTop ;
1051
1052 f.x += clientLeft; f.y += clientTop;
1053 }
1054
1055 // cache this frame if using manual layout mode
1056 this._innerFrame = SC.cloneRect(f);
1057 } else f = SC.cloneRect(this._innerFrame) ;
1058 // console.log('returning x:%@ y:%@ w:%@ h:%@'.fmt(f.x, f.y, f.width, f.height));
1059 return f ;
1060 }.property('frame'),
1061
1062 layout: null, // the default layout
1063
1064 // controls how the frame method positions the view (16 different possibilities)
1065 autoresize: SC.AUTORESIZE_BORDERS,
1066
1067 preferredSize: { width: 150, height: 100 }, // the default preferred size
1068
1069 // preferredLayout: fuction() {
1070 // var parent = this.parentNode;
1071 // while (parent) this.parentNode;
1072 //
1073 // }.property();
1074
1075 page: function(key, value) {
1076 if (value !== undefined) {
1077 this._page = value;
1078 }
1079 else {
1080 var page = this._page;
1081 if (!page && this.parentNode ) page = this.parentNode.get('page');
1082 return page;
1083 }
1084 }.property(),
1085
1086 parentLayout: function() {
1087 if ( !this._parentLayout || this.didChangeFor('parentNode', 'page')) {
1088 var parentLayout = this.parentNode ? this.parentNode.get('layout') : null;
1089 if (!parentLayout) {
1090 var page = this.get('page');
1091 if (page) parentLayout = page.get('frame');
1092 }
1093 this._parentLayout = parentLayout;
1094 }
1095 return this._parentLayout;
1096 }.property('parentNode', 'page'),
1097
1098 layoutSubviews: function() {
1099 var ary = this.get('childNodes');
1100 for (var i = 0, e = ary.length; i < e; ++i ) {
1101 var child = ary[i];
1102 child.set('frame', child.get('layout'));
1103 child.layoutSubviews();
1104 }
1105 },
1106
1107 isView: true,
1108
1109 /**
1110 The outside bounds of your view, offset top/left from its offsetParent
1111
1112 The frame rect is the area actually occupied by a view including any
1113 borders or padding, but excluding margins.
1114
1115 The frame is calculated and cached the first time you get it. Afer that,
1116 the frame cache should automatically update when you make changes that
1117 will effect the view frames unless you change the frame indirectly, such
1118 as through changing CSS classes or by-passing the view to edit the DOM.
1119
1120 If you make a change like this, be sure to wrap the code that makes this
1121 change with calls to viewFrameWillChange() and viewFrameDidChange() on the
1122 highest-level view that will be impacted by the change. Calling this
1123 method will automatically update child frames as well.
1124
1125 When you set the frame property, it will update the left, top, height,
1126 and width CSS attributes on the element. Since the height and width in
1127 the frame rect includes borders and padding, the view will automatically
1128 adjust the height and width CSS it sets to account for this.
1129
1130 If you would prefer to edit the CSS attributes for the frame directly
1131 instead, you can do so by using the styleTop, styleLeft, styleRight,
1132 styleBottom, styleWidth, and styleHeight properties on the view. These
1133 properties will update the CSS attributes and call viewFrameDidChange()/
1134 viewFrameWillChange().
1135
1136 @field
1137 */
1138 frame: function(key, value) {
1139 if (value !== undefined) {
1140 this._frame = value;
1141 if ( value && this.didChangeFor('autoresize', 'layout') ) {
1142 var style = { position: 'absolute', padding: '0px', border: '0px', display: 'block' };
1143 var autoresize = this.get('autoresize');
1144 var f = this.get('layout') || value; // value used for computed frames
1145 var pf = this.get('parentLayout');
1146
1147 if (!pf) {
1148 console.log("SC.View#set('frame', %@) called in parent-less view.".fmt($I(value)));
1149 return; // not visible, nothing to do...
1150 }
1151
1152 // okay, actually change the frame...
1153 this.viewFrameWillChange();
1154
1155 console.log('updating positioning for ' + this);
1156 console.log('autoresize: ' + autoresize);
1157 console.log('frame is %@'.fmt($I(f)));
1158 console.log('parentFrame is %@'.fmt($I(pf)));
1159
1160 switch (autoresize) {
1161 case SC.AUTORESIZE_WIDTH: // fixed left and right, variable top and bottom
1162 style.left = f.x + 'px';
1163 style.right = (pf.width - (f.x + f.width)) + 'px';
1164 style.height = f.height + 'px';
1165 style.top = '50%';
1166 style.marginTop = (0 - f.height/2) + 'px';
1167 break;
1168 case SC.AUTORESIZE_RIGHT: // fixed width and left, variable top and bottom
1169 style.width = f.width + 'px';
1170 style.left = f.x + 'px';
1171 style.top = '50%';
1172 style.marginTop = (0 - f.height/2) + 'px';
1173 break;
1174 case SC.AUTORESIZE_LEFT: // fixed width and right, variable top and bottom
1175 style.width = f.width + 'px';
1176 style.right = (pf.width - (f.x + f.width)) + 'px';
1177 style.top = '50%';
1178 style.marginTop = (0 - f.height/2) + 'px';
1179 break;
1180 case SC.AUTORESIZE_HEIGHT: // variable left and right, fixed top and bottom
1181 style.width = f.width + 'px';
1182 style.left = '50%';
1183 style.marginLeft = (0 - f.width/2) + 'px';
1184 style.top = f.y + 'px';
1185 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1186 break;
1187 case SC.AUTORESIZE_TOP: // variable left and right, fixed height and bottom
1188 style.width = f.width + 'px';
1189 style.left = '50%';
1190 style.marginLeft = (0 - f.width/2) + 'px';
1191 style.height = f.height + 'px';
1192 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1193 break;
1194 case SC.AUTORESIZE_BOTTOM: // variable left and right, fixed height and top
1195 style.width = f.width + 'px';
1196 style.left = '50%';
1197 style.marginLeft = (0 - f.width/2) + 'px';
1198 style.height = f.height + 'px';
1199 style.top = f.y + 'px';
1200 break;
1201 case SC.AUTORESIZE_WIDTH_HEIGHT: // fixed left, right, top and bottom
1202 style.left = f.x + 'px';
1203 style.right = (pf.width - (f.x + f.width)) + 'px';
1204 style.top = f.y + 'px';
1205 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1206 break;
1207 case SC.AUTORESIZE_WIDTH_TOP: // fixed left and right, fixed height and bottom
1208 style.left = f.x + 'px';
1209 style.right = (pf.width - (f.x + f.width)) + 'px';
1210 style.height = f.height + 'px';
1211 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1212 break;
1213 case SC.AUTORESIZE_WIDTH_BOTTOM: // fixed left and right, fixed height and top
1214 style.left = f.x + 'px';
1215 style.right = (pf.width - (f.x + f.width)) + 'px';
1216 style.height = f.height + 'px';
1217 style.top = f.y + 'px';
1218 break;
1219 case SC.AUTORESIZE_RIGHT_HEIGHT: // fixed width and left, fixed top and bottom
1220 style.width = f.width + 'px';
1221 style.left = f.x + 'px';
1222 style.top = f.y + 'px';
1223 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1224 break;
1225 case SC.AUTORESIZE_RIGHT_TOP: // fixed width and left, fixed height and bottom
1226 style.width = f.width + 'px';
1227 style.left = f.x + 'px';
1228 style.height = f.height + 'px';
1229 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1230 break;
1231 case SC.AUTORESIZE_RIGHT_BOTTOM: // fixed width and left, fixed height and top
1232 style.width = f.width + 'px';
1233 style.left = f.x + 'px';
1234 style.height = f.height + 'px';
1235 style.top = f.y + 'px';
1236 break;
1237 case SC.AUTORESIZE_LEFT_TOP: // fixed width and right, fixed height and bottom
1238 style.width = f.width + 'px';
1239 style.right = (pf.width - (f.x + f.width)) + 'px';
1240 style.height = f.height + 'px';
1241 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1242 break;
1243 case SC.AUTORESIZE_LEFT_BOTTOM: // fixed width and right, fixed height and top
1244 style.width = f.width + 'px';
1245 style.right = (pf.width - (f.x + f.width)) + 'px';
1246 style.height = f.height + 'px';
1247 style.top = f.y + 'px';
1248 break;
1249 case SC.AUTORESIZE_LEFT_HEIGHT: // fixed width and right, fixed top and bottom
1250 style.width = f.width + 'px';
1251 style.right = (pf.width - (f.x + f.width)) + 'px';
1252 style.top = f.y + 'px';
1253 style.bottom = (pf.height - (f.y + f.height)) + 'px';;
1254 break;
1255 case SC.AUTORESIZE_BORDERS: // fixed width and height, variable top, bottom, left, and right
1256 style.width = f.width + 'px';
1257 style.left = '50%';
1258 style.marginLeft = (0 - f.width/2) + 'px';
1259 style.height = f.height + 'px';
1260 style.top = '50%';
1261 style.marginTop = (0 - f.height/2) + 'px';
1262 break;
1263 default:
1264 console.log('missing or invalid autoresize value; using SC.AUTORESIZE_BORDERS');
1265 style.width = f.width + 'px';
1266 style.left = '50%';
1267 style.marginLeft = (0 - f.width/2) + 'px';
1268 style.height = f.height + 'px';
1269 style.top = '50%';
1270 style.marginTop = (0 - f.height/2) + 'px';
1271 }
1272
1273 console.log('style is %@'.fmt($I(style)));
1274
1275 // now apply style change and clear the cached frame
1276 this.setStyle(style);
1277
1278 // notify for a resize only.
1279 this.viewFrameDidChange();
1280 }
1281 }
1282 else {
1283 var f = this._frame;
1284 return f ? SC.cloneRect(f) : { x: 0, y: 0, width: 100, height: 150 };
1285 }
1286 }.property('autoresize', 'layout'),
1287
1288 /**
1289 The current frame size.
1290
1291 This property will actually return the same value as the frame property,
1292 however setting this property will set only the frame size and ignore any
1293 origin you might pass.
1294
1295 @field
1296 */
1297 size: function(key, value) {
1298 if (value !== undefined) {
1299 this.set('frame',{ width: value.width, height: value.height }) ;
1300 }
1301 return this.get('frame') ;
1302 }.property('frame'),
1303
1304 /**
1305 The current frame origin.
1306
1307 This property will actually return the same value as the frame property,
1308 however setting this property will set only the frame origin and ignore
1309 any size you might pass.
1310
1311 @field
1312 */
1313 origin: function(key, value) {
1314 if (value !== undefined) {
1315 this.set('frame',{ x: value.x, y: value.y }) ;
1316 }
1317 return this.get('frame') ;
1318 }.property('frame'),
1319
1320 /**
1321 Call this method before you make a change that will impact the frame of
1322 the view such as changing the border thickness or adding/removing a CSS
1323 style.
1324
1325 Once you finish making your changes, be sure to call viewFrameDidChange()
1326 as well. This will deliver any relevant resizing and other notifications.
1327 It is safe to nest multiple calls to this method.
1328
1329 This method is called automatically anytime you set the frame.
1330
1331 @returns {void}
1332 */
1333 viewFrameWillChange: function() {
1334 if (this._frameChangeLevel++ <= 0) {
1335 this._frameChangeLevel = 1 ;
1336
1337 // save frame information if view has manual layout.
1338 if (this.get('needsFrameChanges')) {
1339 this._cachedFrames = this.getEach('innerFrame', 'clippingFrame', 'frame') ;
1340 } else this._cachedFrames = null ;
1341 this.beginPropertyChanges(); // suspend change notifications
1342 }
1343 },
1344
1345 /**
1346 Call this method just after you finish making changes that will impace the
1347 frame of the view such as changing the border thickness or adding/removing
1348 a CSS style.
1349
1350 It is safe to next multiple calls to this method. This method is called
1351 automatically anytime you set the frame.
1352
1353 @returns {void}
1354 */
1355 viewFrameDidChange: function(force) {
1356
1357 // clear the frame caches
1358 this.recacheFrames() ;
1359
1360 // if this is a top-level call then also deliver notifications as needed.
1361 if (--this._frameChangeLevel <= 0) {
1362 this._frameChangeLevel = 0 ;
1363 if (this._cachedFrames) {
1364 var newFrames = this.getEach('innerFrame', 'clippingFrame') ;
1365
1366 // notify if clippingFrame has changed and clippingFrameDidChange is
1367 // implemented.
1368 var nf = newFrames[1]; var of = this._cachedFrames[1] ;
1369 if (force || (nf.width != of.width) || (nf.height != of.height)) {
1370 this._invalidateClippingFrame() ;
1371 }
1372
1373 // notify children if the size of the innerFrame has changed.
1374 var nf = newFrames[0]; var of = this._cachedFrames[0] ;
1375 if (force || (nf.width != of.width) || (nf.height != of.height)) {
1376 this.resizeChildrenWithOldSize(this._cachedFrames.last()) ;
1377 }
1378
1379 // clear parent scrollFrame if needed
1380 var parent = this.parentNode ;
1381 while (parent && parent != SC.window) {
1382 if (parent._scrollFrame) parent._scrollFrame = null ;
1383 parent = parent.parentNode ;
1384 }
1385
1386 this.notifyPropertyChange('frame') ; // trigger notifications.
1387 }
1388
1389 // allow notifications again
1390 this.endPropertyChanges() ;
1391 }
1392 },
1393
1394
1395 /**
1396 Clears any cached frames so the next get will recompute them.
1397
1398 This method does not notify any observers of changes to the frames. It
1399 should only be used when you need to make sure your frame info is up to
1400 date but you do not expect anything to have happened that frame observers
1401 would be interested in.
1402 */
1403 recacheFrames: function() {
1404 this._innerFrame = this._frame = this._clippingFrame = this._scrollFrame = null ;
1405 },
1406
1407 /**
1408 Set to true if you expect this view to have scrollable content.
1409
1410 Normally views do not monitor their onscroll event. If you set this
1411 property to true, however, the view will observe its onscroll event and
1412 update its scrollFrame and clippedFrame.
1413
1414 This will also register the view as a scrollable area that can be
1415 auto-scrolled during a drag/drop event.
1416 */
1417 isScrollable: false,
1418
1419 /**
1420 The frame used to control scrolling of content.
1421
1422 x,y => offset from the innerFrame root.
1423 width,height => total size of the frame
1424
1425 If the frame does not have scrollable content, then the size will be equal
1426 to the innerFrame size.
1427
1428 This frame changes when:
1429 - the receiver's innerFrame changes
1430 - the scroll location is changed programatically
1431 - the size of child views changes
1432 - the user scrolls the view
1433
1434 @field
1435 */
1436 scrollFrame: function(key, value) {
1437
1438 // if value was passed, update the scroll x,y only.
1439 if (value != undefined) {
1440 var el = this.rootElement ;
1441 if (value.x != null) el.scrollLeft = 0-value.x ;
1442 if (value.y != null) el.scrollTop = 0-value.y ;
1443 this._scrollFrame = null ;
1444 this._invalidateClippingFrame() ;
1445 }
1446
1447 // build frame. We can use a cached version but only
1448 var f;
1449 if (this._scrollFrame == null) {
1450 var el = this.rootElement ;
1451 var func;
1452 if (SC.isIE()) {
1453 func = function() {
1454 var borderTopWidth = 0;
1455 var borderBottomWidth = 0;
1456 var borderLeftWidth = 0;
1457 var borderRightWidth = 0;
1458
1459 var overflow = el.currentStyle.overflow;
1460 if ( overflow != 'hidden' && overflow != 'auto' ) {
1461 borderTopWidth = parseInt(el.currentStyle.borderTopWidth, 0) || 0 ;
1462 borderBottomWidth = parseInt(el.currentStyle.borderBottomWidth, 0) || 0 ;
1463 borderLeftWidth = parseInt(el.currentStyle.borderLeftWidth, 0) || 0 ;
1464 borderRightWidth = parseInt(el.currentStyle.borderRightWidth, 0) || 0 ;
1465 }
1466 return {
1467 x: 0 - el.scrollLeft,
1468 y: 0 - el.scrollTop,
1469 width: el.scrollWidth + borderLeftWidth + borderRightWidth,
1470 height: Math.max(el.scrollHeight, el.clientHeight) + borderTopWidth + borderBottomWidth
1471 };
1472 };
1473 }
1474 else {
1475 func = function() {
1476 return {
1477 x: 0 - el.scrollLeft,
1478 y: 0 - el.scrollTop,
1479 width: el.scrollWidth,
1480 height: el.scrollHeight
1481 };
1482 };
1483 }
1484 f = this._collectFrame(func);
1485
1486 // cache this frame if using manual layout mode
1487 this._scrollFrame = SC.cloneRect(f);
1488 } else f = SC.cloneRect(this._scrollFrame) ;
1489
1490 // finally return the frame.
1491 return f ;
1492 }.property('frame'),
1493
1494 /**
1495 The visible portion of the view.
1496
1497 Returns the subset of the receivers frame that is actually visible on
1498 screen. This frame is automatically updated whenever one of the following
1499 changes:
1500
1501 - A parent view is resized
1502 - A parent view's scrollFrame changes.
1503 - The receiver is moved or resized
1504 - The receiver or a parent view is added to or removed from the window.
1505
1506 @field
1507 */
1508 clippingFrame: function() {
1509 var f ;
1510 if (this._clippingFrame == null) {
1511
1512 //if (this instanceof SC.SplitView) debugger ;
1513
1514 // my clipping frame is usually my frame
1515 f = this.get('frame') ;
1516
1517 // scope to my parents clipping frame.
1518 if (this.parentNode) {
1519
1520 // use only the visible portion of the parent's innerFrame.
1521 var parent = this.parentNode ;
1522 var prect = SC.intersectRects(parent.get('clippingFrame'), parent.get('innerFrame'));
1523
1524 // convert the local view's coordinates
1525 prect = this.convertFrameFromView(prect, parent) ;
1526
1527 // if parent is scrollable, then adjust by scroll frame also.
1528 if (this.parentNode.get('isScrollable')) {
1529 var scrollFrame = this.get('scrollFrame') ;
1530 prect.x -= scrollFrame.x ;
1531 prect.y -= scrollFrame.y ;
1532 }
1533
1534 // blend with current frame
1535 f = SC.intersectRects(f, prect) ;
1536 } else {
1537 f.width = f.height = 0 ;
1538 }
1539
1540 this._clippingFrame = SC.cloneRect(f) ;
1541
1542 } else f = SC.cloneRect(this._clippingFrame) ;
1543 return f ;
1544 }.property('frame', 'scrollFrame'),
1545
1546 /**
1547 Called whenever the receivers clippingFrame has changed. You can override
1548 this method to perform partial rendering or other clippingFrame-dependent
1549 actions.
1550
1551 The default implementation does nothing (and may not even be called do to
1552 optimizations). Note that this is the preferred way to respond to changes
1553 in the clippingFrame of using an observer since this method is gauranteed
1554 to happen in the correct order. You can use observers and bindings as
1555 well if you wish to handle anything that need not be handled
1556 synchronously.
1557 */
1558 clippingFrameDidChange: function() {
1559
1560 },
1561
1562 /**
1563 Called whenever the view's innerFrame size changes. You can override this
1564 method to perform your own layout of your child views.
1565
1566 If you do not override this method, the view will assume you are using
1567 CSS to layout your child views. As an optimization the view may not
1568 always call this method if it determines that you have not overridden it.
1569
1570 This default version simply calls resizeWithOldParentSize() on all of its
1571 children.
1572
1573 @param oldSize {Size} The old frame size of the view.
1574 @returns {void}
1575 */
1576 resizeChildrenWithOldSize: function(oldSize) {
1577 var child = this.get('firstChild') ;
1578 while(child) {
1579 child.resizeWithOldParentSize(oldSize) ;
1580 child = child.get('nextSibling') ;
1581 }
1582 },
1583
1584 /**
1585 Called whenever the parent's innerFrame size has changed. You can
1586 override this method to change how your view responds to this change.
1587
1588 If you do not override this method, the view will assume you are using CSS
1589 to control your layout and it will simply relay the change information to
1590 your child views. As an optmization, the view may not always call this
1591 method if it determines that you have not overridden it.
1592
1593 @param oldSize {Size} The old frame size of the parent view.
1594 */
1595 resizeWithOldParentSize: function(oldSize) {
1596 this.viewFrameWillChange() ;
1597 this.viewFrameDidChange(YES) ;
1598 },
1599
1600 /** @private
1601 Handler for the onscroll event. Hooked in on init if isScrollable is
1602 true. Notify children that their clipping frame has changed.
1603 */
1604 _onscroll: function() {
1605 this._scrollFrame = null ;
1606 this.notifyPropertyChange('scrollFrame') ;
1607 SC.Benchmark.start('%@.onscroll'.fmt(this)) ;
1608 this._invalidateClippingFrame() ;
1609 SC.Benchmark.end('%@.onscroll'.fmt(this)) ;
1610 },
1611
1612 _frameChangeLevel: 0,
1613
1614 /** @private
1615 Used internally to collect client offset and location info. If the
1616 element is not in the main window or hidden, it will be added temporarily
1617 and then the passed function will be called.
1618 */
1619 _collectFrame: function(func) {
1620 var el = this.rootElement ;
1621
1622 // if not visible in window, move parent node into window and get
1623 // dim and offset. If the element has no parentNode, then just move
1624 // the element in.
1625 var isVisibleInWindow = this.get('isVisibleInWindow') ;
1626 if (!isVisibleInWindow) {
1627 var pn = el.parentNode || el ;
1628 if (pn === SC.window.rootElement) pn = el ;
1629
1630 var pnParent = pn.parentNode ; // cache former parent node
1631 var pnSib = pn.nextSibling ; // cache next sibling
1632 SC.window.rootElement.insertBefore(pn, null) ;
1633 }
1634
1635 // if view is not displayed, temporarily display it also
1636 var display = this.getStyle('display') ;
1637 var isHidden = !(display != 'none' && display != null) ;
1638
1639 // All *Width and *Height properties give 0 on elements with display none,
1640 // so enable the element temporarily
1641 if (isHidden) {
1642 var els = this.rootElement.style;
1643 var originalVisibility = els.visibility;
1644 var originalPosition = els.position;
1645 var originalDisplay = els.display;
1646 els.visibility = 'hidden';
1647 els.position = 'absolute';
1648 els.display = 'block';
1649 }
1650
1651 var ret = func.call(this) ;
1652
1653 if (isHidden) {
1654 els.display = originalDisplay;
1655 els.position = originalPosition;
1656 els.visibility = originalVisibility;
1657 }
1658
1659 if (!isVisibleInWindow) {
1660 if (pnParent) {
1661 pnParent.insertBefore(pn, pnSib) ;
1662 } else {
1663 if(pn.parentNode)
1664 SC.window.rootElement.removeChild(pn) ;
1665 }
1666 }
1667
1668 return ret;
1669 },
1670
1671 /** @private
1672 Called whenever some aspect of the receiver's frames have changed that
1673 probably has invalidated the child views clippingFrames. Events that cause
1674 this include:
1675
1676 - change to the innerFrame size
1677 - change to the scrollFrame
1678 - change to the clippingFrame
1679
1680 For performance reasons, this only passes onto children if they or a decendent
1681 implements the clippingFrameDidChange method.
1682 */
1683 _invalidateChildrenClippingFrames: function() {
1684 var view = this.get('firstChild') ;
1685 while(view) {
1686 view._invalidateClippingFrame() ;
1687 view = view.get('nextSibling') ;
1688 }
1689 },
1690
1691 /** @private
1692 Called by a parentNode whenever the clippingFrame needs to be recalculated.
1693 */
1694 _invalidateClippingFrame: function() {
1695 if (this.get('needsClippingFrame')) {
1696 this._clippingFrame = null ;
1697 this.clippingFrameDidChange() ;
1698 this.notifyPropertyChange('clippingFrame') ;
1699 this._invalidateChildrenClippingFrames() ;
1700 }
1701 },
1702
1703 // ..........................................
1704 // PROPERTIES
1705 //
1706
1707 /**
1708 Used to show or hide the view.
1709
1710 If this property is set to NO, then the DOM element will be hidden using
1711 display:none. You will often want to bind this property to some setting
1712 in your application to make various parts of your app visible as needed.
1713
1714 If you have animation enabled, then changing this property will actually
1715 trigger the animation to bring the view in or out.
1716
1717 The default binding format is SC.Binding.Bool
1718
1719 @field
1720 @type {Boolean}
1721 */
1722 isVisible: true,
1723
1724 /** @private */
1725 isVisibleBindingDefault: SC.Binding.Bool,
1726
1727 /**
1728 (Read Only) The current display visibility of the view.
1729
1730 Usually, this property will mirror the current state of the isVisible
1731 property. However, if your view animates its visibility in and out, then
1732 this will not become false until the animation completes.
1733
1734 @type {Boolean}
1735 */
1736 displayIsVisible: true,
1737
1738 /**
1739 true when the view is actually visible in the DOM window.
1740
1741 This property is set to true only when the view is (a) in the main DOM
1742 hierarchy and (b) all parent nodes are visible and (c) the receiver node
1743 is visible.
1744
1745 @type {Boolean}
1746 @field
1747 */
1748 isVisibleInWindow: NO,
1749
1750 /**
1751 If true, the tooltip will be localized. Also used by some subclasses.
1752
1753 @type {Boolean}
1754 @field
1755 */
1756 localize: false,
1757
1758 /**
1759 Applied to the title attribute of the rootElement DOM if set.
1760
1761 If localize is true, then the toolTip will be localized first.
1762
1763 @type {String}
1764 @field
1765 */
1766 toolTip: '',
1767
1768
1769 /**
1770 The HTML you want to use when creating a new element.
1771
1772 You can specify the HTML as a string of text, using the NodeDescriptor, or
1773 by pointing directly to an element.
1774
1775 Note that as an optimization, SC.View will actually convert the value of
1776 this property to an actual DOM structure the first time you create a view
1777 and then clone the DOM structure for future views.
1778
1779 This means that in general you should only set the value of emptyElement
1780 when you create a view subclass. Changing this property value at other
1781 times will often have no effect.
1782
1783 @field
1784 @type {String}
1785 */
1786 emptyElement: "<div></div>",
1787
1788 /**
1789 If true, view will display in a lightbox when you show it.
1790
1791 @field
1792 @type {Boolean}
1793 */
1794 isPanel: false,
1795
1796 /**
1797 If true, the view should be modal when shown as a panel.
1798
1799 @field
1800 @type {Boolean}
1801 */
1802 isModal: true,
1803
1804 /**
1805 Enable visible animation by default.
1806 */
1807 isAnimationEnabled: true,
1808
1809 /**
1810 General support for animation. Just call this method and it will build
1811 and play an animation starting from the current state. The second param
1812 is optional. It should either be a hash of animator options or an
1813 animator object returned by a previous call to transitionTo().
1814
1815 */
1816 transitionTo: function(target,animator,opts) {
1817 var animatorOptions = opts || {} ;
1818
1819 // Create or reset the animator.
1820 if (animator && !animator._isAnimator) {
1821 var finalStyle = animator ;
1822 if (!this.get("isAnimationEnabled")) {
1823 animatorOptions = Object.clone(animatorOptions) ;
1824 animatorOptions.duration = 1;
1825 }
1826 if (animatorOptions.duration) {
1827 animatorOptions.duration = parseInt(animatorOptions.duration,0) ;
1828 }
1829
1830 animator = Animator.apply(this.rootElement, finalStyle, animatorOptions);
1831 animator._isAnimator = true ;
1832 }
1833
1834 // trigger animation
1835 if (animator) {
1836 animator.jumpTo(animator.state) ;
1837 animator.seekTo(target) ;
1838 }
1839 return animator ;
1840 },
1841
1842 /**
1843 The contents of the view as HTML. You can use this property to both
1844 retrieve the content and to change it. Use this property instead of
1845 manually changing the content of your view as this property works around
1846 certain cross-browser bugs.
1847
1848 @field
1849 */
1850 innerHTML: function(key, value) {
1851 if (value !== undefined) {
1852
1853 // Clear the text node.
1854 this._textNode = null ;
1855
1856 // Safari2 has a bad habit of sometimes not actually changing its
1857 // innerHTML. This will make sure the innerHTML get's changed properly.
1858 if (SC.isSafari() && !SC.isSafari3()) {
1859 var el = (this.containerElement || this.rootElement) ; var reps = 0 ;
1860 var f = function() {
1861 el.innerHTML = '' ; el.innerHTML = value ;
1862 if ((reps++ < 5) && (value.length>0) && (el.innerHTML == '')) {
1863 f.invokeLater() ;
1864 }
1865 };
1866 f();
1867 } else (this.containerElement || this.rootElement).innerHTML = value;
1868 } else value = (this.containerElement || this.rootElement).innerHTML ;
1869 return value ;
1870 }.property(),
1871
1872 /**
1873 The contents of the view as plain text. You can use this property to
1874 both retrieve the content and to change it. Use this property instead of
1875 the innerHTML property when you want to set plain text only as this
1876 property is much faster.
1877
1878 @field
1879 */
1880 innerText: function(key, value) {
1881 if (value !== undefined) {
1882 if (value == null) value = '' ;
1883
1884 // add a textNode if necessary
1885 if (this._textNode == null) {
1886 this._textNode = document.createTextNode(value) ;
1887 var el = this.rootElement || this.containerElement ;
1888 while(el.firstChild) el.removeChild(el.firstChild) ;
1889 el.appendChild(this._textNode) ;
1890 } else this._textNode.data = value ;
1891 }
1892
1893 return (this._textNode) ? this._textNode.data : this.innerHTML().unescapeHTML() ;
1894
1895 }.property(),
1896
1897 // ..........................................
1898 // SUPPORT METHODS
1899 //
1900 init: function() {
1901 arguments.callee.base.call(this) ;
1902
1903 // configure them outlets.
1904 if (SC.BENCHMARK_CONFIGURE_OUTLETS) SC.Benchmark.start('SC.View.configureOutlets') ;
1905 this.configureOutlets() ;
1906 if (SC.BENCHMARK_CONFIGURE_OUTLETS) SC.Benchmark.end('SC.View.configureOutlets') ;
1907
1908 var toolTip = this.get('toolTip') ;
1909 if(toolTip && (toolTip != '')) this._updateToolTipObserver();
1910
1911 // if container element is a string, convert it to an actual DOM element.
1912 if (this.containerElement && ($type(this.containerElement) === T_STRING)) {
1913 this.containerElement = this.$sel(this.containerElement);
1914 }
1915
1916 // register as a drop target and scrollable.
1917 if (this.get('isDropTarget')) SC.Drag.addDropTarget(this) ;
1918 if (this.get('isScrollable')) SC.Drag.addScrollableView(this) ;
1919
1920 // add scrollable handler
1921 if (this.isScrollable) this.rootElement.onscroll = SC.View._onscroll ;
1922
1923 // setup isVisibleInWindow ;
1924 this.isVisibleInWindow = (this.parentNode) ? this.parentNode.get('isVisibleInWindow') : NO;
1925
1926 // set frame
1927 // var layout = this.get('layout');
1928 // if ( !layout ) {
1929 // var preferredSize = this.get('preferredSize') || { width: 150, height: 100 };
1930 // layout = { x: 0, y: 0, width: preferredSize.width, height: preferredSize.height };
1931 // }
1932 // this.set('frame', layout );
1933 },
1934
1935 // this method looks through your outlets array and will try to
1936 // reconfigure any missing ones.
1937 configureOutlets: function() {
1938
1939 if (!this.outlets || (this.outlets.length <= 0)) return ;
1940
1941 // lookup outlets as selector paths or execute the function if there
1942 // is one.
1943 this.beginPropertyChanges(); // bundle changes
1944 for(var oloc=0;oloc < this.outlets.length;oloc++) {
1945 var view = this.outlet(this.outlets[oloc]) ;
1946 }
1947 this.endPropertyChanges() ;
1948 },
1949
1950 // ..........................................
1951 // VISIBILITY METHODS
1952 //
1953
1954 // Calling this method will show the view. Don't call this method
1955 // directly but instead set the isVisible property to true. You can
1956 // override this method to provide your own show capabilities.
1957 show: function() {
1958 Element.show(this.rootElement) ;
1959 this.removeClassName('hidden') ;
1960 this.set('displayIsVisible',true) ;
1961 },
1962
1963 // This is the primitive method for hiding a view. It will be called when
1964 // isVisible is set to false after an animation runs or immediate if no
1965 // animation is defined.
1966 hide: function() {
1967 Element.hide(this.rootElement) ;
1968 this.addClassName('hidden') ;
1969 this.set('displayIsVisible', false) ;
1970 },
1971
1972 // ..........................................
1973 // DEPRECATED. DO NOT USE
1974 //
1975
1976 // deprecated. Included only for compatibility.
1977 animateVisible: function(key, value) {
1978 if (value !== undefined) return this.set('isAnimationEnabled',value) ;
1979 return this.get('isAnimationEnabled');
1980 }.property('isAnimationEnabled'),
1981
1982
1983 // ..........................................
1984 // PRIVATE METHODS
1985 //
1986
1987 // this will set the rootElement, cleaning up any old element.
1988 _attachRootElement: function(el) {
1989 if (this.rootElement) this.rootElement._configured = null ;
1990 this.rootElement = el ;
1991 el._configured = this._guid ;
1992 },
1993
1994 // This method is called internally after you add or remove a child view.
1995 // It will rebuild the childNodes array to reflect all children.
1996 _rebuildChildNodes: function() {
1997 var ret = [] ; var view = this.firstChild;
1998 while(view) { ret.push(view); view = view.nextSibling; }
1999 this.set('childNodes', ret) ;
2000 },
2001
2002 _toolTipObserver: function() {
2003 var toolTip = this.get('toolTip') ;
2004 if (this.get('localize')) toolTip = toolTip.loc() ;
2005 this.rootElement.title = toolTip ;
2006 }.observes("toolTip"),
2007
2008 _isVisibleObserver: function() {
2009 var flag = this.get('isVisible') ;
2010 if ((this._isVisible === undefined) || (flag != this._isVisible)) {
2011 this._isVisible = flag ;
2012 if (flag) {
2013 this._show() ;
2014 } else this._hide() ;
2015
2016 // update parent state.
2017 this._updateIsVisibleInWindow() ;
2018 }
2019 }.observes('isVisible'),
2020
2021 _updateIsVisibleInWindow: function(parentNodeState) {
2022 if (parentNodeState === undefined) {
2023 var parentNode = this.get('parentNode') ;
2024 parentNodeState = (parentNode) ? parentNode.get('isVisibleInWindow') : false ;
2025 }
2026
2027 var visible = parentNodeState && this.get('isVisible') ;
2028
2029 // if state changes, update and notify children.
2030 if (visible != this.get('isVisibleInWindow')) {
2031 this.set('isVisibleInWindow', visible) ;
2032 this.recacheFrames() ;
2033 var child = this.get('firstChild') ;
2034 while(child) {
2035 child._updateIsVisibleInWindow(visible) ;
2036 child = child.get('nextSibling') ;
2037 }
2038 }
2039 },
2040
2041 // Calling this method will show the view. Don't call this method
2042 // directly but instead set the isVisible property to true. You can
2043 // override this method to provide your own show capabilities.
2044 _show: function(anchorView, triggerEvent) {
2045 // compatibility
2046 if (this.showView) return this.showView() ;
2047
2048 // if this is a type of pane, call the pane manager.
2049 var paneType = this.get('paneType') ;
2050 if (this.get('isPanel')) paneType = SC.PANEL_PANE; // compatibility
2051 if (paneType) {
2052 if (anchorView === undefined) anchorView = null ;
2053 if (triggerEvent === undefined) triggerEvent = null ;
2054 SC.PaneManager.manager().showPaneView(this, paneType, anchorView, triggerEvent) ;
2055 this.set('displayIsVisible', true) ;
2056
2057 // if an animation is defined and animations are configured, use that.
2058 // the displayIsVisible property will be set to true when the animation
2059 // completes.
2060 } else if (this.visibleAnimation && this.get('isAnimationEnabled')) {
2061 this._transitionVisibleTo(1.0) ;
2062
2063 // at this point the animation has been reset to the beginng. Run the
2064 // core show() method immediately so the animation will be visible.
2065 this.show() ;
2066
2067 // otherwise, just change over visible settings.
2068 } else {
2069 this._visibleAnimator = null ;
2070 this.show() ;
2071 }
2072
2073 return this ;
2074 },
2075
2076 _hide: function() {
2077 // compatibility
2078 if (this.hideView) return this.hideView() ;
2079
2080 // if this is a type of pane, call the pane manager.
2081 var isPane = (!!this.get('paneType')) || this.get('isPanel') ;
2082 if (isPane) {
2083 SC.PaneManager.manager().hidePaneView(this) ;
2084 this.set('displayIsVisible', false) ;
2085
2086 // if an animation is defined and animations are configured, use that.
2087 // the displayIsVisible property will be set to false when the animation
2088 // completes.
2089 } else if (this.visibleAnimation && this.get('isAnimationEnabled')) {
2090 this._transitionVisibleTo(0.0) ;
2091
2092 // otherwise, just change over visible settings.
2093 } else {
2094 this._visibleAnimator = null;
2095 this.hide();
2096 }
2097
2098 return this ;
2099 },
2100
2101
2102 _transitionVisibleTo: function(target) {
2103 var a ;
2104
2105 // if an animator already exists, just transition to the new state.
2106 if (this._visibleAnimator) {
2107 this.transitionTo(target,this._visibleAnimator);
2108
2109 // otherwise, build the animator from the options passed. Patch in our
2110 // own onComplete handler.
2111 } else {
2112 var opts = this.visibleAnimation ;
2113 var style = [opts.hidden,opts.visible] ;
2114 opts.onComplete =
2115 this._animateVisibleDidComplete.bind(this,opts.onComplete) ;
2116 this._visibleAnimator = this.transitionTo(target,style,opts);
2117 }
2118 },
2119
2120 // This is called when the animation completes. Finish cleaning up the
2121 // visibility section.
2122 _animateVisibleDidComplete: function(chainFunc) {
2123 if (!this.get('isVisible')) this.hide() ;
2124 if (chainFunc) chainFunc(this) ;
2125 },
2126
2127 _firstResponderObserver: function(target, key, value) {
2128 this.setClassName('focus',value) ;
2129 }.observes('isFirstResponder'),
2130
2131 _dropTargetObserver: function() {
2132 if (this.get('isDropTarget')) {
2133 SC.Drag.addDropTarget(this) ;
2134 } else SC.Drag.removeDropTarget(this) ;
2135 }.observes('isDropTarget'),
2136
2137 // .............................................
2138 // SPECIAL TYPES OF VIEWS
2139 //
2140
2141 // This will show the pane as a popup or picker (depending on the paneType
2142 // you have set.) This works just like setting isVisible to true, except
2143 // that it also passes the anchorView and triggerEvent you pass in.
2144 popup: function(anchorView, triggerEvent) {
2145
2146 // this will bypass the normal observer machinery, calling the private
2147 // _show method ourselves. To avoid triggering _show twice, we patch up
2148 // the internal _isVisible property.
2149 this._isVisible = true ;
2150 this._show(anchorView, triggerEvent) ;
2151 this.set('isVisible', true);
2152 },
2153
2154 // This can be used to manually add observers to the rootElement for the
2155 // methods in the passed map. You generally don't want to do this since we
2156 // handle event propgation through the responder chain.
2157 configureObserverMethods: function(methodMap) {
2158 for(var name in methodMap) {
2159 if (!methodMap.hasOwnProperty(name)) continue ;
2160 if (this[name]) {
2161 var method = this[name].bindAsEventListener(this);
2162 Event.observe(this.rootElement,methodMap[name],method) ;
2163 }
2164 }
2165 },
2166
2167 //
2168 // SC.Tree support
2169 //
2170
2171 /**
2172 @property
2173
2174 Returns the parent of this view.
2175
2176 Required by SC.Tree.
2177
2178 @returns {SC.View} Can be null.
2179 */
2180 parent: function() {
2181 return this.parentNode;
2182 }.property('parentNode'),
2183
2184 /**
2185 @property
2186
2187 Returns the childen of this view, as an SC.Array-compatible object.
2188
2189 Required by SC.Tree.
2190
2191 @returns {SC.Array}
2192 */
2193 children: function() {
2194 if (!this._viewChildren) this._viewChildren = SC._ViewChildren.create({ view: this });
2195 return this._viewChildren;
2196 }, // don't need to observe childNodes; SC._ViewChildren does it for us
2197
2198 /**
2199 @property
2200
2201 Returns the number of children.
2202
2203 Required by SC.Tree.
2204
2205 @returns {Number}
2206 */
2207 childCount: function() {
2208 return this.childNodes.length;
2209 }.property('childNodes')
2210
2211 // toString: function() {
2212 // var el = this.rootElement ;
2213 // var tagName = (!!el.tagName) ? el.tagName.toLowerCase() : 'document' ;
2214 //
2215 // var className = el.className ;
2216 // className = (className && className.length>0) ? 'class=%@'.fmt(className) : null;
2217 //
2218 // var idName = el.id ;
2219 // idName = (idName && idName.length>0) ? 'id=%@'.fmt(idName) : null;
2220 //
2221 // return "%@:%@<%@>".fmt(this._type, this._guid, [tagName,idName, className].compact().join(' ')) ;
2222 // }
2223
2224}) ;
2225
2226//
2227// This is a private, internal class returned by SC.View.children(). It dips
2228// directly into the internals of SC.View to do its work.
2229//
2230// Note, this will still relay changes not made using this object.
2231//
2232// This cannot be implemented by SC.View itself, due to method naming collisions.
2233//
2234SC._ViewChildren = SC.Object.extend( SC.Array, {
2235
2236 view: null,
2237
2238 length: function() {
2239 if (!view) return 0;
2240 return view.childNodes.length;
2241 }, // don't need to observe anything
2242
2243 childNodesObserver: function() {2
2244 this.arrayContentDidChange();
2245 }.observes('view.childNodes'),
2246
2247 //
2248 // SC.Array support
2249 //
2250
2251 /**
2252 SC.Array primitive implementation.
2253
2254 @param {Number} idx
2255 Starting index in the children to replace. If idx >= length, then append to
2256 the end of the array.
2257
2258 @param {Number} amt
2259 Number of children that should be removed, starting at *idx*.
2260
2261 @param {Array} objects
2262 An array of zero or more childvews that should be inserted into children at
2263 *idx*
2264 */
2265 replace: function(idx, amt, objects) {
2266 var insertBeforeView = null;
2267 var len;
2268
2269 if (!view) return;
2270
2271 // TODO: replacing an array of views could be *way* faster
2272 view.beginPropertyChanges();
2273
2274 if (idx >= 0) {
2275 for (len = 0; amt > len; len++ ) {
2276 // this.view.childNodes.length changes each iteration
2277 if (idx < this.view.childNodes.length) this.view.removeChild(this.view.childNodes[idx]);
2278 else break; // nothing more to remove
2279 }
2280 }
2281
2282 if (0 <= idx && idx+1 < this.view.childNodes.length) insertBeforeView = this.view.childNodes[idx+1];
2283 if (objects) len = objects.length;
2284
2285 while (--len >= 0) {
2286 this.view.insertBefore(objects[len], insertBeforeView);
2287 insertBeforeView = objects[len];
2288 }
2289
2290 view.endPropertyChanges();
2291 // this.arrayContentDidChange() will be triggered by childNodesObserver if needed
2292 },
2293
2294 /**
2295 SC.Array primitive implementation.
2296
2297 This works on the view's *children*.
2298
2299 @param {Number} idx
2300 The index of the item to return. If idx exceeds the current length, returns null.
2301 */
2302 objectAt: function(idx)
2303 {
2304 if (!_view) return undefined;
2305
2306 if (idx < 0) return undefined ;
2307 if (idx >= this._view.childNodes.length) return null;
2308 return this._view.childNodes[idx];
2309 }
2310
2311});
2312
2313// Class Methods
2314SC.View.mixin({
2315
2316 // this is the global registry of views. It's used to map elements back
2317 // to the views that own them.
2318 _view: {},
2319
2320 findViewForElement: function(el) {
2321 var guid = el._configured ;
2322 return (guid) ? SC.View._view[guid] : null ;
2323 },
2324
2325 // ..........................................
2326 // SETUP
2327 //
2328 // This works much like create except that it works on the passed in
2329 // element instead of trying to find something new. If you pass null for
2330 // the first parameter, then a new element will be created with the html
2331 // you set in content.
2332 viewFor: function(el,config) {
2333 if (el) el = $(el) ;
2334
2335 var r = SC.idt.active ; var vStart ;
2336 if (r) SC.idt.v_count++;
2337
2338 if (r) vStart = new Date().getTime() ;
2339
2340 // find or build the element.
2341 if (!el) {
2342 var emptyElement = this.prototype._cachedEmptyElement || this.prototype.emptyElement;
2343
2344 // if the emptyElement is a string not starting with '<', treat it like
2345 // an id and find it in the doc. If an element is found, cache it for
2346 // future use.
2347 var isString = typeof(emptyElement) == 'string' ;
2348 if (isString && (emptyElement.slice(0,1) != '<')) {
2349 var el = $sel(emptyElement) ;
2350 if (el) {
2351 this.prototype.emptyElement = emptyElement = el ;
2352 isString = false ;
2353 }
2354 }
2355
2356 // if still a string, then use it to create HTML. Save the generated
2357 // element so that we can avoid doing this over again.
2358 if (isString) {
2359 SC._ViewCreator.innerHTML = emptyElement ;
2360 el = $(SC._ViewCreator.firstChild) ;
2361 SC.NodeCache.appendChild(el) ;
2362 this.prototype._cachedEmptyElement = el.cloneNode(true) ;
2363
2364 } else if (typeof(emptyElement) == "object") {
2365 if (emptyElement.tagName) {
2366 el = emptyElement.cloneNode(true) ;
2367 } else el = SC.NodeDescriptor.create(emptyElement) ;
2368 }
2369 }
2370 if (r) SC.idt.vc_t += (new Date().getTime()) - vStart ;
2371
2372 // configure only once.
2373 if (el && el._configured) return SC.View.findViewForElement(el);
2374
2375 // Now that we have found an element, instantiate the view.
2376 var args = SC.$A(arguments) ; args[0] = { rootElement: el } ;
2377 if (r) vStart = new Date().getTime();
2378 var ret = new this(args,this) ; // create instance.
2379 if (r) SC.idt.v_t += (new Date().getTime()) - vStart;
2380 el._configured = ret._guid ;
2381
2382 // return the view.
2383 SC.View._view[ret._guid] = ret ;
2384 return ret ;
2385 },
2386
2387 // create in the view work is like viewFor but with 'null' for el
2388 create: function(configs) {
2389 var args = SC.$A(arguments) ;
2390 args.unshift(null) ;
2391 return this.viewFor.apply(this,args) ;
2392 },
2393
2394 // extend works just like a normal extend except that we need to delete the cached empty
2395 // element.
2396 extend: function(configs) {
2397 var ret = SC.Object.extend.apply(this, arguments) ;
2398 ret.prototype._cachedEmptyElement = null ;
2399 return ret ;
2400 },
2401
2402 /**
2403 Creates a new subclass, add to the receiver any passed properties
2404 or methods, and configure it as an outlet, which will cause an
2405 instance of the subclass to be created during awake().
2406
2407 @params {Hash} props the methods of properties you want to add
2408 @returns {Class} A new object class
2409 */
2410 outlet: function(props) {
2411 if (SC.BENCHMARK_OBJECTS) SC.Benchmark.start('SC.Object.outlet') ;
2412
2413 var viewClass = this.extend(props) ; // save the view class
2414 var func = function() { return viewClass.viewFor(null) ; } ;
2415 func.isOutlet = true ;
2416
2417 if (SC.BENCHMARK_OBJECTS) SC.Benchmark.end('SC.Object.outlet') ;
2418
2419 return func ;
2420 },
2421
2422 /**
2423 Defines a view as an outlet. This will return an function that
2424 can be executed at a later time to actually create itself as an outlet.
2425 */
2426 outletFor: function(path) {
2427 var viewClass = this ; // save the view class
2428 var func = function() {
2429 if (SC.BENCHMARK_OUTLETS) SC.Benchmark.start("OUTLET(%@)".format(path)) ;
2430
2431 // if no path was passed, then create the view from scratch
2432 if (path == null) {
2433 var ret = viewClass.viewFor(null) ;
2434
2435 // otherwise, try to find the HTML element identified by the path.
2436 // If the element cannot be found in the caller (the owner view), then
2437 // search the entire document.
2438 } else {
2439 var ret = (this.$$sel) ? this.$$sel(path) : $$sel(path) ;
2440
2441 // if some HTML has been found, then loop through and create views for each
2442 // one. Be sure to setup the proper parent view.
2443 if (ret) {
2444 var owner = this ; var views = [] ;
2445 for(var loc=0;loc<ret.length;loc++) {
2446
2447 // create the new view instance
2448 var view = viewClass.viewFor(ret[loc], { owner: owner }) ;
2449
2450 // if successful, then we need to determine the new parentNode.
2451 // then walk up the DOM tree to find the first parent element
2452 // managed by a view (including this).
2453 //
2454 // If a matching view is not found, but the view IS in a DOM
2455 // somewhere then make the view a child of either SC.page or
2456 // SC.window.
2457 //
2458 // Add the view to the list of child views also.
2459 //
2460 if (view && view.rootElement && view.rootElement.parentNode) {
2461 var node = view.rootElement.parentNode;
2462 var parentView = null ;
2463
2464 // go up the chain. stop when we find a parent view, or the rootElement
2465 // for SC.page.
2466 while(node && !parentView) {
2467 switch(node) {
2468 case this.rootElement:
2469 parentView = this;
2470 break ;
2471 case SC.page.rootElement:
2472 parentView = SC.page ;
2473 break;
2474 case SC.window.rootElement:
2475 parentView = SC.window ;
2476 break;
2477 default:
2478 node = node.parentNode ;
2479 }
2480 }
2481
2482 // if a parentView was found, then add to parentView.
2483 if (parentView) {
2484 parentView._insertBefore(view,null,false) ;
2485 parentView._rebuildChildNodes() ; // this is not done with _insertBefore.
2486 view._updateIsVisibleInWindow();
2487 }
2488
2489 // view is not in a DOM. nothing to do.
2490 }
2491
2492 // add to return array
2493 views[views.length] = view ;
2494
2495 }
2496 ret = views ;
2497 ret = (ret.length == 0) ? null : ((ret.length == 1) ? ret[0] : ret);
2498 }
2499
2500 }
2501
2502 if (SC.BENCHMARK_OUTLETS) SC.Benchmark.end("OUTLET(%@)".format(path)) ;
2503 return ret ;
2504 } ;
2505 func.isOutlet = true ;
2506 return func ;
2507 },
2508
2509 automaticOutletFor: function() {
2510 var ret = this.outletFor.apply(this, arguments) ;
2511 ret.autoconfiguredOutlet = YES ;
2512 return ret ;
2513 }
2514
2515}) ;
2516
2517// IE Specfic Overrides
2518if (SC.Platform.IE) {
2519 SC.View.prototype.getStyle = function(style) {
2520 var element = this.rootElement ;
2521
2522 // collect value
2523 style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
2524 var value = element.style[style];
2525 if (!value && element.currentStyle) value = element.currentStyle[style];
2526
2527 // handle opacity
2528 if (style === 'opacity') {
2529 if (value = (this.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) {
2530 if (value[1]) value = parseFloat(value[1]) / 100;
2531 }
2532 value = 1.0;
2533 }
2534
2535 // handle auto
2536 if (value === 'auto') {
2537 switch(style) {
2538 case 'width':
2539 if (this.getStyle('display') === 'none') {
2540 value = null ;
2541 } else if (element.currentStyle) {
2542 var paddingLeft = parseInt(element.currentStyle.paddingLeft,0)||0;
2543 var paddingRight = parseInt(element.currentStyle.paddingRight,0)||0;
2544 var borderLeftWidth = parseInt(element.currentStyle.borderLeftWidth, 0) || 0 ;
2545 var borderRightWidth = parseInt(element.currentStyle.borderRightWidth, 0) || 0 ;
2546 value = (element.offsetWidth - paddingLeft - paddingRight - borderLeftWidth - borderRightWidth) + 'px' ;
2547 }
2548 break ;
2549 case 'height':
2550 if (this.getStyle('display') === 'none') {
2551 value = null ;
2552 } else if (element.currentStyle) {
2553 var paddingTop = parseInt(element.currentStyle.paddingTop,0)||0;
2554 var paddingBottom = parseInt(element.currentStyle.paddingBottom,0)||0;
2555 var borderTopWidth = parseInt(element.currentStyle.borderTopWidth, 0) || 0 ;
2556 var borderBottomWidth = parseInt(element.currentStyle.borderBottomWidth, 0) || 0 ;
2557 value = (element.offsetHeight - paddingTop - paddingBottom - borderTopWidth - borderBottomWidth) + 'px' ;
2558 }
2559
2560 break ;
2561 default:
2562 value = null ;
2563 }
2564 }
2565
2566 return value;
2567 };
2568
2569 // Called from innerFrame to actually collect the values for the innerFrame.
2570 // Normally the value we want for the width/height is stored in clientWidth/
2571 // height but in IE this is only good if the element hasLayout. In this
2572 // case always use the scrollWidth/Height.
2573 SC.View._collectInnerFrame = function() {
2574 var el = this.rootElement ;
2575 var hasLayout = (el.currentStyle) ? el.currentStyle.hasLayout : false ;
2576 var borderTopWidth = parseInt(el.currentStyle.borderTopWidth, 0) || 0 ;
2577 var borderBottomWidth = parseInt(el.currentStyle.borderBottomWidth, 0) || 0 ;
2578 var scrollHeight = el.offsetHeight - borderTopWidth - borderBottomWidth ;
2579 if (el.clientWidth > el.scrollWidth) scrollHeight - 15 ;
2580
2581 return {
2582 x: el.offsetLeft,
2583 y: el.offsetTop,
2584 width: (hasLayout) ? Math.min(el.scrollWidth, el.clientWidth) : el.scrollWidth,
2585 height: (hasLayout) ? Math.min(scrollHeight, el.clientHeight) : scrollHeight
2586 };
2587 } ;
2588
2589} else {
2590
2591 // Called from innerFrame to actually collect the values for the innerFrame.
2592 // This method should return the smaller of the scrollWidth/height (which
2593 // will be set if the element is scrollable), or the clientWdith/height
2594 // (which is set if the element is not scrollable).
2595 SC.View._collectInnerFrame = function() {
2596 var el = this.rootElement ;
2597 return {
2598 x: el.offsetLeft,
2599 y: el.offsetTop,
2600 width: Math.min(el.scrollWidth, el.clientWidth),
2601 height: Math.min(el.scrollHeight, el.clientHeight)
2602 };
2603 } ;
2604}
2605
2606
2607// this handler goes through the guid to avoid any potential memory leaks
2608SC.View._onscroll = function(evt) { $view(this)._onscroll(evt); } ;
2609
2610SC.View.WIDTH_PADDING_STYLES = ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'];
2611
2612SC.View.HEIGHT_PADDING_STYLES = ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth'];
2613
2614SC.View.SCROLL_WIDTH_PADDING_STYLES = ['borderLeftWidth', 'borderRightWidth'];
2615SC.View.SCROLL_HEIGHT_PADDING_STYLES = ['borderTopWidth', 'borderBottomWidth'];
2616
2617SC.View.elementFor = SC.View.viewFor ; // Old Sprout Compatibility.
2618
2619// This div is used to create nodes. It should normally remain empty.
2620SC._ViewCreator = document.createElement('div') ;
2621
2622// This div can be used to hold elements you don't want on the page right now.
2623SC.NodeCache = document.createElement('div') ;
2624
2625})();