392 lines
11 KiB
JavaScript
392 lines
11 KiB
JavaScript
/**
|
|
* scrollablebox.js - scrollable box element for blessed
|
|
* Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
|
|
* https://github.com/chjj/blessed
|
|
*/
|
|
|
|
/**
|
|
* Modules
|
|
*/
|
|
|
|
var helpers = require('../helpers');
|
|
|
|
var Node = require('./node');
|
|
var Box = require('./box');
|
|
|
|
/**
|
|
* ScrollableBox
|
|
*/
|
|
|
|
function ScrollableBox(options) {
|
|
var self = this;
|
|
|
|
if (!(this instanceof Node)) {
|
|
return new ScrollableBox(options);
|
|
}
|
|
|
|
options = options || {};
|
|
|
|
Box.call(this, options);
|
|
|
|
if (options.scrollable === false) {
|
|
return this;
|
|
}
|
|
|
|
this.scrollable = true;
|
|
this.childOffset = 0;
|
|
this.childBase = 0;
|
|
this.baseLimit = options.baseLimit || Infinity;
|
|
this.alwaysScroll = options.alwaysScroll;
|
|
|
|
this.scrollbar = options.scrollbar;
|
|
if (this.scrollbar) {
|
|
this.scrollbar.ch = this.scrollbar.ch || ' ';
|
|
this.style.scrollbar = this.style.scrollbar || this.scrollbar.style;
|
|
if (!this.style.scrollbar) {
|
|
this.style.scrollbar = {};
|
|
this.style.scrollbar.fg = this.scrollbar.fg;
|
|
this.style.scrollbar.bg = this.scrollbar.bg;
|
|
this.style.scrollbar.bold = this.scrollbar.bold;
|
|
this.style.scrollbar.underline = this.scrollbar.underline;
|
|
this.style.scrollbar.inverse = this.scrollbar.inverse;
|
|
this.style.scrollbar.invisible = this.scrollbar.invisible;
|
|
}
|
|
//this.scrollbar.style = this.style.scrollbar;
|
|
if (this.track || this.scrollbar.track) {
|
|
this.track = this.scrollbar.track || this.track;
|
|
this.style.track = this.style.scrollbar.track || this.style.track;
|
|
this.track.ch = this.track.ch || ' ';
|
|
this.style.track = this.style.track || this.track.style;
|
|
if (!this.style.track) {
|
|
this.style.track = {};
|
|
this.style.track.fg = this.track.fg;
|
|
this.style.track.bg = this.track.bg;
|
|
this.style.track.bold = this.track.bold;
|
|
this.style.track.underline = this.track.underline;
|
|
this.style.track.inverse = this.track.inverse;
|
|
this.style.track.invisible = this.track.invisible;
|
|
}
|
|
this.track.style = this.style.track;
|
|
}
|
|
// Allow controlling of the scrollbar via the mouse:
|
|
if (options.mouse) {
|
|
this.on('mousedown', function(data) {
|
|
if (self._scrollingBar) {
|
|
// Do not allow dragging on the scrollbar:
|
|
delete self.screen._dragging;
|
|
delete self._drag;
|
|
return;
|
|
}
|
|
var x = data.x - self.aleft;
|
|
var y = data.y - self.atop;
|
|
if (x === self.width - self.iright - 1) {
|
|
// Do not allow dragging on the scrollbar:
|
|
delete self.screen._dragging;
|
|
delete self._drag;
|
|
var perc = (y - self.itop) / (self.height - self.iheight);
|
|
self.setScrollPerc(perc * 100 | 0);
|
|
self.screen.render();
|
|
var smd, smu;
|
|
self._scrollingBar = true;
|
|
self.onScreenEvent('mousedown', smd = function(data) {
|
|
var y = data.y - self.atop;
|
|
var perc = y / self.height;
|
|
self.setScrollPerc(perc * 100 | 0);
|
|
self.screen.render();
|
|
});
|
|
// If mouseup occurs out of the window, no mouseup event fires, and
|
|
// scrollbar will drag again on mousedown until another mouseup
|
|
// occurs.
|
|
self.onScreenEvent('mouseup', smu = function(data) {
|
|
self._scrollingBar = false;
|
|
self.removeScreenEvent('mousedown', smd);
|
|
self.removeScreenEvent('mouseup', smu);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (options.mouse) {
|
|
this.on('wheeldown', function(el, data) {
|
|
self.scroll(self.height / 2 | 0 || 1);
|
|
self.screen.render();
|
|
});
|
|
this.on('wheelup', function(el, data) {
|
|
self.scroll(-(self.height / 2 | 0) || -1);
|
|
self.screen.render();
|
|
});
|
|
}
|
|
|
|
if (options.keys && !options.ignoreKeys) {
|
|
this.on('keypress', function(ch, key) {
|
|
if (key.name === 'up' || (options.vi && key.name === 'k')) {
|
|
self.scroll(-1);
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
if (key.name === 'down' || (options.vi && key.name === 'j')) {
|
|
self.scroll(1);
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
if (options.vi && key.name === 'u' && key.ctrl) {
|
|
self.scroll(-(self.height / 2 | 0) || -1);
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
if (options.vi && key.name === 'd' && key.ctrl) {
|
|
self.scroll(self.height / 2 | 0 || 1);
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
if (options.vi && key.name === 'b' && key.ctrl) {
|
|
self.scroll(-self.height || -1);
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
if (options.vi && key.name === 'f' && key.ctrl) {
|
|
self.scroll(self.height || 1);
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
if (options.vi && key.name === 'g' && !key.shift) {
|
|
self.scrollTo(0);
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
if (options.vi && key.name === 'g' && key.shift) {
|
|
self.scrollTo(self.getScrollHeight());
|
|
self.screen.render();
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
this.on('parsed content', function() {
|
|
self._recalculateIndex();
|
|
});
|
|
|
|
self._recalculateIndex();
|
|
}
|
|
|
|
ScrollableBox.prototype.__proto__ = Box.prototype;
|
|
|
|
ScrollableBox.prototype.type = 'scrollable-box';
|
|
|
|
// XXX Potentially use this in place of scrollable checks elsewhere.
|
|
ScrollableBox.prototype.__defineGetter__('reallyScrollable', function() {
|
|
if (this.shrink) return this.scrollable;
|
|
return this.getScrollHeight() > this.height;
|
|
});
|
|
|
|
ScrollableBox.prototype._scrollBottom = function() {
|
|
if (!this.scrollable) return 0;
|
|
|
|
// We could just calculate the children, but we can
|
|
// optimize for lists by just returning the items.length.
|
|
if (this._isList) {
|
|
return this.items ? this.items.length : 0;
|
|
}
|
|
|
|
if (this.lpos && this.lpos._scrollBottom) {
|
|
return this.lpos._scrollBottom;
|
|
}
|
|
|
|
var bottom = this.children.reduce(function(current, el) {
|
|
// el.height alone does not calculate the shrunken height, we need to use
|
|
// getCoords. A shrunken box inside a scrollable element will not grow any
|
|
// larger than the scrollable element's context regardless of how much
|
|
// content is in the shrunken box, unless we do this (call getCoords
|
|
// without the scrollable calculation):
|
|
// See: $ node test/widget-shrink-fail-2.js
|
|
if (!el.detached) {
|
|
var lpos = el._getCoords(false, true);
|
|
if (lpos) {
|
|
return Math.max(current, el.rtop + (lpos.yl - lpos.yi));
|
|
}
|
|
}
|
|
return Math.max(current, el.rtop + el.height);
|
|
}, 0);
|
|
|
|
// XXX Use this? Makes .getScrollHeight() useless!
|
|
// if (bottom < this._clines.length) bottom = this._clines.length;
|
|
|
|
if (this.lpos) this.lpos._scrollBottom = bottom;
|
|
|
|
return bottom;
|
|
};
|
|
|
|
ScrollableBox.prototype.setScroll =
|
|
ScrollableBox.prototype.scrollTo = function(offset, always) {
|
|
// XXX
|
|
// At first, this appeared to account for the first new calculation of childBase:
|
|
this.scroll(0);
|
|
return this.scroll(offset - (this.childBase + this.childOffset), always);
|
|
};
|
|
|
|
ScrollableBox.prototype.getScroll = function() {
|
|
return this.childBase + this.childOffset;
|
|
};
|
|
|
|
ScrollableBox.prototype.scroll = function(offset, always) {
|
|
if (!this.scrollable) return;
|
|
|
|
if (this.detached) return;
|
|
|
|
// Handle scrolling.
|
|
var visible = this.height - this.iheight
|
|
, base = this.childBase
|
|
, d
|
|
, p
|
|
, t
|
|
, b
|
|
, max
|
|
, emax;
|
|
|
|
if (this.alwaysScroll || always) {
|
|
// Semi-workaround
|
|
this.childOffset = offset > 0
|
|
? visible - 1 + offset
|
|
: offset;
|
|
} else {
|
|
this.childOffset += offset;
|
|
}
|
|
|
|
if (this.childOffset > visible - 1) {
|
|
d = this.childOffset - (visible - 1);
|
|
this.childOffset -= d;
|
|
this.childBase += d;
|
|
} else if (this.childOffset < 0) {
|
|
d = this.childOffset;
|
|
this.childOffset += -d;
|
|
this.childBase += d;
|
|
}
|
|
|
|
if (this.childBase < 0) {
|
|
this.childBase = 0;
|
|
} else if (this.childBase > this.baseLimit) {
|
|
this.childBase = this.baseLimit;
|
|
}
|
|
|
|
// Find max "bottom" value for
|
|
// content and descendant elements.
|
|
// Scroll the content if necessary.
|
|
if (this.childBase === base) {
|
|
return this.emit('scroll');
|
|
}
|
|
|
|
// When scrolling text, we want to be able to handle SGR codes as well as line
|
|
// feeds. This allows us to take preformatted text output from other programs
|
|
// and put it in a scrollable text box.
|
|
this.parseContent();
|
|
|
|
// XXX
|
|
// max = this.getScrollHeight() - (this.height - this.iheight);
|
|
|
|
max = this._clines.length - (this.height - this.iheight);
|
|
if (max < 0) max = 0;
|
|
emax = this._scrollBottom() - (this.height - this.iheight);
|
|
if (emax < 0) emax = 0;
|
|
|
|
this.childBase = Math.min(this.childBase, Math.max(emax, max));
|
|
|
|
if (this.childBase < 0) {
|
|
this.childBase = 0;
|
|
} else if (this.childBase > this.baseLimit) {
|
|
this.childBase = this.baseLimit;
|
|
}
|
|
|
|
// Optimize scrolling with CSR + IL/DL.
|
|
p = this.lpos;
|
|
// Only really need _getCoords() if we want
|
|
// to allow nestable scrolling elements...
|
|
// or if we **really** want shrinkable
|
|
// scrolling elements.
|
|
// p = this._getCoords();
|
|
if (p && this.childBase !== base && this.screen.cleanSides(this)) {
|
|
t = p.yi + this.itop;
|
|
b = p.yl - this.ibottom - 1;
|
|
d = this.childBase - base;
|
|
|
|
if (d > 0 && d < visible) {
|
|
// scrolled down
|
|
this.screen.deleteLine(d, t, t, b);
|
|
} else if (d < 0 && -d < visible) {
|
|
// scrolled up
|
|
d = -d;
|
|
this.screen.insertLine(d, t, t, b);
|
|
}
|
|
}
|
|
|
|
return this.emit('scroll');
|
|
};
|
|
|
|
ScrollableBox.prototype._recalculateIndex = function() {
|
|
var max, emax;
|
|
|
|
if (this.detached || !this.scrollable) {
|
|
return 0;
|
|
}
|
|
|
|
// XXX
|
|
// max = this.getScrollHeight() - (this.height - this.iheight);
|
|
|
|
max = this._clines.length - (this.height - this.iheight);
|
|
if (max < 0) max = 0;
|
|
emax = this._scrollBottom() - (this.height - this.iheight);
|
|
if (emax < 0) emax = 0;
|
|
|
|
this.childBase = Math.min(this.childBase, Math.max(emax, max));
|
|
|
|
if (this.childBase < 0) {
|
|
this.childBase = 0;
|
|
} else if (this.childBase > this.baseLimit) {
|
|
this.childBase = this.baseLimit;
|
|
}
|
|
};
|
|
|
|
ScrollableBox.prototype.resetScroll = function() {
|
|
if (!this.scrollable) return;
|
|
this.childOffset = 0;
|
|
this.childBase = 0;
|
|
return this.emit('scroll');
|
|
};
|
|
|
|
ScrollableBox.prototype.getScrollHeight = function() {
|
|
return Math.max(this._clines.length, this._scrollBottom());
|
|
};
|
|
|
|
ScrollableBox.prototype.getScrollPerc = function(s) {
|
|
var pos = this.lpos || this._getCoords();
|
|
if (!pos) return s ? -1 : 0;
|
|
|
|
var height = (pos.yl - pos.yi) - this.iheight
|
|
, i = this.getScrollHeight()
|
|
, p;
|
|
|
|
if (height < i) {
|
|
if (this.alwaysScroll) {
|
|
p = this.childBase / (i - height);
|
|
} else {
|
|
p = (this.childBase + this.childOffset) / (i - 1);
|
|
}
|
|
return p * 100;
|
|
}
|
|
|
|
return s ? -1 : 0;
|
|
};
|
|
|
|
ScrollableBox.prototype.setScrollPerc = function(i) {
|
|
// XXX
|
|
// var m = this.getScrollHeight();
|
|
var m = Math.max(this._clines.length, this._scrollBottom());
|
|
return this.scrollTo((i / 100) * m | 0);
|
|
};
|
|
|
|
/**
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = ScrollableBox;
|