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