ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
528 lines (527 loc) • 18.2 kB
JavaScript
//*@protected
/**
This is a delegate (strategy) used by _enyo.DataList_ for vertically oriented
lists. This is used by all lists for this strategy and does not get copied but
called directly from the list.
*/
enyo.DataList.delegates.vertical = {
/**
Used to determine the minumum size of the pages. The page size will be at least
this number of times greater than the viewport size.
*/
pageSizeMultiplier: 2,
/**
Simply set the priority properties for this orientation that can be differentiated
by other delegates that wish to share some basic functionality.
*/
initList: function (list) {
list.posProp = "top";
list.upperProp = "top";
list.lowerProp = "bottom";
list.psizeProp = "height";
list.ssizeProp = "width";
// set the scroller options
var so = list.scrollerOptions? (list.scrollerOptions = enyo.clone(list.scrollerOptions)): (list.scrollerOptions = {});
// this is a datalist...it has to be scroll or auto for vertical
so.vertical = so.vertical == "scroll"? "scroll": "auto";
so.horizontal = so.horizontal || "hidden";
},
/**
A hard reset of the list pages and children. Will scroll to the top, reset children
of each page to the correct indices starting at the beginning.
*/
reset: function (list) {
// go ahead and reset the page content and the pages to their original
// positions
for (var i=0, p; (p=list.pages[i]); ++i) {
this.generatePage(list, p, i);
}
// adjust page positions
this.adjustPagePositions(list);
// now update the buffer
this.adjustBuffer(list);
list.hasReset = true;
// reset the scroller so it will also start from the 'top' whatever that may
// be (left/top)
list.$.scroller.scrollTo(0, 0);
},
/**
Returns a hash of the pages marked by there position as either 'firstPage' or 'lastPage'.
*/
pagesByPosition: function (list) {
var metrics = list.metrics.pages,
pos = list.pagePositions || (list.pagePositions={}),
upperProp = list.upperProp,
firstIndex = list.$.page1.index,
secondIndex = list.$.page2.index;
pos.firstPage = (
metrics[firstIndex][upperProp] < metrics[secondIndex][upperProp]
? list.$.page1
: list.$.page2
);
pos.lastPage = (pos.firstPage === list.$.page1? list.$.page2: list.$.page1);
return pos;
},
/**
Refreshes each page in the given list, adjusting their positions and adjusting
the buffer accordingly.
*/
refresh: function (list) {
if (!list.hasReset) { return this.reset(list); }
var pageCount = Math.max(this.pageCount(list) - 1, 0),
firstIndex = list.$.page1.index,
secondIndex = list.$.page2.index;
if (firstIndex > pageCount) {
firstIndex = pageCount;
}
if (secondIndex > pageCount) {
if ((firstIndex + 1) > pageCount && (firstIndex - 1) >= 0) {
secondIndex = firstIndex - 1;
} else {
secondIndex = firstIndex + 1;
}
}
list.$.page1.index = firstIndex;
list.$.page2.index = secondIndex;
// update according to their current indices
for (var i=0, p; (p=list.pages[i]); ++i) {
this.generatePage(list, p, p.index);
}
// adjust their positions in case they've changed at all
this.adjustPagePositions(list);
// now update the buffer
this.adjustBuffer(list);
},
/**
Once the list is initially rendered it will generate its scroller (so
we know that is available). Now we need to cache our initial size values
and apply them to our pages individually.
*/
rendered: function (list) {
if (list.$.scroller.addScrollListener) {
list.usingScrollListener = true;
list.$.scroller.addScrollListener(
enyo.bindSafely(this, "scrollHandler", list)
);
}
// get our initial sizing cached now since we should actually have
// bounds at this point
this.updateBounds(list);
// now if we already have a length then that implies we have a controller
// and that we have data to render at this point, otherwise we don't
// want to do any more initialization
if (list.length) { this.reset(list); }
},
/**
This method generates the markup for the page content.
*/
generatePage: function (list, page, index) {
// in case it hasn't been set we ensure it is marked correctly
page.index = index;
// the collection of data with records to use
var data = list.collection,
// the metrics for the entire list
metrics = list.metrics,
// controls per page
perPage = this.controlsPerPage(list),
// placeholder for the control we're going to update
view;
// the first index for this generated page
page.start = perPage * index;
// the last index for this generated page
page.end = Math.min((data.length - 1), (page.start + perPage) - 1);
// if generating a control we need to use the correct page as the control parent
list.controlParent = page;
for (var i=page.start; i <= page.end && i < data.length; ++i) {
view = (page.children[i - page.start] || list.createComponent({}));
// disable notifications until all properties to be updated
// have been
view.teardownRender();
view.stopNotifications();
view.set("model", data.at(i));
view.set("index", i);
view.set("selected", list.isSelected(view.model));
view.startNotifications();
view.canGenerate = true;
}
// if there are any controls that need to be hidden we do that now
for (i=(i-page.start); i < page.children.length; ++i) {
view = page.children[i];
view.teardownRender();
view.canGenerate = false;
}
// update the entire page at once - this removes old nodes and updates
// to the correct ones
page.render();
// now to update the metrics
metrics = metrics.pages[index] || (metrics.pages[index] = {});
metrics.height = this.pageHeight(list, page);
metrics.width = this.pageWidth(list, page);
},
/**
Generates a child size for the given list.
*/
childSize: function (list) {
var pageIndex = list.$.page1.index,
sizeProp = list.psizeProp,
n = list.$.page1.node || list.$.page1.hasNode(),
size, props;
if (pageIndex >= 0 && n) {
props = list.metrics.pages[pageIndex];
size = props? props[sizeProp]: 0;
list.childSize = Math.floor(size / (n.children.length || 1));
}
return list.childSize || (list.childSize = 100); // we have to start somewhere
},
/**
When necessary will update the the value of controlsPerPage dynamically
to ensure the page size is always larger than the viewport. Note that
once a control is instanced (if this number becomes greater and then is
reduced) the number of available controls will be used instead. This method
will updated the _childSize_ value as well used internally for other values
such as _defaultPageSize_.
*/
controlsPerPage: function (list) {
var updatedControls = list._updatedControlsPerPage,
updatedBounds = list._updatedBounds,
childSize = list.childSize,
perPage = list.controlsPerPage,
sizeProp = list.psizeProp,
multi = list.pageSizeMultiplier || this.pageSizeMultiplier,
fn = this[sizeProp];
// if we've never updated the value or it was done longer ago than the most
// recent updated sizing/bounds we need to update
if (!updatedControls || (updatedControls < updatedBounds)) {
// we always update the default child size value first, here
childSize = this.childSize(list);
// using height/width of the available viewport times our multiplier value
perPage = list.controlsPerPage = Math.ceil((fn(list) * multi) / childSize);
// update our time for future comparison
list._updatedControlsPerPage = enyo.bench();
}
/*jshint -W093 */
return (list.controlsPerPage = perPage);
},
/**
Retrieves the page index for the given record index.
*/
pageForIndex: function (list, i) {
var perPage = list.controlsPerPage || this.controlsPerPage(list);
return Math.floor(i / (perPage || 1));
},
/**
Attempts to scroll to the given index.
*/
scrollToIndex: function (list, i) {
// first see if the child is already available to scroll to
var c = this.childForIndex(list, i),
// but we also need the page so we can find its position
p = this.pageForIndex(list, i);
// if there is no page then the index is bad
if (p < 0 || p > this.pageCount(list)) { return; }
// if there isn't one, then we know we need to go ahead and
// update, otherwise we should be able to use the scroller's
// own methods to find it
if (c) {
list.$.scroller.scrollIntoView(c, this.pagePosition(list, p));
} else {
// we do this to ensure we trigger the paging event when necessary
this.resetToPosition(list, this.pagePosition(list, p));
// now retry the original logic until we have this right
enyo.asyncMethod(function () {
list.scrollToIndex(i);
});
}
},
/**
Returns the calculated height for the given page.
*/
pageHeight: function (list, page) {
return page.node.offsetHeight;
},
/**
Returns the calculated width for the given page.
*/
pageWidth: function (list, page) {
return page.node.offsetWidth;
},
/**
Attempts to intelligently decide when to force updates for models being added
if the models are part of any visible pages. For now an assumption is made that
records being added are ordered and sequential.
*/
modelsAdded: function (list, props) {
if (!list.hasReset) { return this.reset(list); }
// the current indices that are rendered
var fi = list.$.page1.index || (list.$.page1.index=0),
si = list.$.page2.index || (list.$.page2.index=1), rf, rs, pi;
for (var i=0, ri; (ri=props.records[i]) >= 0; ++i) {
pi = this.pageForIndex(list, ri);
// we ensure that if the page index is either page we flag that page as
// needing to be updated
if (pi == fi) {
rf = true;
} else if (pi == si) {
rs = true;
} else if (pi > si) {
// no need to continue looking if the page index is greater
// than the known second page index
break;
}
}
// if either page was flagged go ahead and update it
if (rf) { this.generatePage(list, list.$.page1, fi); }
if (rs) { this.generatePage(list, list.$.page2, si); }
// if either was updated we want to go ahead and ensure that our page positions
// are still appropriate
if (rf || rs) { this.adjustPagePositions(list); }
// either way we need to adjust the buffer size
this.adjustBuffer(list);
},
/**
Attempts to find the control for the requested index.
*/
childForIndex: function (list, i) {
var p = this.pageForIndex(list, i),
p1 = list.$.page1,
p2 = list.$.page2;
p = (p==p1.index && p1) || (p==p2.index && p2);
if (p) {
for (var j=0, c; (c=p.children[j]); ++j) {
if (c.index == i) {
return c;
}
}
}
},
/**
Attempts to inelligently decide when to force updates for models being removed
if the models are part of any visible pages.
*/
modelsRemoved: function (list, props) {
// we know that the removed records have been ordered so we can
// work from the bottom to the top, a major difference between adding
// and removing, however, is the fact that added records are grouped
// and removed models could be random so we may have to check them all
var keys = enyo.keys(props.records),
fi = list.$.page1.index,
si = list.$.page2.index, pi;
for (var i=keys.length-1, k; (k=keys[i]) >= 0; --i) {
pi = this.pageForIndex(list, k);
// if either page is included we'll break here and refresh them both
// to ensure accurate view
if (pi == fi || pi == si) {
this.refresh(list);
// for sanity we check to ensure that the current scroll position is
// showing our available content fully since elements were removed
var pos = this.pagesByPosition(list);
this.scrollToIndex(list, pos.firstPage.start);
break;
}
}
},
/**
Recalculates the buffer size based on the current metrics for the given
list. This may or may not be completely accurate until the final page is
scrolled into view.
*/
adjustBuffer: function (list) {
var pc = this.pageCount(list),
ds = this.defaultPageSize(list),
bs = 0, sp = list.psizeProp, ss = list.ssizeProp,
n = list.$.buffer.node || list.$.buffer.hasNode(), p;
if (n) {
if (pc !== 0) {
for (var i=0; i<pc; ++i) {
p = list.metrics.pages[i];
bs += (p && p[sp]) || ds;
}
}
list.bufferSize = bs;
n.style[sp] = bs + "px";
n.style[ss] = this[ss](list) + "px";
}
},
/**
Will ensure that the pages are positioned according to their calculated
positions and update if necessary.
*/
adjustPagePositions: function (list) {
for (var i=0, p; (p=list.pages[i]); ++i) {
var pi = p.index,
cp = this.pagePosition(list, p.index),
mx = list.metrics.pages[pi] || (list.metrics.pages[pi] = {}),
pp = list.posProp,
up = list.upperProp,
lp = list.lowerProp,
sp = list.psizeProp;
p.node.style[pp] = cp + "px";
p[up] = mx[up] = cp;
p[lp] = mx[lp] = (mx[sp] + cp);
}
this.setScrollThreshold(list);
},
/**
Retrieves the assumed position for the requested page index.
*/
pagePosition: function (list, index) {
var mx = list.metrics.pages,
ds = this.defaultPageSize(list),
tt = 0, sp = list.psizeProp, cp;
while (index > 0) {
cp = mx[--index];
// if the index is > 0 then we need to ensure we have at least
// the minimum height available so this is a deliberate 'fail-on-zero' case
tt += (cp && cp[sp]? cp[sp]: ds);
}
return tt;
},
/**
Retrieves the default page size.
*/
defaultPageSize: function (list) {
var perPage = list.controlsPerPage || this.controlsPerPage(list);
return (perPage * (list.childSize || 100));
},
/**
Retrieves the number of pages for for given list.
*/
pageCount: function (list) {
var perPage = list.controlsPerPage || this.controlsPerPage(list);
return (Math.ceil(list.length / (perPage || 1)));
},
/**
Retrieves the current (and desired) scroll position from the scroller
for the given list.
*/
getScrollPosition: function (list) {
return list.$.scroller.getScrollTop();
},
scrollHandler: function (list, bounds) {
var last = this.pageCount(list)-1,
pos = this.pagesByPosition(list);
if ((bounds.xDir === 1 || bounds.yDir === 1) && pos.lastPage.index !== (last)) {
this.generatePage(list, pos.firstPage, pos.lastPage.index + 1);
this.adjustPagePositions(list);
this.adjustBuffer(list);
// note that the reference to the page positions has been udpated by
// another method so we trust the actual pages
list.triggerEvent("paging", {
start: pos.firstPage.start,
end: pos.lastPage.end,
action: "scroll"
});
} else if ((bounds.xDir === -1 || bounds.yDir === -1) && pos.firstPage.index !== 0) {
this.generatePage(list, pos.lastPage, pos.firstPage.index - 1);
this.adjustPagePositions(list);
// note that the reference to the page positions has been udpated by
// another method so we trust the actual pages
list.triggerEvent("paging", {
start: pos.firstPage.start,
end: pos.lastPage.end,
action: "scroll"
});
}
},
setScrollThreshold: function (list) {
var threshold = list.scrollThreshold || (list.scrollThreshold={}),
metrics = list.metrics.pages,
pos = this.pagesByPosition(list),
firstIdx = pos.firstPage.index,
lastIdx = pos.lastPage.index,
count = this.pageCount(list)-1,
lowerProp = list.lowerProp,
upperProp = list.upperProp,
fn = upperProp == "top"? this.height: this.width;
// now to update the properties the scroller will use to determine
// when we need to be notified of position changes requiring paging
if (firstIdx === 0) {
threshold[upperProp] = undefined;
} else {
threshold[upperProp] = metrics[lastIdx][upperProp] - fn(list);
}
if (lastIdx === count) {
threshold[lowerProp] = undefined;
} else {
threshold[lowerProp] = metrics[firstIdx][lowerProp];
}
if (list.usingScrollListener) {
list.$.scroller.setScrollThreshold(threshold);
}
},
resetToPosition: function (list, px) {
if (px >= 0 && px <= list.bufferSize) {
var index = Math.ceil(px / this.defaultPageSize(list)),
last = this.pageCount(list) - 1,
pos = this.pagesByPosition(list);
if (
(px <= pos.firstPage[list.upperProp]) ||
(px >= pos.lastPage[list.lowerProp])
) {
list.$.page1.index = (index = Math.min(index, last));
list.$.page2.index = (index === last? (index-1): (index+1));
this.refresh(list);
list.triggerEvent("paging", {
start: list.$.page1.start,
end: list.$.page2.end,
action: "reset"
});
}
}
},
/**
Handles scroll events for the given list. The events themselves aren't
helpful as depending on the underlying _scrollStrategy_ they have varied
information. This is a hefty method but contained to keep from calling
too many functions whenever this event is propagated.
*/
didScroll: function (list, event) {
if (!list.usingScrollListener) {
var threshold = list.scrollThreshold,
bounds = event.scrollBounds,
lowerProp = list.lowerProp,
upperProp = list.upperProp;
if (bounds.xDir === 1 || bounds.yDir === 1) {
if (bounds[upperProp] > threshold[lowerProp]) {
this.scrollHandler(list, bounds);
}
} else if (bounds.yDir === -1 || bounds.xDir === -1) {
if (bounds[upperProp] < threshold[upperProp]) {
this.scrollHandler(list, bounds);
}
}
}
},
/**
Delegate's resize event handler.
*/
didResize: function (list) {
list._updateBounds = true;
this.updateBounds(list);
this.refresh(list);
},
/**
Returns the height for the given list, will cache this value and reuse
if no resizing of the list has taken place.
*/
height: function (list) {
if (list._updateBounds) { this.updateBounds(list); }
return list.boundsCache.height;
},
/**
Returns the width for the given list, will cache this value and reuse
if no resizing of the list has takekn place.
*/
width: function (list) {
if (list._updateBounds) { this.updateBounds(list); }
return list.boundsCache.width;
},
/**
Updates the cached values for the sizing of the given list.
*/
updateBounds: function (list) {
list.boundsCache = list.getBounds();
list._updatedBounds = enyo.bench();
list._updateBounds = false;
}
};