// Copyright 2007 Sheepshank. All rights reserved.
// THOR base and utils.
// Depends on YAHOO.

if (typeof THOR == "undefined") {
  THOR = {
    log: function(s) {
      YAHOO.log(s, THOR.log.where, THOR.log.where);
    },

    bolt: function(obj, aa) {
      for (var key in aa) {
	obj[key] = aa[key];
      }
    },

    extend: function(clsSub, clsSup, aa) {
      YAHOO.extend(clsSub, clsSup);
      THOR.bolt(clsSub.prototype, aa);
    }

  };
}

THOR.log.where = 'thor.js';

/////////////// Common utils

if (!THOR.utils) {
  THOR.utils = {};
}

var _bolt = {
  fnCurried: function(fn) {
    var arArgPartial = Array.prototype.slice.call(arguments, 1);
    return this.fnCurriedScoped.apply(this, [fn, null].concat(arArgPartial));
  },

  fnCurriedScoped: function(fn, obj) {
    var arArgPartial = Array.prototype.slice.call(arguments, 2);

    return function() {
      var arArg = arArgPartial.concat(Array.prototype.slice.apply(arguments));
      return fn.apply(obj, arArg);
    };
  },

  boltObservable: {
    addObserver: function(obj) {
      try {
	this._arObserver.push(obj);
      } catch(e) {
	this._arObserver = [obj];
      }
      this.notifyStatus && this.notifyStatus();
    },

    removeObserver: function(obj) {
      if (this._arObserver && this._arObserver.length > 0) {
	for (var ix = this._arObserver.length - 1; ix >= 0; ix--) {
	  if (this._arObserver[ix] == obj) {
	    this._arObserver.splice(ix, 1);
	  }
	}
      }
    },

    notifyObservers: function(s) {
      try {
	var arArg = Array.prototype.slice.call(arguments, 1);
	for (var ix = this._arObserver.length - 1; ix >= 0; ix--) {
	  var obj = this._arObserver[ix];
	  var fn = obj['on' + s];
	  fn && fn.apply(obj, [this].concat(arArg));
	}
      } catch(e) { }
    }
  }

};

THOR.bolt(THOR.utils, _bolt);
delete _bolt;

////////// Dom 'static' methods

THOR.utils.Dom = {
  dxdyIncBorderPadding: function(elt) {
    var arW = ['width', 'padding-left', 'padding-right',
	       'border-left-width', 'border-right-width'];
    var arH = ['height', 'padding-top', 'padding-bottom',
	       'border-top-width', 'border-bottom-width'];

    var dx = 0;
    var dy = 0;
    for (var ix = 0; ix < arW.length; ix++) {
      dx += (parseInt(YAHOO.util.Dom.getStyle(elt, arW[ix])) || 0);
    }
    for (var ix = 0; ix < arH.length; ix++) {
      dy += (parseInt(YAHOO.util.Dom.getStyle(elt, arH[ix])) || 0);
    }

    return [dx, dy];
  },

  arSizeEdge: function(elt) {
    var arSize = [];
    var arEdge = ['top', 'right', 'bottom', 'left'];

    
    for (var ix = 0; ix < arEdge.length; ix++) {
      var size = 0;
      size += (parseInt(YAHOO.util.Dom.getStyle(elt, 'padding-' + arEdge[ix])) || 0); 
      size += (parseInt(YAHOO.util.Dom.getStyle(elt, 'border-' + arEdge[ix] + '-width')) || 0);

      arSize.push(size);
    }

    return arSize;
  }
};

////////// WaiterView - standard view to display wait cursor when needed.

THOR.utils.WaiterView = function(query, id) {
  this.query = query;
  this.elt = document.getElementById(id);

  this.query.addObserver(this);
};

THOR.utils.WaiterView.prototype = {
  sClassCSSWaiting: 'waiting',

  onInitialising: function() {
    YAHOO.util.Dom.addClass(this.elt, this.sClassCSSWaiting);
  },

  onSubmitting: function() {
    YAHOO.util.Dom.addClass(this.elt, this.sClassCSSWaiting);
  },

  onDataReady: function() {
    YAHOO.util.Dom.removeClass(this.elt, this.sClassCSSWaiting);
  },

  onSubmitError: function() {
    YAHOO.util.Dom.removeClass(this.elt, this.sClassCSSWaiting);
  },

  onDestroy: function() {
    YAHOO.util.Dom.removeClass(this.elt, this.sClassCSSWaiting);
    this.query.removeObserver(this);
  }
};



////////// Image loader with placeholder

THOR.utils.ImageLoader = function(url, eltParent, bAnim) {
  this.url = url;
  this.eltParent = eltParent;
  this.bAnimCreate = !!bAnim;

  this.elt = null;
  this.createProxyElt();
  this.load();
};

THOR.utils.ImageLoader.prototype = {
  createProxyElt: function() {
    var elt = document.createElement('div');
    elt.className = 'imageProxy';
    this.eltParent.appendChild(elt);

    this.eltProxy = elt;
  },

  load: function() {
    this.elt = document.createElement('img');
    this.elt.onload = THOR.utils.fnCurriedScoped(this.onLoad, this);
    this.elt.src = this.url;
  },

  secAnim: 0.25,

  onLoad: function() {
    if (this.elt == null) {
      return; // we've been destroyed before the image has arrived
    }

    this.dxdyParent = [
      parseInt(YAHOO.util.Dom.getStyle(this.eltParent, 'width')),
      parseInt(YAHOO.util.Dom.getStyle(this.eltParent, 'height'))
      ];

    // IE: when going back/next between pages, onLoad can fire before
    // the parent element is attached to the page; in this case, we get
    // back 'auto' for width/height, which parses to NaN.
    // Sit and wait for a time, and try again.
    if (isNaN(this.dxdyParent[0]) || isNaN(this.dxdyParent[1])) {
      setTimeout(this.elt.onload, 50);
      return;
    }

    this.eltParent.removeChild(this.eltProxy);

    this.elt.style.position = 'absolute';
    YAHOO.util.Dom.setStyle(this.elt, 'opacity', 0);
    this.eltParent.appendChild(this.elt);

    // separate method for easy extension.
    this.setImagePosition();

    if (this.bAnimCreate) {
      var anim = new YAHOO.util.Anim(this.elt, { 
	opacity: { to: 1 }
	},
	this.secAnim
	);
      anim.animate();
      
    } else {
      YAHOO.util.Dom.setStyle(this.elt, 'opacity', 1);
    }
  },

  setImagePosition: function() {
    // Webkit/Safari bug 8087: img width and height 0 when creating
    // elt dynamically. Render the measured element anyway, so we can
    // take any border etc into account.
    var dxdy = THOR.utils.Dom.dxdyIncBorderPadding(this.elt);

    var x = (this.dxdyParent[0] - dxdy[0]) / 2;
    var y = (this.dxdyParent[1] - dxdy[1]) / 2;

    this.elt.style.left = x + 'px';
    this.elt.style.top = y + 'px';
  },

  destroy: function(fnOnComplete, bAnimDestroy) {
    if (bAnimDestroy) {
      var anim = new YAHOO.util.Anim(this.elt, { 
	opacity: { to: 0 }
	},
	this.secAnim
	);
      var fn = THOR.utils.fnCurriedScoped(this._destroy, this, fnOnComplete);
      anim.onComplete.subscribe(fn);
      anim.animate();
    } else {
      this._destroy(fnOnComplete);
    }
  },

  _destroy: function(fnOnComplete) {
    if (this.eltProxy) {
      if (this.eltProxy.parentNode) {
	this.eltProxy.parentNode.removeChild(this.eltProxy);
      }
      this.eltProxy = null;
    }

    if (this.elt) {
      if (this.elt.parentNode) {
	this.elt.parentNode.removeChild(this.elt);
      }
      this.elt = null;
    }

    fnOnComplete && fnOnComplete();
  }
};


/////// Simple extension for bottom-alignment of image with previous sibling

THOR.utils.AlignedImageLoader = function() {
  THOR.utils.AlignedImageLoader.superclass.constructor.apply(this, arguments);
};

var _extend = {
  setImagePosition: function() {
    // Webkit/Safari bug 8087: img width and height 0 when creating
    // elt dynamically. Render the measured element anyway, so we can
    // take any border etc into account.
    var dxdy = THOR.utils.Dom.dxdyIncBorderPadding(this.elt);
    
    var x = (this.dxdyParent[0] - dxdy[0]) / 2;

    // Position above the previous sibling, the p title etc.
    var yOther = parseInt(YAHOO.util.Dom.getStyle(this.elt.previousSibling, 'top'));
    var y = yOther - dxdy[1];

    this.elt.style.left = x + 'px';
    this.elt.style.top = y + 'px';
  }
};

THOR.extend(THOR.utils.AlignedImageLoader, THOR.utils.ImageLoader, _extend);
delete _extend;


////////// JSON fetcher, supports get/post to the server

THOR.utils.JSONConnect = function(aaSource, param) {
  if (param != undefined) {
    this.url = aaSource.url.replace(/%d/, param);
  } else {
    this.url = aaSource.url;
  }
};

THOR.utils.JSONConnect.prototype = {
  fetchData: function(arg) {
    var aaCallback = {
      success: this._onDataReceived,
      failure: this._onFailure,
      scope: this,
      argument: arg
    };

    this.conn = YAHOO.util.Connect.asyncRequest('GET', this.url, aaCallback); 
  },

  submit: function(form) {
    var aaCallback = {
      upload: this._onDataReceived,
      scope: this
    };

    YAHOO.util.Connect.setForm(form, true); 
    this.conn = YAHOO.util.Connect.asyncRequest('POST', this.url, aaCallback); 
  },

  abort: function() {
    YAHOO.util.Connect.abort(this.conn);
  },

  _onDataReceived: function(oResponse) {
    var aaResponse = eval('(' + oResponse.responseText + ')');
    var arg = oResponse.argument;
    this.notifyObservers('NewData', aaResponse, arg);
  },

  _onFailure: function(oResponse) {
    var arg = oResponse.argument;

    if (oResponse.status == 0) {
      this.notifyObservers('ConnectFailure', arg);
    } else {
      this.notifyObservers('Aborted', arg);
    }
  }
};

THOR.bolt(THOR.utils.JSONConnect.prototype, THOR.utils.boltObservable);


////////// JSON fetcher, supports get - but from a variable

THOR.utils.JSONVar = function(aaSource) {
  // any second argument is ignored.
  this._data = aaSource.variable;
};

THOR.utils.JSONVar.prototype = {
  fetchData: function(arg) {
    this.notifyObservers('NewData', this._data, arg);
  },

  submit: function() {},  // not supported
  abort: function() {}  // noop
};

THOR.bolt(THOR.utils.JSONVar.prototype, THOR.utils.boltObservable);


////////// General-purpose cache

THOR.utils.Cache = function(ctCxMax) {
  // How many objects to cache. Objects are opaque to us.
  this.ctCxMax = ctCxMax;

  // The cache indexes, in order: oldest to newest
  this._arCx = [];
  // The cached objects, indexed by cache index.
  this.aaObjByCx = {};
};

THOR.utils.Cache.prototype = {
  addCxObj: function(cx, obj) {
    if (this.bContainsCx(cx)) {
      this.removeCx(cx);
    }

    if (this._arCx.length >= this.ctCxMax) {
      this.arCxRemoveOldestCt(this._arCx.length - this.ctCxMax + 1);
    }

    this.aaObjByCx[cx] = obj;
    this._arCx.push(cx);
  },

  bContainsCx: function(cx) {
    return !!(this.aaObjByCx[cx]);
  },

  objFromCx: function(cx) {
    if (this.bContainsCx(cx)) {
      this.touchCx(cx);
      return this.aaObjByCx[cx];
    }
    return null;
  },

  touchCx: function(cx) {
    // make cx the head of the cache.
    for (var ix = this._arCx.length - 1; ix >= 0; ix--) {
      if (this._arCx[ix] == cx) {
	this._arCx.splice(ix, 1);
	this._arCx.push(cx);
	break;
      }
    }
  },

  arCxRemoveOldestCt: function(ct) {
    var ct = Math.min(ct, this._arCx.length);

    if (ct > 0) {
      var arCxRemoved = this._arCx.splice(0, ct);

      for (var ix = 0; ix < arCxRemoved.length; ix++) {
	var obj = this.aaObjByCx[arCxRemoved[ix]];
	delete this.aaObjByCx[arCxRemoved[ix]];
	this.destroyObj(obj);
      }

      return arCxRemoved;
    }
    return [];
  },

  cxRemoveOldest: function() {
    return this.arCxRemoveOldestCt(1)[0];
  },

  removeCx: function(cx) {
    for (var ix = this._arCx.length - 1; ix >= 0; ix--) {
      if (this._arCx[ix] == cx) {
	this._arCx.splice(ix, 1);
	var obj = this.aaObjByCx[cx];
	delete this.aaObjByCx[cx];
	this.destroyObj(obj);
	break;
      }
    }
  },

  destroyObj: function(obj) {
    // filled in if required by others.
  },

  destroy: function() {
    this.arCxRemoveOldestCt(this._arCx.length);
  }
};

////////// Caching of objects found through JSON requests.

THOR.utils.QueryCache = function(aaSource, clsProxy, clsObj, ctCx) {
  this.aaSource = aaSource;
  this.clsProxy = clsProxy;
  this.clsObj = clsObj;
  this.ctCx = ctCx;

  this._aaFetcherByCx = {};
  this.bFetchAllowed = true;

  this.cache = new this.clsCache(ctCx)
  this.cache.destroyObj = this.destroyCacheObj;
};

THOR.utils.QueryCache.prototype = {
  clsCache: THOR.utils.Cache,

  init: function() {
    this.notifyObservers('Initialising');
    this._fetchCx(0);
  },

  destroy: function() {
    this.bFetchAllowed = false;

    // abort any fetches in progress
    for (var cx in this._aaFetcherByCx) {
      this._aaFetcherByCx[cx].abort();
    }
    
    this.cache.destroy();
  },

  destroyCacheObj: function(arObj) {
    for (var ix = 0; ix < arObj.length; ix++) {
      arObj[ix].destroy && arObj[ix].destroy();
    }
  },

  objByRx: function(rx) {
    this.throwIfInvalidRx(rx);

    var cx = Math.floor(rx / this.ctRxPerCx);
    var rxInCx = rx % this.ctRxPerCx;

    if (!this.cache.objFromCx(cx)) {
      this._fetchCx(cx);
    }

    // there's something there now if there wasn't before.
    return this.cache.objFromCx(cx)[rxInCx];
  },

  throwIfInvalidRx: function(rx) {
    if (!this.ctRx) {
      throw new Error('Data not ready.');
    }

    if (isNaN(rx) || rx < 0 || rx >= this.ctRx) {
      throw new Error('Invalid index');
    }
  },

  _fetchCx: function(cx) {
    if (!this.bFetchAllowed) {
      return;
    }

    if (this.ctRx && (cx * this.ctRxPerCx) >= this.ctRx) {
      return; // requested cx out of range
    }

    if (this._aaFetcherByCx[cx]) {
      return; // already fetching this cx.
    }

    var fetcher = new this.aaSource.clsFetcher(this.aaSource, cx + 1);
    fetcher.addObserver(this);

    if (this.ctRx) {
      this.cache.addCxObj(cx, this._arProxyFromCx(cx));
    }

    this._aaFetcherByCx[cx] = fetcher;
    fetcher.fetchData(cx);
  },

  onNewData: function(obj, data, cx) {
    // NB may be > 1 request on the go at once.
    
    var fetcher = this._aaFetcherByCx[cx];

    if (!fetcher) {
      return; // we didn't ask for this!
    }
    fetcher.removeObserver(this);
    delete this._aaFetcherByCx[cx];

    if (cx != data.meta.numPage - 1) {
      this._uncacheIfProxyAtCx(cx);
      return; // a set of results we didn't ask for
    }

    if (!this.ctRx) {
      // need to do this before we cache the objects, as they
      // may need to know various metadata.
      this.meta = data.meta;
      this.ctRxPerCx = data.arResult.length;
      this.ctRx = data.meta.ctResult;
      var bNotifyDataReady = true;
    } else {
      var bNotifyDataReady = false;
    }

    var arObj = this._arObjFromCxData(cx, data);
    this.cache.addCxObj(cx, arObj);

    if (bNotifyDataReady) {
      this.notifyObservers('DataReady');
    }

    this.notifyObservers('RxRangeReceived',
			 cx * this.ctRxPerCx,
			 arObj.length);
  },

  onAborted: function(obj, cx) {
    // called by fetcher on manual or other abort 
    this._aaFetcherByCx[cx].removeObserver(this);
    delete this._aaFetcherByCx[cx];

    this._uncacheIfProxyAtCx(cx);
  },

  _uncacheIfProxyAtCx: function(cx) {
    // On receipt of wrong data or on abort, remove any proxies sitting
    // in the cache waiting to be replaced.
    if (this.ctRx && this.cache.aaObjByCx[cx][0] instanceof this.clsProxy) {
      var ctRx = this.cache.objFromCx(cx).length;

      this.cache.removeCx(cx);

      this.notifyObservers('RxRangeCancelled',
			   cx * this.ctRxPerCx,
			   ctRx);
    }
  },

  _arProxyFromCx: function(cx) {
    var ar = [];
    var rxMin = cx * this.ctRxPerCx;
    var rxMaxExc = Math.min(rxMin + this.ctRxPerCx, this.ctRx);

    for (var rx = rxMin; rx < rxMaxExc; rx++) {
      ar.push(new this.clsProxy(rx));
    }

    return ar;
  },

  _arObjFromCxData: function(cx, data) {
    var ar = [];
    var rxMin = cx * this.ctRxPerCx;

    for (var ix = 0; ix < data.arResult.length; ix++) {
      ar.push(new this.clsObj(data.arResult[ix], rxMin + ix));
    }

    return ar;
  }
};

THOR.bolt(THOR.utils.QueryCache.prototype, THOR.utils.boltObservable);


////////// Query - wraps QueryCache, loads and returns results for a query. Prefetches.

THOR.utils.Query = function(clsProxy, clsObj) {
  this.clsProxy = clsProxy;
  this.clsObj = clsObj;
};

THOR.utils.Query.prototype = {
  clsQueryCache: THOR.utils.QueryCache,
  ctCx: 10,
  msPrefetch: 500,

  load: function(aaSource) {
    this.rxCurrent = undefined;
    this.ctRx = undefined;
    this._arRxWaiting = [];

    // !!!TODO: cache these by URL?
    this.oc = new this.clsQueryCache(aaSource, 
				     this.clsProxy, this.clsObj,
				     this.ctCx);
    this.oc.addObserver(this); // calls this.notifyStatus

    this.oc.init();
  },

  notifyStatus: function() {
    // called automatically by this.oc when we start observing.
    if (isNaN(this.ctRx)) {
      this.notifyObservers('Initialising');
    } else {
      this.notifyObservers('DataReady');
    }
  },

  destroy: function() {
    this.notifyObservers('Destroy');

    if (this.oc) {
      this.oc.destroy();
    }
  },

  onInitialising: function(obj) {
    this.notifyStatus();
  },

  onDataReady: function() {
    this.ctRx = this.oc.ctRx;
    this.rxCurrent = 0;
    this.meta = this.oc.meta;

    this.notifyStatus();
  },

  onRxRangeReceived: function(obj, rxStart, ctRx) {
    for (var ix = this._arRxWaiting.length - 1; ix >= 0; ix--) {
      var rx = this._arRxWaiting[ix]
      if (rx >= rxStart && rx < rxStart + ctRx) {
	this._arRxWaiting.splice(ix, 1);
	this.notifyObservers('ObjChanged', rx);
      }
    }
  },

  setCurrentRx: function(rx) {
    this.oc.throwIfInvalidRx(rx);
    this.rxCurrent = rx;

    this.notifyObservers('RxChanged');
  },

  objByRx: function(rx) {
    this.oc.throwIfInvalidRx(rx);
    var obj = this.oc.objByRx(rx);

    if (obj instanceof this.clsProxy) {
      this._arRxWaiting.unshift(rx);
    }

    this._prefetch(rx + this.oc.ctRxPerCx, this.msPrefetch);
    this._prefetch(rx - this.oc.ctRxPerCx, this.msPrefetch * 2);

    return obj;
  },

  _prefetch: function(rx, ms) {
    var fn = function() {
      try {
	var obj = this.oc.objByRx(rx)
      } catch(e) {
      }
    };
    fn = THOR.utils.fnCurriedScoped(fn, this);

    setTimeout(fn, ms);
  }
};

THOR.bolt(THOR.utils.Query.prototype, THOR.utils.boltObservable);



////////// ScrollView - subclassed if so desired

THOR.utils.ScrollView = function(query, idParent) {
  this.query = query;
  this.eltParent = document.getElementById(idParent);

  this.query.addObserver(this);
};

THOR.utils.ScrollView.prototype = {
  pxMargin: 8,
  secPerAnim: 0.5,
  bAnim: true,
  sClassCSS: 'object',

  onInitialising: function() {
    // may happen at any time: may need to throw away current
    // set of elts.
    if (this.rxMin != undefined) {
      this._destroyElts();
    }
  },

  onDataReady: function() {
    // Must do this here, not onInitialising, since the query may have
    // already initialised when the view starts observing it.
    this._passGridSizeToQuery();

    this._determineRange();
    this._createElts();

    // First time, no animation.
    var bAnim = this.bAnim;
    this.bAnim = false;
    this.onRxChanged(); // don't get this automatically here.
    this.bAnim = bAnim;
  },

  onObjChanged: function(obj, rx) {
    this._removeEltForRx(rx);
    this._addEltForRx(rx);
  },

  onRxChanged: function() {
    var y = Math.floor(-this.query.rxCurrent / this.query.ctObjPerRow) * this.dyImage;

    if (this.bAnim) {
      var anim = new YAHOO.util.Anim(this.eltScroller, { 
	top: { to: y }
	},
	this.secPerAnim,
	YAHOO.util.Easing.easeBoth
	);

	anim.animate();
    } else {
      this.eltScroller.style.top = y + 'px';
    }

    this._slideRange();
  },

  onDestroy: function() {
    this._destroyElts();
    this.query.removeObserver(this);
  },

  _passGridSizeToQuery: function() {
    // Place a dummy element into the parent to determine its size.
    var elt = document.createElement('div');
    elt.className = this.sClassCSS;
    elt.style.visibility = 'hidden';

    this.eltParent.appendChild(elt);

    var dxdyImage = THOR.utils.Dom.dxdyIncBorderPadding(elt);
    var dxImage = dxdyImage[0] + this.pxMargin; // left
    var dyImage = dxdyImage[1] + this.pxMargin; // top

    this.eltParent.removeChild(elt); // no longer required.

    // And the 'canvas' size, assuming no padding.
    var dxParent = parseInt(YAHOO.util.Dom.getStyle(this.eltParent, 'width'));
    var dyParent = parseInt(YAHOO.util.Dom.getStyle(this.eltParent, 'height'));
    
    this.query.ctObjPerRow = Math.floor(dxParent / dxImage);
    this.query.ctRowVisible = Math.floor(dyParent / dyImage);

    this.dxImage = dxImage;
    this.dyImage = dyImage;
  },

  _xyFromRx: function(rx) {
    // Position of final image, taking margin into account.
    var ctObjPerRow = this.query.ctObjPerRow;

    var y = Math.floor(rx / ctObjPerRow) * this.dyImage + this.pxMargin;
    var x = (rx % ctObjPerRow) * this.dxImage + this.pxMargin;

    return [x, y];
  },

  _determineRange: function() {
    var rx = this.query.rxCurrent;

    // Back one visible page
    var ctObjVisible = this.query.ctObjPerRow * this.query.ctRowVisible; 
    this.rxMin = rx - ctObjVisible;
    this.rxMax = rx + 2 * ctObjVisible - 1; // inclusive

    if (this.rxMin < 0) {
      // Slide the window along so it doesn't go before zero.
      this.rxMax -= this.rxMin;
      this.rxMin = 0;
    }
    // And doesn't go further than the end of data.
    this.rxMax = Math.min(this.rxMax, this.query.ctRx - 1);
  },

  _createElts: function() {
    // Everything is created inside a scroller element.
    var eltScroller = document.createElement('div');
    eltScroller.className = 'scroller';

    this.eltParent.appendChild(eltScroller);
    this.eltScroller = eltScroller;

    this._aaEltByRx = {};

    for (var rx = this.rxMin; rx <= this.rxMax; rx++) {
      this._addEltForRx(rx);
    }
  },

  _addEltForRx: function(rx) {
    var xy = this._xyFromRx(rx);

    var elt = this.query.objByRx(rx).elt();
    
    YAHOO.util.Dom.addClass(elt, this.sClassCSS);
    elt.style.left = xy[0] + 'px';
    elt.style.top = xy[1] + 'px';
    this.eltScroller.appendChild(elt);

    this._aaEltByRx[rx] = elt;
  },

  _removeEltForRx: function(rx) {
    this.eltScroller.removeChild(this._aaEltByRx[rx]);
    delete this._aaEltByRx[rx];
  },

  _destroyElts: function() {
    for (var rx = this.rxMin; rx <= this.rxMax; rx++) {
      this._removeEltForRx(rx);
    }

    this._aaEltByRx = {};

    // Destroy the scroller in which all those elements lived.
    this.eltParent.removeChild(this.eltScroller);
  },

  _slideRange: function() {
    var rxMinOld = this.rxMin;
    var rxMaxOld = this.rxMax;

    // Sets desired new rxMin, rxMax
    this._determineRange();

    if (rxMinOld == this.rxMin && rxMaxOld == this.rxMax) {
      // no change
      return;
    }

    if (rxMaxOld < this.rxMin || rxMinOld > this.rxMax) {
      // no intersection between old and new
      for (var rx = this.rxMin; rx <= this.rxMax; rx++) {
	this._addEltForRx(rx);
      }
      for (var rx = rxMinOld; rx <= rxMaxOld; rx++) {
	this._removeEltForRx(rx);
      }
      return;
    }

    // There's overlap between old and new.
    // Keep the intersection, replace the rest.

    // Add missing elements for the new range.
    if (this.rxMin < rxMinOld) {
      // Add elts for rxMin .. rxMinOld-exclusive.
      for (var rx = this.rxMin; rx < rxMinOld; rx++) {
	this._addEltForRx(rx);
      }
    }

    if (this.rxMax > rxMaxOld) {
      // Add elts for rxMaxOld-exclusive ... rxMax.
      for (var rx = rxMaxOld + 1; rx <= this.rxMax; rx++) {
	this._addEltForRx(rx);
      }
    }
    
    // Now delete the elements outside the new range.
    if (this.rxMin > rxMinOld) {
      // Delete elts for rxMinOld .. rxMin-exclusive.
      for (var rx = rxMinOld; rx < this.rxMin; rx++) {
	this._removeEltForRx(rx);
      }
    }

    if (this.rxMax < rxMaxOld) {
      // Delete elts for rxMax-exclusive .. rxMaxOld.
      for (var rx = this.rxMax + 1; rx <= rxMaxOld; rx++) {
	this._removeEltForRx(rx);
      }
    }
  }

};


// EOF
