/**
* @private
* A cache of View elements keyed using the index of the associated record in the store.
*
* This implements the methods of {Ext.dom.CompositeElement} which are used by {@link Ext.view.AbstractView}
* to provide a map of record nodes and methods to manipulate the nodes.
* @class Ext.view.NodeCache
*/
Ext.define('Ext.view.NodeCache', {
requires: [
'Ext.dom.CompositeElementLite'
],
statics: {
range: document.createRange && document.createRange()
},
constructor: function(view) {
this.view = view;
this.clear();
this.el = new Ext.dom.Fly();
},
/**
* Removes all elements from this NodeCache.
* @param {Boolean} [removeDom] True to also remove the elements from the document.
*/
clear: function(removeDom) {
var me = this,
elements = me.elements,
range = me.statics().range,
key;
if (me.count && removeDom) {
if (range) {
range.setStartBefore(elements[me.startIndex]);
range.setEndAfter(elements[me.endIndex]);
range.deleteContents();
} else {
for (key in elements) {
Ext.removeNode(elements[key]);
}
}
}
me.elements = {};
me.count = me.startIndex = 0;
me.endIndex = -1;
},
/**
* Clears this NodeCache and adds the elements passed.
* @param {HTMLElement[]} els An array of DOM elements from which to fill this NodeCache.
* @return {Ext.view.NodeCache} this
*/
fill: function(newElements, startIndex, fixedNodes) {
fixedNodes = fixedNodes || 0;
var me = this,
elements = me.elements = {},
i,
len = newElements.length - fixedNodes;
if (!startIndex) {
startIndex = 0;
}
for (i = 0; i < len; i++) {
elements[startIndex + i] = newElements[i + fixedNodes];
}
me.startIndex = startIndex;
me.endIndex = startIndex + len - 1;
me.count = len;
return this;
},
insert: function(insertPoint, nodes) {
var me = this,
elements = me.elements,
i,
nodeCount = nodes.length;
// If not inserting into empty cache, validate, and possibly shuffle.
if (me.count) {
//
if (insertPoint > me.endIndex + 1 || insertPoint + nodes.length - 1 < me.startIndex) {
Ext.Error.raise('Discontiguous range would result from inserting ' + nodes.length + ' nodes at ' + insertPoint);
}
//
// Move following nodes forwards by positions
if (insertPoint < me.count) {
for (i = me.endIndex + nodeCount; i >= insertPoint + nodeCount; i--) {
elements[i] = elements[i - nodeCount];
elements[i].setAttribute('data-recordIndex', i);
}
}
me.endIndex = me.endIndex + nodeCount;
}
// Empty cache. set up counters
else {
me.startIndex = insertPoint;
me.endIndex = insertPoint + nodeCount - 1;
}
// Insert new nodes into place
for (i = 0; i < nodeCount; i++, insertPoint++) {
elements[insertPoint] = nodes[i];
elements[insertPoint].setAttribute('data-recordIndex', insertPoint);
}
me.count += nodeCount;
},
invoke: function(fn, args) {
var me = this,
element,
i;
fn = Ext.dom.Element.prototype[fn];
for (i = me.startIndex; i <= me.endIndex; i++) {
element = me.item(i);
if (element) {
fn.apply(element, args);
}
}
return me;
},
item: function(index, asDom) {
var el = this.elements[index],
result = null;
if (el) {
result = asDom ? this.elements[index] : this.el.attach(this.elements[index]);
}
return result;
},
first: function(asDom) {
return this.item(this.startIndex, asDom);
},
last: function(asDom) {
return this.item(this.endIndex, asDom);
},
// @private
// Used by buffered renderer when adding or removing record ranges which are above the
// rendered block. The element block must be shuffled up or down the index range,
// and the data-recordIndex connector attribute must be updated.
moveBlock: function(increment) {
var me = this,
elements = me.elements,
node,
end,
step,
i;
if (increment < 0) {
i = me.startIndex - 1;
end = me.endIndex;
step = 1;
} else {
i = me.endIndex + 1;
end = me.startIndex;
step = -1;
}
me.startIndex += increment;
me.endIndex += increment;
do {
i += step;
node = elements[i + increment] = elements[i];
node.setAttribute('data-recordIndex', i + increment);
// "from" element is outside of the new range, then delete it.
if (i < me.startIndex || i > me.endIndex) {
delete elements[i];
}
} while (i !== end);
delete elements[i];
},
getCount : function() {
return this.count;
},
slice: function(start, end) {
var elements = this.elements,
result = [],
i;
if (!end) {
end = this.endIndex;
} else {
end = Math.min(this.endIndex, end - 1);
}
for (i = start||this.startIndex; i <= end; i++) {
result.push(elements[i]);
}
return result;
},
/**
* Replaces the specified element with the passed element.
* @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element, the Element itself, the index of the
* element in this composite to replace.
* @param {String/Ext.dom.Element} replacement The id of an element or the Element itself.
* @param {Boolean} [domReplace] True to remove and replace the element in the document too.
*/
replaceElement: function(el, replacement, domReplace) {
var elements = this.elements,
index = (typeof el === 'number') ? el : this.indexOf(el);
if (index > -1) {
replacement = Ext.getDom(replacement);
if (domReplace) {
el = elements[index];
el.parentNode.insertBefore(replacement, el);
Ext.removeNode(el);
replacement.setAttribute('data-recordIndex', index);
}
this.elements[index] = replacement;
}
return this;
},
/**
* Find the index of the passed element within the composite collection.
* @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element, or an Ext.dom.Element, or an HTMLElement
* to find within the composite collection.
* @return {Number} The index of the passed Ext.dom.Element in the composite collection, or -1 if not found.
*/
indexOf: function(el) {
var elements = this.elements,
index;
el = Ext.getDom(el);
for (index = this.startIndex; index <= this.endIndex; index++) {
if (elements[index] === el) {
return index;
}
}
return -1;
},
removeRange: function(start, end, removeDom) {
var me = this,
elements = me.elements,
el, i, removeCount, fromPos;
if (end == null) {
end = me.endIndex + 1;
} else {
end = Math.min(me.endIndex + 1, end + 1);
}
if (start == null) {
start = me.startIndex;
}
removeCount = end - start;
for (i = start, fromPos = end; i <= me.endIndex; i++, fromPos++) {
el = elements[i];
// Within removal range and we are removing from DOM
if (removeDom && i < end) {
Ext.removeNode(el);
}
// If the from position is occupied, shuffle that entry back into reference "i"
if (fromPos <= me.endIndex) {
el = elements[i] = elements[fromPos];
el.setAttribute('data-recordIndex', i);
}
// The from position has walked off the end, so delete reference "i"
else {
delete elements[i];
}
}
me.count -= removeCount;
me.endIndex -= removeCount;
},
/**
* Removes the specified element(s).
* @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element, the Element itself, the index of the
* element in this composite or an array of any of those.
* @param {Boolean} [removeDom] True to also remove the element from the document
*/
removeElement: function(keys, removeDom) {
var me = this,
inKeys,
key,
elements = me.elements,
el,
deleteCount,
keyIndex = 0, index,
fromIndex;
// Sort the keys into ascending order so that we can iterate through the elements
// collection, and delete items encountered in the keys array as we encounter them.
if (Ext.isArray(keys)) {
inKeys = keys;
keys = [];
deleteCount = inKeys.length;
for (keyIndex = 0; keyIndex < deleteCount; keyIndex++) {
key = inKeys[keyIndex];
if (typeof key !== 'number') {
key = me.indexOf(key);
}
// Could be asked to remove data above the start, or below the end of rendered zone in a buffer rendered view
// So only collect keys which are within our range
if (key >= me.startIndex && key <= me.endIndex) {
keys[keys.length] = key;
}
}
Ext.Array.sort(keys);
deleteCount = keys.length;
} else {
// Could be asked to remove data above the start, or below the end of rendered zone in a buffer rendered view
if (keys < me.startIndex || keys > me.endIndex) {
return;
}
deleteCount = 1;
keys = [keys];
}
// Iterate through elements starting at the element referenced by the first deletion key.
// We also start off and index zero in the keys to delete array.
for (index = fromIndex = keys[0], keyIndex = 0; index <= me.endIndex; index++, fromIndex++) {
// If the current index matches the next key in the delete keys array, this
// entry is being deleted, so increment the fromIndex to skip it.
// Advance to next entry in keys array.
if (keyIndex < deleteCount && index === keys[keyIndex]) {
fromIndex++;
keyIndex++;
if (removeDom) {
Ext.removeNode(elements[index]);
}
}
// Shuffle entries forward of the delete range back into contiguity.
if (fromIndex <= me.endIndex && fromIndex >= me.startIndex) {
el = elements[index] = elements[fromIndex];
el.setAttribute('data-recordIndex', index);
} else {
delete elements[index];
}
}
me.endIndex -= deleteCount;
me.count -= deleteCount;
},
/**
* Appends/prepends records depending on direction flag
* @param {Ext.data.Model[]} newRecords Items to append/prepend
* @param {Number} direction `-1' = scroll up, `0` = scroll down.
* @param {Number} removeCount The number of records to remove from the end. if scrolling
* down, rows are removed from the top and the new rows are added at the bottom.
* @return {HTMLElement[]} The view item nodes added either at the top or the bottom of the view.
*/
scroll: function(newRecords, direction, removeCount) {
var me = this,
view = me.view,
store = view.store,
elements = me.elements,
recCount = newRecords.length,
nodeContainer = view.getNodeContainer(),
fireItemRemove = view.hasListeners.itemremove,
fireItemAdd = view.hasListeners.itemadd,
range = me.statics().range,
i, el, removeEnd, children, result;
if (!newRecords.length) {
return;
}
// Scrolling up (content moved down - new content needed at top, remove from bottom)
if (direction === -1) {
if (removeCount) {
if (range) {
range.setStartBefore(elements[(me.endIndex - removeCount) + 1]);
range.setEndAfter(elements[me.endIndex]);
range.deleteContents();
for (i = (me.endIndex - removeCount) + 1; i <= me.endIndex; i++) {
el = elements[i];
delete elements[i];
if (fireItemRemove) {
view.fireEvent('itemremove', store.getByInternalId(el.getAttribute('data-recordId')), i, el, view);
}
}
} else {
for (i = (me.endIndex - removeCount) + 1; i <= me.endIndex; i++) {
el = elements[i];
delete elements[i];
Ext.removeNode(el);
if (fireItemRemove) {
view.fireEvent('itemremove', store.getByInternalId(el.getAttribute('data-recordId')), i, el, view);
}
}
}
me.endIndex -= removeCount;
}
// Only do rendering if there are rows to render.
// This could have been a remove only operation due to a view resize event.
if (newRecords.length) {
// grab all nodes rendered, not just the data rows
result = view.bufferRender(newRecords, me.startIndex -= recCount);
children = result.children;
for (i = 0; i < recCount; i++) {
elements[me.startIndex + i] = children[i];
}
nodeContainer.insertBefore(result.fragment, nodeContainer.firstChild);
// pass the new DOM to any interested parties
if (fireItemAdd) {
view.fireEvent('itemadd', newRecords, me.startIndex, children);
}
}
}
// Scrolling down (content moved up - new content needed at bottom, remove from top)
else {
if (removeCount) {
removeEnd = me.startIndex + removeCount;
if (range) {
range.setStartBefore(elements[me.startIndex]);
range.setEndAfter(elements[removeEnd - 1]);
range.deleteContents();
for (i = me.startIndex; i < removeEnd; i++) {
el = elements[i];
delete elements[i];
if (fireItemRemove) {
view.fireEvent('itemremove', store.getByInternalId(el.getAttribute('data-recordId')), i, el, view);
}
}
} else {
for (i = me.startIndex; i < removeEnd; i++) {
el = elements[i];
delete elements[i];
Ext.removeNode(el);
if (fireItemRemove) {
view.fireEvent('itemremove', store.getByInternalId(el.getAttribute('data-recordId')), i, el, view);
}
}
}
me.startIndex = removeEnd;
}
// grab all nodes rendered, not just the data rows
result = view.bufferRender(newRecords, me.endIndex + 1);
children = result.children;
for (i = 0; i < recCount; i++) {
elements[me.endIndex += 1] = children[i];
}
nodeContainer.appendChild(result.fragment);
// pass the new DOM to any interested parties
if (fireItemAdd) {
view.fireEvent('itemadd', newRecords, me.endIndex + 1, children);
}
}
// Keep count consistent.
me.count = me.endIndex - me.startIndex + 1;
return children;
},
sumHeights: function() {
var result = 0,
elements = this.elements,
i;
for (i = this.startIndex; i <= this.endIndex; i++) {
result += elements[i].offsetHeight;
}
return result;
}
}, function() {
Ext.dom.CompositeElementLite.importElementMethods.call(this);
});