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