1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require("views/workspace");
  9 sc_require("views/toolbar");
 10 
 11 
 12 /** @class
 13   Master/Detail view is a simple view which manages a master view and a detail view.
 14   This is not all that different from a SplitView, except that, for the moment (this
 15   will hopefully change when SplitView becomes more palatable) the split point is not
 16   actually changeable and the split is always vertical.
 17 
 18   So, why use it when it is limited? Well, simple: it can hide the left side. Completely.
 19   As in, there will be no split divider anymore. There will be no nothing. It will be gone.
 20   Removed from DOM. Gone on to meet its maker, bereft of life, it rests in peace. If it weren't
 21   for the possibility of opening it up in a picker it would be pushing up the daisies!
 22 
 23   Yes, it has a built-in option for opening the master portion in a PickerPane. This is THE KILLER
 24   FEATURES. It is a command on the view: popupMasterPicker. And it is really really easy to call:
 25   make a toolbar button with an action "popupMasterPicker". That's it.
 26 
 27   An interesting feature is that it sets the master and detail views' masterIsVisible settings,
 28   allowing them to know if the master is visible.
 29 
 30   @since SproutCore 1.2
 31 */
 32 SC.MasterDetailView = SC.View.extend(
 33 /** @scope SC.MasterDetailView.prototype */ {
 34 
 35   /**
 36     @type Array
 37     @default ['sc-master-detail-view']
 38     @see SC.View#classNames
 39   */
 40   classNames: ["sc-master-detail-view"],
 41 
 42   /**
 43     @type String
 44     @default 'masterDetailRenderDelegate'
 45   */
 46   renderDelegateName: 'masterDetailRenderDelegate',
 47 
 48 
 49   // ..........................................................
 50   // Properties
 51   //
 52 
 53   /**
 54     The master view. For your development pleasure, it defaults to a
 55     WorkspaceView with a top toolbar.
 56 
 57     @type SC.View
 58     @default SC.WorkspaceView
 59   */
 60   masterView: SC.WorkspaceView.extend({
 61     topToolbar: SC.ToolbarView.extend({
 62     }),
 63     contentView: SC.View.extend({ backgroundColor: "white" })
 64   }),
 65 
 66   /**
 67     The detail view. For your development experience, it defaults to holding
 68     a top toolbar view with a button that closes/shows master. Come take a peek at
 69     the code to see what it looks like--it is so simple.
 70 
 71     @type SC.View
 72     @default SC.WorkspaceView
 73   */
 74   detailView: SC.WorkspaceView.extend({
 75     topToolbar: SC.ToolbarView.extend({
 76       childViews: ["showHidePicker"],
 77       showHidePicker: SC.ButtonView.extend({
 78         layout: { left: 7, centerY: 0, height: 30, width: 100 },
 79         controlSize: SC.AUTO_CONTROL_SIZE,
 80         title: "Picker",
 81         action: "toggleMasterPicker",
 82         isVisible: NO,
 83         isVisibleBinding: ".parentView.masterIsHidden"
 84       })
 85     })
 86   }),
 87 
 88   /**
 89     Whether to automatically hide the master panel in portrait orientation.
 90 
 91     By default, this property is a computed property based on whether the browser is a touch
 92     browser. Your purpose in overriding it is either to disable it from automatically
 93     disappearing on iPad and other touch devices, or force it to appear when a desktop
 94     browser changes.
 95 
 96     @field
 97     @type Boolean
 98     @default NO
 99   */
100   autoHideMaster: function() {
101     if (SC.platform.touch) return YES;
102     return NO;
103   }.property().cacheable(),
104 
105   /**
106     The width of the 'master' side of the master/detail view.
107 
108     @type Number
109     @default 250
110   */
111   masterWidth: 250,
112 
113   /**
114     The width of the divider between the master and detail views.
115 
116     @type Number
117     @default From theme, or 1.
118   */
119   dividerWidth: SC.propertyFromRenderDelegate('dividerWidth', 1),
120 
121   /**
122     A property (computed) that says whether the master view is hidden.
123 
124     @field
125     @type Boolean
126     @default NO
127     @observes autoHideMaster
128     @observes orientation
129   */
130   masterIsHidden: function() {
131     if (!this.get("autoHideMaster")) return NO;
132     if (this.get("orientation") === SC.HORIZONTAL_ORIENTATION) return NO;
133     return YES;
134   }.property("autoHideMaster", "orientation"),
135 
136   /**
137     Tracks the orientation of the view. Possible values:
138 
139       - SC.VERTICAL_ORIENTATION
140       - SC.HORIZONTAL_ORIENTATION
141 
142     @type String
143     @default SC.VERTICAL_ORIENTATION
144   */
145   orientation: SC.VERTICAL_ORIENTATION,
146 
147   /** @private */
148   _scmd_frameDidChange: function() {
149     var f = this.get("frame"), ret;
150     if (f.width > f.height) ret = SC.HORIZONTAL_ORIENTATION;
151     else ret = SC.VERTICAL_ORIENTATION;
152 
153     this.setIfChanged('orientation', ret);
154   }.observes('frame'),
155 
156   /** @private */
157   init: function() {
158     sc_super();
159     this._scmd_frameDidChange();
160     this._scmd_masterIsHiddenDidChange();
161   },
162 
163   /**
164     If the master is hidden, this toggles the master picker pane.
165     Of course, since pickers are modal, this actually only needs to handle showing.
166 
167     @param {SC.View} view The view to anchor the picker to
168   */
169   toggleMasterPicker: function(view) {
170     if (!this.get("masterIsHidden")) return;
171     if (this._picker && this._picker.get("isVisibleInWindow")) {
172       this.hideMasterPicker();
173     } else {
174       this.showMasterPicker(view);
175     }
176   },
177 
178   /**
179     @param {SC.View} view The view to anchor the picker to
180   */
181   showMasterPicker: function(view) {
182     if (this._picker && this._picker.get("isVisibleInWindow")) return;
183     if (!this._picker) {
184       var pp = this.get("pickerPane");
185       this._picker = pp.create({ });
186     }
187 
188     this._picker.set("contentView", this.get("masterView"));
189     this._picker.set("extraRightOffset", this.get("pointerDistanceFromEdge"));
190 
191     this.showPicker(this._picker, view);
192   },
193 
194   hideMasterPicker: function() {
195     if (this._picker && this._picker.get("isVisibleInWindow")) {
196       this.hidePicker(this._picker);
197     }
198   },
199 
200   /**
201     @param {SC.PickerPane} picker The picker to popup
202     @param {SC.View} view The view to anchor the picker to
203   */
204   showPicker: function(picker, view) {
205     picker.popup(view, SC.PICKER_POINTER, [3, 0, 1, 2, 3], [9, -9, -18, 18]);
206   },
207 
208   /**
209     @param {SC.PickerPane} picker The picker to popup
210   */
211   hidePicker: function(picker) {
212     picker.remove();
213   },
214 
215   /**
216     The picker pane class from which to create a picker pane.
217 
218     This defaults to one with a special theme.
219 
220     @type SC.PickerPane
221     @default SC.PickerPane
222   */
223   pickerPane: SC.PickerPane.extend({
224     layout: { width: 250, height: 480 },
225     themeName: 'popover'
226   }),
227 
228 
229   // ..........................................................
230   // Internal Support
231   //
232 
233   /** @private */
234   _picker: null,
235 
236   /** @private */
237   pointerDistanceFromEdge: 46,
238 
239   /** @private
240     Updates masterIsHidden in child views.
241   */
242   _scmd_masterIsHiddenDidChange: function() {
243     var mih = this.get("masterIsHidden");
244     this.get("masterView").set("masterIsHidden", mih);
245     this.get("detailView").set("masterIsHidden", mih);
246   }.observes("masterIsHidden"),
247 
248   /** @private
249     When the frame changes, we don't need to do anything. We use smart positioning.
250     However, if the orientation were to change, well, then we might need to do something.
251   */
252   _scmd_orientationDidChange: function() {
253     this.invokeOnce("_scmd_tile");
254   }.observes("orientation"),
255 
256   /** @private
257     Observes properties which require retiling.
258   */
259   _scmd_retileProperties: function() {
260     this.invokeOnce("_scmd_tile");
261   }.observes("masterIsHidden", "masterWidth"),
262 
263   /** @private
264     Instantiates master and detail views.
265   */
266   createChildViews: function() {
267     var master = this.get("masterView");
268     master = this.masterView = this.createChildView(master);
269 
270     var detail = this.get("detailView");
271     detail = this.detailView = this.createChildView(detail);
272     this.appendChild(detail);
273 
274     this.invokeOnce("_scmd_tile");
275   },
276 
277   /** @private */
278   _masterIsDrawn: NO, // whether the master is in the view
279 
280   /** @private
281     Tiles the views as necessary.
282   */
283   _scmd_tile: function() {
284     // first, determine what is and is not visible.
285     var masterIsVisible = !this.get('masterIsHidden');
286 
287     // now, tile
288     var masterWidth = this.get('masterWidth'),
289         master = this.get('masterView'),
290         detail = this.get('detailView');
291 
292     if (masterIsVisible) {
293       // hide picker if needed
294       this.hideMasterPicker();
295 
296       // draw master if needed
297       if (!this._masterIsDrawn) {
298         if (this._picker) this._picker.set('contentView', null);
299         this.appendChild(master);
300         this._masterIsDrawn = YES;
301       }
302 
303       // set master layout
304       master.set('layout', {
305         left: 0, top: 0, bottom: 0, width: masterWidth
306       });
307 
308       // and child, naturally
309       var extra = this.get('dividerWidth');
310       detail.set("layout", { left: masterWidth + extra, right: 0, top: 0, bottom: 0 });
311     } else {
312       // remove master if needed
313       if (this._masterIsDrawn) {
314         // Removes the child from the document, but doesn't destroy it or its layer.
315         this.removeChild(master);
316         this._masterIsDrawn = NO;
317       }
318 
319       // and child, naturally
320       detail.set('layout', { left: 0, right: 0, top: 0, bottom: 0 });
321     }
322   }
323 
324 });
325