-
Notifications
You must be signed in to change notification settings - Fork 273
Expand file tree
/
Copy pathgridList.js
More file actions
483 lines (443 loc) · 14.6 KB
/
Copy pathgridList.js
File metadata and controls
483 lines (443 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.GridList = factory();
}
}(this, function() {
var GridList = function(items, options) {
/**
* A GridList manages the two-dimensional positions from a list of items,
* within a virtual matrix.
*
* The GridList's main function is to convert the item positions from one
* grid size to another, maintaining as much of their order as possible.
*
* The GridList's second function is to handle collisions when moving an item
* over another.
*
* The positioning algorithm places items in columns. Starting from left to
* right, going through each column top to bottom.
*
* The size of an item is expressed using the number of cols and rows it
* takes up within the grid (w and h)
*
* The position of an item is express using the col and row position within
* the grid (x and y)
*
* An item is an object of structure:
* {
* w: 3, h: 1,
* x: 0, y: 1
* }
*/
this.options = options;
for (var k in this.defaults) {
if (!this.options.hasOwnProperty(k)) {
this.options[k] = this.defaults[k];
}
}
this.items = items;
this._adjustHeightOfItems();
this.generateGrid();
};
GridList.cloneItems = function(items, _items) {
/**
* Clone items with a deep level of one. Items are not referenced but their
* properties are
*/
var _item,
i,
k;
if (_items === undefined) {
_items = [];
}
for (i = 0; i < items.length; i++) {
// XXX: this is good because we don't want to lose item reference, but
// maybe we should clear their properties since some might be optional
if (!_items[i]) {
_items[i] = {};
}
for (k in items[i]) {
_items[i][k] = items[i][k];
}
}
return _items;
};
GridList.prototype = {
defaults: {
rows: 5
},
generateGrid: function() {
/**
* Build the grid structure from scratch, with the current item positions
*/
var i;
this._resetGrid();
for (i = 0; i < this.items.length; i++) {
this._markItemPositionToGrid(this.items[i]);
}
},
resizeGrid: function(rows) {
var currentColumn = 0,
item,
i;
this.options.rows = rows;
this._adjustHeightOfItems();
this._sortItemsByPosition();
this._resetGrid();
// The items will be sorted based on their index within the this.items array,
// that is their "1d position"
for (i = 0; i < this.items.length; i++) {
item = this.items[i];
this._updateItemPosition(
item, this.findPositionForItem(item, {x: currentColumn, y: 0}));
// New items should never be placed to the left of previous items
currentColumn = Math.max(currentColumn, item.x);
}
},
findPositionForItem: function(item, start, fixedRow) {
/**
* This method has two options for the position we want for the item:
* - Starting from a certain row/column number and only looking for
* positions to its right
* - Accepting positions for a certain row number only (use-case: items
* being shifted to the left/right as a result of collisions)
*/
var x, y, position;
// Start searching for a position from the horizontal position of the
// rightmost item from the grid
for (x = start.x; x < this.grid.length; x++) {
if (fixedRow !== undefined) {
position = [x, fixedRow];
if (this._itemFitsAtPosition(item, position)) {
return position;
}
} else {
for (y = start.y; y < this.options.rows; y++) {
position = [x, y];
if (this._itemFitsAtPosition(item, position)) {
return position;
}
}
}
}
// If we've reached this point, we need to start a new column
return [this.grid.length, fixedRow || 0];
},
moveItemToPosition: function(item, position) {
this._updateItemPosition(item, position);
this._resolveCollisions(item);
},
resizeItem: function(item, width) {
this._updateItemSize(item, width);
this._resolveCollisions(item);
},
getChangedItems: function(initialItems, idAttribute) {
/**
* Compare the current items against a previous snapshot and return only
* the ones that changed their attributes in the meantime. This includes both
* position (x, y) and size (w, h)
*
* Since both their position and size can change, the items need an
* additional identifier attribute to match them with their previous state
*/
var changedItems = [],
i,
item;
for (i = 0; i < initialItems.length; i++) {
item = this._getItemByAttribute(idAttribute, initialItems[i][idAttribute]);
if (item.x !== initialItems[i].x ||
item.y !== initialItems[i].y ||
item.w !== initialItems[i].w ||
item.h !== initialItems[i].h) {
changedItems.push(item);
}
}
return changedItems;
},
_sortItemsByPosition: function() {
var _this = this;
this.items.sort(function(item1, item2) {
// Cols preced rows when it comes to position order
if (item1.x != item2.x) {
return item1.x - item2.x;
}
if (item1.y != item2.y) {
return item1.y - item2.y;
}
// The items are placed on the same position
return 0;
});
},
_adjustHeightOfItems: function() {
/**
* Some items have 100% height, that height is expressed as 0. We need to
* ensure a valid height for each of those items (always as all the number of
* rows of the current grid configuration)
*/
var item,
i;
for (i = 0; i < this.items.length; i++) {
item = this.items[i];
// This only happens the first time they are picked up
if (item.autoHeight === undefined) {
item.autoHeight = !item.h;
}
if (item.autoHeight) {
item.h = this.options.rows;
}
}
},
_resetGrid: function() {
this.grid = [];
},
_itemFitsAtPosition: function(item, position) {
/**
* Check that an item wouldn't overlap with another one if placed at a
* certain position within the grid
*/
var x, y, row;
// No coordonate can be negative
if (position[0] < 0 || position[1] < 0) {
return false;
}
// Make sure the item isn't larger than the entire grid
if (position[1] + item.h > this.options.rows) {
return false;
}
// Make sure the item doesn't overlap with an already positioned item
for (x = position[0]; x < position[0] + item.w; x++) {
col = this.grid[x];
// Surely a column that hasn't even been created yet is available
if (!col) {
continue;
}
for (y = position[1]; y < position[1] + item.h; y++) {
// Any space occupied by an item can continue to be occupied by the same
// item
if (col[y] && col[y] != item) {
return false;
}
}
}
return true;
},
_updateItemPosition: function(item, position) {
if (item.x !== null && item.y !== null) {
this._deleteItemPositionFromGrid(item);
}
item.x = position[0];
item.y = position[1];
this._markItemPositionToGrid(item);
},
_updateItemSize: function(item, width) {
// TODO: Implement height change
if (item.x !== null && item.y !== null) {
this._deleteItemPositionFromGrid(item);
}
item.w = width;
this._markItemPositionToGrid(item);
},
_markItemPositionToGrid: function(item) {
/**
* Mark the grid cells that are occupied by an item. This prevents items
* from overlapping in the grid
*/
var x, y;
// Ensure that the grid has enough columns to accomodate the current item.
this._ensureColumns(item.x + item.w);
for (x = item.x; x < item.x + item.w; x++) {
for (y = item.y; y < item.y + item.h; y++) {
this.grid[x][y] = item;
}
}
},
_deleteItemPositionFromGrid: function(item) {
var x, y;
for (x = item.x; x < item.x + item.w; x++) {
// It can happen to try to remove an item from a position not generated
// in the grid, probably when loading a persisted grid of items. No need
// to create a column to be able to remove something from it, though
if (!this.grid[x]) {
continue;
}
for (y = item.y; y < item.y + item.h; y++) {
// Don't clear the cell if it's been occupied by a different widget in
// the meantime (e.g. when an item has been moved over this one, and
// thus by continuing to clear this item's previous position you would
// cancel the first item's move, leaving it without any position even)
if (this.grid[x][y] == item) {
this.grid[x][y] = null;
}
}
}
},
_ensureColumns: function(N) {
/**
* Ensure that the grid has at least N columns available.
*/
var i;
for (i = 0; i < N; i++) {
if (!this.grid[i]) {
this.grid.push(new GridCol(this.options.rows));
}
}
},
_getItemsCollidingWithItem: function(item) {
var collidingItems = [];
for (var i = 0; i < this.items.length; i++) {
if (item != this.items[i] &&
this._itemsAreColliding(item, this.items[i])) {
collidingItems.push(i);
}
}
return collidingItems;
},
_itemsAreColliding: function(item1, item2) {
return !(item2.x >= item1.x + item1.w ||
item2.x + item2.w <= item1.x ||
item2.y >= item1.y + item1.h ||
item2.y + item2.h <= item1.y);
},
_resolveCollisions: function(item) {
if (!this._tryToResolveCollisionsLocally(item)) {
this._pullItemsToLeft(item);
}
this._pullItemsToLeft();
},
_tryToResolveCollisionsLocally: function(item) {
/**
* Attempt to resolve the collisions after moving a an item over one or more
* other items within the grid, by shifting the position of the colliding
* items around the moving one. This might result in subsequent collisions,
* in which case we will revert all position permutations. To be able to
* revert to the initial item positions, we create a virtual grid in the
* process
*/
var collidingItems = this._getItemsCollidingWithItem(item);
if (!collidingItems.length) {
return true;
}
var _gridList = new GridList([], this.options),
collidingItem,
i,
leftOfItem,
rightOfItem,
aboveOfItem,
belowOfItem;
GridList.cloneItems(this.items, _gridList.items);
_gridList.generateGrid();
for (i = 0; i < collidingItems.length; i++) {
collidingItem = _gridList.items[collidingItems[i]];
// We use a simple algorithm for moving items around when collisions occur:
// In this prioritized order, we try to move a colliding item around the
// moving one:
// 1. to its left side
// 2. above it
// 3. under it
// 4. to its right side
leftOfItem = [item.x - collidingItem.w, collidingItem.y];
rightOfItem = [item.x + item.w, collidingItem.y];
aboveOfItem = [collidingItem.x, item.y - collidingItem.h];
belowOfItem = [collidingItem.x, item.y + item.h];
if (_gridList._itemFitsAtPosition(collidingItem, leftOfItem)) {
_gridList._updateItemPosition(collidingItem, leftOfItem);
} else if (_gridList._itemFitsAtPosition(collidingItem, aboveOfItem)) {
_gridList._updateItemPosition(collidingItem, aboveOfItem);
} else if (_gridList._itemFitsAtPosition(collidingItem, belowOfItem)) {
_gridList._updateItemPosition(collidingItem, belowOfItem);
} else if (_gridList._itemFitsAtPosition(collidingItem, rightOfItem)) {
_gridList._updateItemPosition(collidingItem, rightOfItem);
} else {
// Collisions failed, we must use the pullItemsToLeft method to arrange
// the other items around this item with fixed position. This is our
// plan B for when local collision resolving fails.
return false;
}
}
// If we reached this point it means we managed to resolve the collisions
// from one single iteration, just by moving the colliding items around. So
// we accept this scenario and marge the brached-out grid instance into the
// original one
GridList.cloneItems(_gridList.items, this.items);
this.generateGrid();
return true;
},
_pullItemsToLeft: function(fixedItem) {
/**
* Build the grid from scratch, by using the current item positions and
* pulling them as much to the left as possible, removing as space between
* them as possible.
*
* If a "fixed item" is provided, its position will be kept intact and the
* rest of the items will be layed around it.
*/
var item,
i;
// Start a fresh grid with the fixed item already placed inside
this._sortItemsByPosition();
this._resetGrid();
// Start the grid with the fixed item as the first positioned item
if (fixedItem) {
this._updateItemPosition(fixedItem, [fixedItem.x, fixedItem.y]);
}
for (i = 0; i < this.items.length; i++) {
item = this.items[i];
// The fixed item keeps its exact position
if (fixedItem && item == fixedItem) {
continue;
}
this._updateItemPosition(item, this.findPositionForItem(
item,
{x: this._findLeftMostPositionForItem(item), y: 0},
item.y));
}
},
_findLeftMostPositionForItem: function(item) {
/**
* When pulling items to the left, we need to find the leftmost position for
* an item, with two considerations in mind:
* - preserving its current row
* - preserving the previous horizontal order between items
*/
var tail = 0,
otherItem,
i;
for (i = 0; i < this.grid.length; i++) {
otherItem = this.grid[i][item.y];
if (!otherItem) {
continue;
}
if (this.items.indexOf(otherItem) < this.items.indexOf(item)) {
tail = otherItem.x + otherItem.w;
}
}
return tail;
},
_getItemByAttribute: function(key, value) {
for (var i = 0; i < this.items.length; i++) {
if (this.items[i][key] === value) {
return this.items[i];
}
}
return null;
}
};
var GridCol = function(rows) {
for (var i = 0; i < rows; i++) {
this.push(null);
}
};
// Extend the Array prototype
GridCol.prototype = [];
// This module will have direct access to the GridList class
return GridList;
}));