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 sc_require('views/list'); 8 9 10 /** @class 11 12 A grid view renders a collection of items in a grid of rows and columns. 13 14 ## Dropping on an Item 15 16 When the grid view is configured to accept drags and drops onto its items, it 17 will set the isDropTarget property on the target item accordingly. This 18 allows you to modify the appearance of the drop target grid item accordingly 19 (@see SC.ListItemView#isDropTarget). 20 21 @extends SC.ListView 22 @author Charles Jolley 23 @version 1.0 24 */ 25 SC.GridView = SC.ListView.extend( 26 /** @scope SC.GridView.prototype */ { 27 28 /** @private */ 29 _lastFrameWidth: null, 30 31 /** 32 @type Array 33 @default ['sc-grid-view'] 34 @see SC.View#classNames 35 */ 36 classNames: ['sc-grid-view'], 37 38 /** 39 @type Hash 40 @default { left:0, right:0, top:0, bottom:0 } 41 @see SC.View#layout 42 */ 43 layout: { left: 0, right: 0, top: 0, bottom: 0 }, 44 45 /** 46 The common row height for grid items. 47 48 The value should be an integer expressed in pixels. 49 50 @type Number 51 @default 48 52 */ 53 rowHeight: 48, 54 55 /** 56 The minimum column width for grid items. Items will actually 57 be laid out as needed to completely fill the space, but the minimum 58 width of each item will be this value. 59 60 @type Number 61 @default 64 62 */ 63 columnWidth: 64, 64 65 /** 66 The default example item view will render text-based items. 67 68 You can override this as you wish. 69 70 @type SC.View 71 @default SC.LabelView 72 */ 73 exampleView: SC.LabelView, 74 75 /** 76 Possible values: 77 78 - SC.HORIZONTAL_ORIENTATION 79 - SC.VERTICAL_ORIENTATION 80 81 @type String 82 @default SC.HORIZONTAL_ORIENTATION 83 */ 84 insertionOrientation: SC.HORIZONTAL_ORIENTATION, 85 86 /** @private */ 87 itemsPerRow: function () { 88 var frameWidth = this.get('frame').width, 89 columnWidth = this.get('columnWidth') || 0; 90 91 return (columnWidth < 1) ? 1 : Math.floor(frameWidth / columnWidth); 92 }.property('columnWidth', '_frameWidth').cacheable(), 93 94 /** @private 95 Find the contentIndexes to display in the passed rect. Note that we 96 ignore the width of the rect passed since we need to have a single 97 contiguous range. 98 */ 99 contentIndexesInRect: function (rect) { 100 var rowHeight = this.get('rowHeight') || 48, 101 itemsPerRow = this.get('itemsPerRow'), 102 min = Math.floor(SC.minY(rect) / rowHeight) * itemsPerRow, 103 max = Math.ceil(SC.maxY(rect) / rowHeight) * itemsPerRow; 104 return SC.IndexSet.create(min, max - min); 105 }, 106 107 /** @private */ 108 layoutForContentIndex: function (contentIndex) { 109 var rowHeight = this.get('rowHeight') || 48, 110 frameWidth = this.get('frame').width, 111 itemsPerRow = this.get('itemsPerRow'), 112 columnWidth = Math.floor(frameWidth / itemsPerRow), 113 row = Math.floor(contentIndex / itemsPerRow), 114 col = contentIndex - (itemsPerRow * row); 115 116 // If the frame is not ready, then just return an empty layout. 117 // Otherwise, NaN will be entered into layout values. 118 if (frameWidth === 0 || itemsPerRow === 0) { 119 return {}; 120 } 121 122 return { 123 left: col * columnWidth, 124 top: row * rowHeight, 125 height: rowHeight, 126 width: columnWidth 127 }; 128 }, 129 130 /** @private 131 Overrides default CollectionView method to compute the minimum height 132 of the list view. 133 */ 134 computeLayout: function () { 135 var content = this.get('content'), 136 count = (content) ? content.get('length') : 0, 137 rowHeight = this.get('rowHeight') || 48, 138 itemsPerRow = this.get('itemsPerRow'), 139 // Check that itemsPerRow isn't 0 to prevent Infinite rows. 140 rows = itemsPerRow ? Math.ceil(count / itemsPerRow) : 0; 141 142 // use this cached layout hash to avoid allocing memory... 143 var ret = this._cachedLayoutHash; 144 if (!ret) ret = this._cachedLayoutHash = {}; 145 146 ret.height = rows * rowHeight; 147 148 return ret; 149 }, 150 151 /** 152 Default view class used to draw an insertion point, which uses CSS 153 styling to show a horizontal line. 154 155 This view's position (top & left) will be automatically adjusted to the 156 point of insertion. 157 158 @field 159 @type SC.View 160 */ 161 insertionPointClass: SC.View.extend({ 162 classNames: ['sc-grid-insertion-point'], 163 164 layout: { width: 2 }, 165 166 render: function (context, firstTime) { 167 if (firstTime) context.push('<div class="anchor"></div>'); 168 } 169 }), 170 171 /** @private */ 172 showInsertionPoint: function (itemView, dropOperation) { 173 if (!itemView) return; 174 175 // if drop on, then just add a class... 176 if (dropOperation & SC.DROP_ON) { 177 if (itemView !== this._lastDropOnView) { 178 this.hideInsertionPoint(); 179 180 // If the drag is supposed to drop onto an item, notify the item that it 181 // is the current target of the drop. 182 itemView.set('isDropTarget', YES); 183 184 // Track the item so that we can clear isDropTarget when the drag changes; 185 // versus having to clear it from all items. 186 this._lastDropOnView = itemView; 187 } 188 189 } else { 190 if (this._lastDropOnView) { 191 // If there was an item that was the target of the drop previously, be 192 // sure to clear it. 193 this._lastDropOnView.set('isDropTarget', NO); 194 this._lastDropOnView = null; 195 } 196 197 var insertionPoint = this._insertionPointView, 198 layout = itemView.get('layout'), 199 top, left; 200 201 if (!insertionPoint) { 202 insertionPoint = this._insertionPointView = this.insertionPointClass.create(); 203 } 204 205 // Adjust the position of the insertion point. 206 top = layout.top; 207 left = layout.left; 208 if (dropOperation & SC.DROP_AFTER) left += layout.width; 209 var height = layout.height; 210 211 // Adjust the position of the insertion point. 212 insertionPoint.adjust({ top: top, left: left, height: height }); 213 this.appendChild(insertionPoint); 214 } 215 }, 216 217 /** @see SC.CollectionView#hideInsertionPoint */ 218 hideInsertionPoint: function () { 219 // If there was an item that was the target of the drop previously, be 220 // sure to clear it. 221 if (this._lastDropOnView) { 222 this._lastDropOnView.set('isDropTarget', NO); 223 this._lastDropOnView = null; 224 } 225 226 var view = this._insertionPointView; 227 if (view) view.removeFromParent().destroy(); 228 this._insertionPointView = null; 229 }, 230 231 /** @private */ 232 insertionIndexForLocation: function (loc, dropOperation) { 233 var f = this.get('frame'), 234 sf = this.get('clippingFrame'), 235 itemsPerRow = this.get('itemsPerRow'), 236 columnWidth = Math.floor(f.width / itemsPerRow), 237 row = Math.floor((loc.y - f.y - sf.y) / this.get('rowHeight')); 238 239 var retOp = SC.DROP_BEFORE, 240 offset = (loc.x - f.x - sf.x), 241 col = Math.floor(offset / columnWidth), 242 percentage = (offset / columnWidth) - col; 243 244 // if the dropOperation is SC.DROP_ON and we are in the center 60% 245 // then return the current item. 246 if (dropOperation === SC.DROP_ON) { 247 if (percentage > 0.80) col++; 248 if ((percentage >= 0.20) && (percentage <= 0.80)) { 249 retOp = SC.DROP_ON; 250 } 251 } else { 252 if (percentage > 0.45) col++; 253 } 254 255 // convert to index 256 var ret = (row * itemsPerRow) + col; 257 return [ret, retOp]; 258 }, 259 260 /** @private 261 Since GridView lays out items evenly from left to right, if the width of the 262 frame changes, all of the item views on screen are potentially in 263 the wrong position. 264 265 Update all of their layouts if necessary. 266 */ 267 _gv_frameDidChange: function () { 268 var frame = this.get('frame'), 269 lastFrameWidth = this._lastFrameWidth, 270 width = frame.width; 271 272 // A change to the width of the frame is the only variable that 273 // alters the layout of item views and our computed layout. 274 if (!SC.none(lastFrameWidth) && width !== lastFrameWidth) { 275 var itemView, 276 nowShowing = this.get('nowShowing'); 277 278 // Internal property used to indicate a possible itemsPerRow change. This 279 // is better than having itemsPerRow dependent on frame which changes frequently. 280 this.set('_frameWidth', width); 281 282 // Only loop through the now showing indexes, if the content was sparsely 283 // loaded we would inadvertently load all the content. 284 nowShowing.forEach(function (idx) { 285 itemView = this.itemViewForContentIndex(idx); 286 itemView.adjust(this.layoutForContentIndex(idx)); 287 }, this); 288 } 289 290 this._lastFrameWidth = width; 291 }.observes('frame'), 292 293 /** @private Recompute our layout if itemsPerRow actually changes. */ 294 _gv_itemsPerRowDidChange: function () { 295 var itemsPerRow = this.get('itemsPerRow'), 296 lastItemsPerRow = this._lastItemsPerRow || 0; 297 298 if (itemsPerRow !== lastItemsPerRow) { 299 this.invokeOnce('adjustLayout'); 300 } 301 302 this._lastItemsPerRow = itemsPerRow; 303 }.observes('itemsPerRow') 304 305 }); 306