Location: A review of cardiac cellular electrophysiology models @ f954e5918331 / dojo-presentation / js / dojo / dojox / grid / compat / _grid / scroller.js

Author:
David Nickerson <david.nickerson@gmail.com>
Date:
2021-09-16 00:41:19+12:00
Desc:
Updating Noble 1962 model: * Exposing the membrane potential to the top-level model; * adding SED-ML for the paced and pacemaker variants of the model. Using OpenCOR Snapshot release 2021-09-14.
Permanent Source URI:
https://models.fieldml.org/workspace/a1/rawfile/f954e59183314cd37f86c8832dc81317d01c8ec5/dojo-presentation/js/dojo/dojox/grid/compat/_grid/scroller.js

dojo.provide('dojox.grid.compat._grid.scroller');

dojo.declare('dojox.grid.scroller.base', null, {
	// summary:
	//	virtual scrollbox, abstract class
	//	Content must in /rows/
	//	Rows are managed in contiguous sets called /pages/
	//	There are a fixed # of rows per page
	//	The minimum rendered unit is a page
	constructor: function(){
		this.pageHeights = [];
		this.stack = [];
	},
	// specified
	rowCount: 0, // total number of rows to manage
	defaultRowHeight: 10, // default height of a row
	keepRows: 100, // maximum number of rows that should exist at one time
	contentNode: null, // node to contain pages
	scrollboxNode: null, // node that controls scrolling
	// calculated
	defaultPageHeight: 0, // default height of a page
	keepPages: 10, // maximum number of pages that should exists at one time
	pageCount: 0,
	windowHeight: 0,
	firstVisibleRow: 0,
	lastVisibleRow: 0,
	// private
	page: 0,
	pageTop: 0,
	// init
	init: function(inRowCount, inKeepRows, inRowsPerPage){
		switch(arguments.length){
			case 3: this.rowsPerPage = inRowsPerPage;
			case 2: this.keepRows = inKeepRows;
			case 1: this.rowCount = inRowCount;
		}
		this.defaultPageHeight = this.defaultRowHeight * this.rowsPerPage;
		//this.defaultPageHeight = this.defaultRowHeight * Math.min(this.rowsPerPage, this.rowCount);
		this.pageCount = Math.ceil(this.rowCount / this.rowsPerPage);
		this.setKeepInfo(this.keepRows);
		this.invalidate();
		if(this.scrollboxNode){
			this.scrollboxNode.scrollTop = 0;
			this.scroll(0);
			this.scrollboxNode.onscroll = dojo.hitch(this, 'onscroll');
		}
	},
	setKeepInfo: function(inKeepRows){
		this.keepRows = inKeepRows;
		this.keepPages = !this.keepRows ? this.keepRows : Math.max(Math.ceil(this.keepRows / this.rowsPerPage), 2);
	},
	// updating
	invalidate: function(){
		this.invalidateNodes();
		this.pageHeights = [];
		this.height = (this.pageCount ? (this.pageCount - 1)* this.defaultPageHeight + this.calcLastPageHeight() : 0);
		this.resize();
	},
	updateRowCount: function(inRowCount){
		this.invalidateNodes();
		this.rowCount = inRowCount;
		// update page count, adjust document height
		oldPageCount = this.pageCount;
		this.pageCount = Math.ceil(this.rowCount / this.rowsPerPage);
		if(this.pageCount < oldPageCount){
			for(var i=oldPageCount-1; i>=this.pageCount; i--){
				this.height -= this.getPageHeight(i);
				delete this.pageHeights[i]
			}
		}else if(this.pageCount > oldPageCount){
			this.height += this.defaultPageHeight * (this.pageCount - oldPageCount - 1) + this.calcLastPageHeight();
		}
		this.resize();
	},
	// abstract interface
	pageExists: function(inPageIndex){
	},
	measurePage: function(inPageIndex){
	},
	positionPage: function(inPageIndex, inPos){
	},
	repositionPages: function(inPageIndex){
	},
	installPage: function(inPageIndex){
	},
	preparePage: function(inPageIndex, inPos, inReuseNode){
	},
	renderPage: function(inPageIndex){
	},
	removePage: function(inPageIndex){
	},
	pacify: function(inShouldPacify){
	},
	// pacification
	pacifying: false,
	pacifyTicks: 200,
	setPacifying: function(inPacifying){
		if(this.pacifying != inPacifying){
			this.pacifying = inPacifying;
			this.pacify(this.pacifying);
		}
	},
	startPacify: function(){
		this.startPacifyTicks = new Date().getTime();
	},
	doPacify: function(){
		var result = (new Date().getTime() - this.startPacifyTicks) > this.pacifyTicks;
		this.setPacifying(true);
		this.startPacify();
		return result;
	},
	endPacify: function(){
		this.setPacifying(false);
	},
	// default sizing implementation
	resize: function(){
		if(this.scrollboxNode){
			this.windowHeight = this.scrollboxNode.clientHeight;
		}
		dojox.grid.setStyleHeightPx(this.contentNode, this.height);
	},
	calcLastPageHeight: function(){
		if(!this.pageCount){
			return 0;
		}
		var lastPage = this.pageCount - 1;
		var lastPageHeight = ((this.rowCount % this.rowsPerPage)||(this.rowsPerPage)) * this.defaultRowHeight;
		this.pageHeights[lastPage] = lastPageHeight;
		return lastPageHeight;
	},
	updateContentHeight: function(inDh){
		this.height += inDh;
		this.resize();
	},
	updatePageHeight: function(inPageIndex){
		if(this.pageExists(inPageIndex)){
			var oh = this.getPageHeight(inPageIndex);
			var h = (this.measurePage(inPageIndex))||(oh);
			this.pageHeights[inPageIndex] = h;
			if((h)&&(oh != h)){
				this.updateContentHeight(h - oh)
				this.repositionPages(inPageIndex);
			}
		}
	},
	rowHeightChanged: function(inRowIndex){
		this.updatePageHeight(Math.floor(inRowIndex / this.rowsPerPage));
	},
	// scroller core
	invalidateNodes: function(){
		while(this.stack.length){
			this.destroyPage(this.popPage());
		}
	},
	createPageNode: function(){
		var p = document.createElement('div');
		p.style.position = 'absolute';
		//p.style.width = '100%';
		p.style[dojo._isBodyLtr() ? "left" : "right"] = '0';
		return p;
	},
	getPageHeight: function(inPageIndex){
		var ph = this.pageHeights[inPageIndex];
		return (ph !== undefined ? ph : this.defaultPageHeight);
	},
	// FIXME: this is not a stack, it's a FIFO list
	pushPage: function(inPageIndex){
		return this.stack.push(inPageIndex);
	},
	popPage: function(){
		return this.stack.shift();
	},
	findPage: function(inTop){
		var i = 0, h = 0;
		for(var ph = 0; i<this.pageCount; i++, h += ph){
			ph = this.getPageHeight(i);
			if(h + ph >= inTop){
				break;
			}
		}
		this.page = i;
		this.pageTop = h;
	},
	buildPage: function(inPageIndex, inReuseNode, inPos){
		this.preparePage(inPageIndex, inReuseNode);
		this.positionPage(inPageIndex, inPos);
		// order of operations is key below
		this.installPage(inPageIndex);
		this.renderPage(inPageIndex);
		// order of operations is key above
		this.pushPage(inPageIndex);
	},
	needPage: function(inPageIndex, inPos){
		var h = this.getPageHeight(inPageIndex), oh = h;
		if(!this.pageExists(inPageIndex)){
			this.buildPage(inPageIndex, this.keepPages&&(this.stack.length >= this.keepPages), inPos);
			h = this.measurePage(inPageIndex) || h;
			this.pageHeights[inPageIndex] = h;
			if(h && (oh != h)){
				this.updateContentHeight(h - oh)
			}
		}else{
			this.positionPage(inPageIndex, inPos);
		}
		return h;
	},
	onscroll: function(){
		this.scroll(this.scrollboxNode.scrollTop);
	},
	scroll: function(inTop){
		this.startPacify();
		this.findPage(inTop);
		var h = this.height;
		var b = this.getScrollBottom(inTop);
		for(var p=this.page, y=this.pageTop; (p<this.pageCount)&&((b<0)||(y<b)); p++){
			y += this.needPage(p, y);
		}
		this.firstVisibleRow = this.getFirstVisibleRow(this.page, this.pageTop, inTop);
		this.lastVisibleRow = this.getLastVisibleRow(p - 1, y, b);
		// indicates some page size has been updated
		if(h != this.height){
			this.repositionPages(p-1);
		}
		this.endPacify();
	},
	getScrollBottom: function(inTop){
		return (this.windowHeight >= 0 ? inTop + this.windowHeight : -1);
	},
	// events
	processNodeEvent: function(e, inNode){
		var t = e.target;
		while(t && (t != inNode) && t.parentNode && (t.parentNode.parentNode != inNode)){
			t = t.parentNode;
		}
		if(!t || !t.parentNode || (t.parentNode.parentNode != inNode)){
			return false;
		}
		var page = t.parentNode;
		e.topRowIndex = page.pageIndex * this.rowsPerPage;
		e.rowIndex = e.topRowIndex + dojox.grid.indexInParent(t);
		e.rowTarget = t;
		return true;
	},
	processEvent: function(e){
		return this.processNodeEvent(e, this.contentNode);
	},
	dummy: 0
});

dojo.declare('dojox.grid.scroller', dojox.grid.scroller.base, {
	// summary:
	//	virtual scroller class, makes no assumption about shape of items being scrolled
	constructor: function(){
		this.pageNodes = [];
	},
	// virtual rendering interface
	renderRow: function(inRowIndex, inPageNode){
	},
	removeRow: function(inRowIndex){
	},
	// page node operations
	getDefaultNodes: function(){
		return this.pageNodes;
	},
	getDefaultPageNode: function(inPageIndex){
		return this.getDefaultNodes()[inPageIndex];
	},
	positionPageNode: function(inNode, inPos){
		inNode.style.top = inPos + 'px';
	},
	getPageNodePosition: function(inNode){
		return inNode.offsetTop;
	},
	repositionPageNodes: function(inPageIndex, inNodes){
		var last = 0;
		for(var i=0; i<this.stack.length; i++){
			last = Math.max(this.stack[i], last);
		}
		//
		var n = inNodes[inPageIndex];
		var y = (n ? this.getPageNodePosition(n) + this.getPageHeight(inPageIndex) : 0);
		//console.log('detected height change, repositioning from #%d (%d) @ %d ', inPageIndex + 1, last, y, this.pageHeights[0]);
		//
		for(var p=inPageIndex+1; p<=last; p++){
			n = inNodes[p];
			if(n){
				//console.log('#%d @ %d', inPageIndex, y, this.getPageNodePosition(n));
				if(this.getPageNodePosition(n) == y){
					return;
				}
				//console.log('placing page %d at %d', p, y);
				this.positionPage(p, y);
			}
			y += this.getPageHeight(p);
		}
	},
	invalidatePageNode: function(inPageIndex, inNodes){
		var p = inNodes[inPageIndex];
		if(p){
			delete inNodes[inPageIndex];
			this.removePage(inPageIndex, p);
			dojox.grid.cleanNode(p);
			p.innerHTML = '';
		}
		return p;
	},
	preparePageNode: function(inPageIndex, inReusePageIndex, inNodes){
		var p = (inReusePageIndex === null ? this.createPageNode() : this.invalidatePageNode(inReusePageIndex, inNodes));
		p.pageIndex = inPageIndex;
		p.id = (this._pageIdPrefix || "") + 'page-' + inPageIndex;
		inNodes[inPageIndex] = p;
	},
	// implementation for page manager
	pageExists: function(inPageIndex){
		return Boolean(this.getDefaultPageNode(inPageIndex));
	},
	measurePage: function(inPageIndex){
		var p = this.getDefaultPageNode(inPageIndex);
		var h = p.offsetHeight;
		if(!this._defaultRowHeight){
			//The default sizes tend to vary, particularly on IE, so we
			//need to calculate a basic default size for a row.  Putting
			//a text character and using it to calculate a default row padding
			//in the div seems to handle this okay.  It keeps IE
			//from loading too much data.
			if(p){
				this._defaultRowHeight = 8;
				var fr = p.firstChild;
				if(fr){
					var text = dojo.doc.createTextNode("T");
					fr.appendChild(text);
					this._defaultRowHeight = fr.offsetHeight;
					fr.removeChild(text);
				}
			}
		}
		//If page height isn't very accurate (empty rows because data hasn't 
		//been populated yet and the size is far too small) we need to adjust 
		//it via the _defaultRowSize.
		return (this.rowsPerPage == h)?(h*this._defaultRowHeight):h;
	},
	positionPage: function(inPageIndex, inPos){
		this.positionPageNode(this.getDefaultPageNode(inPageIndex), inPos);
	},
	repositionPages: function(inPageIndex){
		this.repositionPageNodes(inPageIndex, this.getDefaultNodes());
	},
	preparePage: function(inPageIndex, inReuseNode){
		this.preparePageNode(inPageIndex, (inReuseNode ? this.popPage() : null), this.getDefaultNodes());
	},
	installPage: function(inPageIndex){
		this.contentNode.appendChild(this.getDefaultPageNode(inPageIndex));
	},
	destroyPage: function(inPageIndex){
		var p = this.invalidatePageNode(inPageIndex, this.getDefaultNodes());
		dojox.grid.removeNode(p);
	},
	// rendering implementation
	renderPage: function(inPageIndex){
		var node = this.pageNodes[inPageIndex];
		for(var i=0, j=inPageIndex*this.rowsPerPage; (i<this.rowsPerPage)&&(j<this.rowCount); i++, j++){
			this.renderRow(j, node);
		}
	},
	removePage: function(inPageIndex){
		for(var i=0, j=inPageIndex*this.rowsPerPage; i<this.rowsPerPage; i++, j++){
			this.removeRow(j);
		}
	},
	// scroll control
	getPageRow: function(inPage){
		return inPage * this.rowsPerPage;
	},
	getLastPageRow: function(inPage){
		return Math.min(this.rowCount, this.getPageRow(inPage + 1)) - 1;
	},
	getFirstVisibleRowNodes: function(inPage, inPageTop, inScrollTop, inNodes){
		var row = this.getPageRow(inPage);
		var rows = dojox.grid.divkids(inNodes[inPage]);
		for(var i=0,l=rows.length; i<l && inPageTop<inScrollTop; i++, row++){
			inPageTop += rows[i].offsetHeight;
		}
		return (row ? row - 1 : row);
	},
	getFirstVisibleRow: function(inPage, inPageTop, inScrollTop){
		if(!this.pageExists(inPage)){
			return 0;
		}
		return this.getFirstVisibleRowNodes(inPage, inPageTop, inScrollTop, this.getDefaultNodes());
	},
	getLastVisibleRowNodes: function(inPage, inBottom, inScrollBottom, inNodes){
		var row = this.getLastPageRow(inPage);
		var rows = dojox.grid.divkids(inNodes[inPage]);
		for(var i=rows.length-1; i>=0 && inBottom>inScrollBottom; i--, row--){
			inBottom -= rows[i].offsetHeight;
		}
		return row + 1;
	},
	getLastVisibleRow: function(inPage, inBottom, inScrollBottom){
		if(!this.pageExists(inPage)){
			return 0;
		}
		return this.getLastVisibleRowNodes(inPage, inBottom, inScrollBottom, this.getDefaultNodes());
	},
	findTopRowForNodes: function(inScrollTop, inNodes){
		var rows = dojox.grid.divkids(inNodes[this.page]);
		for(var i=0,l=rows.length,t=this.pageTop,h; i<l; i++){
			h = rows[i].offsetHeight;
			t += h;
			if(t >= inScrollTop){
				this.offset = h - (t - inScrollTop);
				return i + this.page * this.rowsPerPage;
			}
		}
		return -1;
	},
	findScrollTopForNodes: function(inRow, inNodes){
		var rowPage = Math.floor(inRow / this.rowsPerPage);
		var t = 0;
		for(var i=0; i<rowPage; i++){
			t += this.getPageHeight(i);
		}
		this.pageTop = t;
		this.needPage(rowPage, this.pageTop);
		var rows = dojox.grid.divkids(inNodes[rowPage]);
		var r = inRow - this.rowsPerPage * rowPage;
		for(var i=0,l=rows.length; i<l && i<r; i++){
			t += rows[i].offsetHeight;
		}
		return t;
	},
	findTopRow: function(inScrollTop){
		return this.findTopRowForNodes(inScrollTop, this.getDefaultNodes());
	},
	findScrollTop: function(inRow){
		return this.findScrollTopForNodes(inRow, this.getDefaultNodes());
	},
	dummy: 0
});

dojo.declare('dojox.grid.scroller.columns', dojox.grid.scroller, {
	// summary:
	//	Virtual scroller class that scrolls list of columns. Owned by grid and used internally
	//	for virtual scrolling.
	constructor: function(inContentNodes){
		this.setContentNodes(inContentNodes);
	},
	// nodes
	setContentNodes: function(inNodes){
		this.contentNodes = inNodes;
		this.colCount = (this.contentNodes ? this.contentNodes.length : 0);
		this.pageNodes = [];
		for(var i=0; i<this.colCount; i++){
			this.pageNodes[i] = [];
		}
	},
	getDefaultNodes: function(){
		return this.pageNodes[0] || [];
	},
	scroll: function(inTop) {
		if(this.colCount){
			dojox.grid.scroller.prototype.scroll.call(this, inTop);
		}
	},
	// resize
	resize: function(){
		if(this.scrollboxNode){
			this.windowHeight = this.scrollboxNode.clientHeight;
		}
		for(var i=0; i<this.colCount; i++){
			dojox.grid.setStyleHeightPx(this.contentNodes[i], this.height);
		}
	},
	// implementation for page manager
	positionPage: function(inPageIndex, inPos){
		for(var i=0; i<this.colCount; i++){
			this.positionPageNode(this.pageNodes[i][inPageIndex], inPos);
		}
	},
	preparePage: function(inPageIndex, inReuseNode){
		var p = (inReuseNode ? this.popPage() : null);
		for(var i=0; i<this.colCount; i++){
			this.preparePageNode(inPageIndex, p, this.pageNodes[i]);
		}
	},
	installPage: function(inPageIndex){
		for(var i=0; i<this.colCount; i++){
			this.contentNodes[i].appendChild(this.pageNodes[i][inPageIndex]);
		}
	},
	destroyPage: function(inPageIndex){
		for(var i=0; i<this.colCount; i++){
			dojox.grid.removeNode(this.invalidatePageNode(inPageIndex, this.pageNodes[i]));
		}
	},
	// rendering implementation
	renderPage: function(inPageIndex){
		var nodes = [];
		for(var i=0; i<this.colCount; i++){
			nodes[i] = this.pageNodes[i][inPageIndex];
		}
		//this.renderRows(inPageIndex*this.rowsPerPage, this.rowsPerPage, nodes);
		for(var i=0, j=inPageIndex*this.rowsPerPage; (i<this.rowsPerPage)&&(j<this.rowCount); i++, j++){
			this.renderRow(j, nodes);
		}
	}
});