Mon 21 Jul 22:43:21 CEST 2025
This commit is contained in:
parent
41a6dec074
commit
c37d7f01f9
446
js/term/widgets/scrollablebox.js
Normal file
446
js/term/widgets/scrollablebox.js
Normal file
|
@ -0,0 +1,446 @@
|
|||
/**
|
||||
** ==============================
|
||||
** O O O OOOO
|
||||
** O O O O O O
|
||||
** O O O O O O
|
||||
** OOOO OOOO O OOO OOOO
|
||||
** O O O O O O O
|
||||
** O O O O O O O
|
||||
** OOOO OOOO O O OOOO
|
||||
** ==============================
|
||||
** Dr. Stefan Bosse http://www.bsslab.de
|
||||
**
|
||||
** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED
|
||||
** BY THE AUTHOR(S).
|
||||
** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED,
|
||||
** MODIFIED, OR OTHERWISE USED IN A CONTEXT
|
||||
** OUTSIDE OF THE SOFTWARE SYSTEM.
|
||||
**
|
||||
** $AUTHORS: Christopher Jeffrey and contributors, Stefan Bosse
|
||||
** $INITIAL: (C) 2013-2016, Christopher Jeffrey and contributors
|
||||
** $MODIFIED: sbosse (2017-2021).
|
||||
** $VERSION: 1.2.3
|
||||
**
|
||||
** $INFO:
|
||||
*
|
||||
* scrollablebox.js - scrollable box element for blessed
|
||||
*
|
||||
* events: 'mouse', 'click' (low level), 'clicked' (high level)
|
||||
*
|
||||
** $ENDOFINFO
|
||||
*/
|
||||
|
||||
/**
|
||||
* Modules
|
||||
*/
|
||||
var Comp = Require('com/compat');
|
||||
|
||||
var Node = Require('term/widgets/node');
|
||||
var Box = Require('term/widgets/box');
|
||||
|
||||
/**
|
||||
* ScrollableBox
|
||||
*/
|
||||
|
||||
function ScrollableBox(options) {
|
||||
var self = this;
|
||||
|
||||
if (!instanceOf(this,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;
|
||||
self.emit('clicked',{x:x-1,y:y-1,ev:'click'});
|
||||
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() {
|
||||
self._scrollingBar = false;
|
||||
self.removeScreenEvent('mousedown', smd);
|
||||
self.removeScreenEvent('mouseup', smu);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.mouse) {
|
||||
this.on('wheeldown', function() {
|
||||
self.scroll(self.height / 2 | 0 || 1);
|
||||
self.screen.render();
|
||||
});
|
||||
this.on('wheelup', function() {
|
||||
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 (key.name === 'pageup') {
|
||||
self.scroll(-(self.height / 2 | 0) || -1);
|
||||
self.screen.render();
|
||||
return;
|
||||
}
|
||||
if (key.name === 'pagedown') {
|
||||
self.scroll(self.height / 2 | 0 || 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;
|
||||
inheritPrototype(ScrollableBox,Box);
|
||||
|
||||
ScrollableBox.prototype.type = 'scrollable-box';
|
||||
|
||||
/* depricated
|
||||
// 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;
|
||||
});
|
||||
*/
|
||||
Object.defineProperty(ScrollableBox.prototype,'reallyScrollable',{
|
||||
get: function () {
|
||||
if (this.shrink) return this.scrollable;
|
||||
return this.getScrollHeight() > this.height;
|
||||
},
|
||||
set: function (val) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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.scrollBottom = function () {
|
||||
// Workaround: inserting lines when scrollbar was manually set
|
||||
// breaks scroll window (can't usee _scrollBottom...)
|
||||
this.scrollTo(10000000);
|
||||
};
|
||||
|
||||
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;
|
Loading…
Reference in New Issue
Block a user