tweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloud
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
481 lines
17 KiB
481 lines
17 KiB
9 years ago
|
/**
|
||
|
* @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) {
|
||
|
//<debug>
|
||
|
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);
|
||
|
}
|
||
|
//</debug>
|
||
|
|
||
|
// Move following nodes forwards by <nodeCount> 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);
|
||
|
});
|