You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
RnQ/Distro/Template/vselect.tis

555 lines
18 KiB
Plaintext

namespace RecordProvider {
var data = [],
keyToIndex = function(key) {
var (index, record) = data.find(:record: record.RowID == key);
return index;
}
function provider(key, length) {
(this function).totalItems = data.length;
(this function).keyToIndex = keyToIndex;
var out = [];
if (data.length == 0 || length == 0) return out;
if (length > 0) {
var idx = typeof key == #integer ? key : (key ? key.toInteger() + 1 : 0);
var end = Integer.min(idx + length, data.length);
for(var n = idx; n < end; ++n)
out.push({
key: n.toString(),
content: data[n % data.length],
index: n
});
} else {
var idx = typeof key == #integer ? key : (key !== null ? (key.toInteger() - 1) : (data.length - 1));
var first = Integer.max(0, idx + length + 1);
for(var n = idx; n >= first; --n)
out.unshift({
key: n.toString(),
content: data[n % data.length],
index: n
});
}
return out;
}
}
namespace NativeRecordProvider {
function keyToIndex(key) {
return CommonNative.KeyToIndex(key);
}
function getDataLength() {
return CommonNative.GetHistoryLength();
}
function getRecord(index) {
return CommonNative.GetHistoryEntry(index);
}
function provider(key, length) {
var out = [];
var dataLength = getDataLength();
(this function).totalItems = dataLength;
(this function).keyToIndex = keyToIndex;
if (dataLength == 0 || length == 0) return out;
var chunk = CommonNative.GetHistoryChunk(key, length);
return chunk ? chunk : [];
}
}
class VSelect: Reactor.Component {
const styleset = [vselect: $url(vselect.css)];
const BUFFER_SIZE = 30; // this is a max number of items in buffer
// this number has to be at least twice bigger than number of visible items.
const BUFFER_CHUNK_SIZE = 15; // this is a number of items that will be fetched from items source on scroll to fill the buffer.
const SCROLL_DELTA = 60; // dip, for mouse wheel, non-kinetic
const SCROLL_KINETIC_DELTA = 20; // dip, for mouse wheel, kinetic
const SCROLL_KINETIC_DECEL = 0.86; // deceleration
const SCROLL_KINETIC_OUT_DECEL = 0.0; // deceleration when head/tail reached, a.k.a overscroll
const SCROLL_KINETIC_STEP_DELAY = View.ANIMATION_TIMER_SPAN; // milliseconds
const SB_THUMB_SIZE = 16dip;
this var
dataSource,
itemView,
isMultiple,
isKinetic,
atEnd,
scrollbar;
function isItemVisible(item, height) {
var (x1, y1, x2, y2) = item.box(#rect, #border, #parent);
if (y2 < 0) return false;
if (y1 > height) return false;
return true;
}
class Arena: Element {
this var
itemsBuffer = [],
currentIndex = 0,
anchorIndex = -1,
checkedIndexes = [],
velocity = 0,
isOut = false,
scrollDown,
scrollUp,
firstVisible,
lastVisible;
function init(isMultiple, isKinetic, atEnd, atKey) {
if (isMultiple) this.@#multiple = "";
if (isKinetic) {
this.scrollDown = this.scrollDownKinetic;
this.scrollUp = this.scrollUpKinetic;
} else {
this.scrollDown = this.scrollDownNormal;
this.scrollUp = this.scrollUpNormal;
}
if (atKey && atKey >= 0) {
this.parent.dataSource(null, 0); // init totalItems
var atIndex = this.parent.dataSource.keyToIndex(atKey);
if (atIndex >= 0) this.navigateTo(atIndex, #middle, true);
} else if (atEnd)
this.scrollToBottom();
else
this.scrollToTop();
}
function render() {
//var key = this.itemsBuffer.last.key;
//var chunk = this.parent.dataSource(key,BUFFER_SIZE);
var content = this.itemsBuffer.map((item) => this.parent.itemView.call(this, item));
return {content};
}
function hasNoScrolling() {
return this.parent.dataSource.totalItems == 0 ||
(this.firstVisible == 0 && this.lastVisible == this.parent.dataSource.totalItems - 1 &&
this.scroll(#top) <= 0 && this.scroll(#bottom) <= 0);
}
function updateNodes(refresh = false) {
var vnodes = this.itemsBuffer.map((item) => this.parent.itemView.call(this, item));
this.merge({vnodes}, #only-children);
if (refresh) this.update();
return vnodes;
}
function updateVisible() {
if (!this.parent.scrollbar) return;
var height = this.box(#height);
var first = null, last = null;
for (var item in this) {
if (!isItemVisible(item, height)) {
if (last !== null) break;
continue;
}
const recordIndex = item.value.toInteger();
if (first === null) first = recordIndex;
last = recordIndex;
}
if (first === null || last === null || this.hasNoScrolling()) {
this.parent.scrollbar.style#display = "none";
} else {
this.firstVisible = first;
this.lastVisible = last;
this.parent.scrollbar.style#display = undefined;
this.parent.scrollbarSetup(height, this.firstVisible, this.lastVisible);
}
}
function scrollTo(x, y, animate) {
super.scrollTo(x, y, false, animate);
this.updateVisible();
}
function scrollStepDown(scrollDelta, noOverflow = false) {
var (scroll_left, scroll_top, scroll_right, scroll_bottom) = this.scroll(#rect);
var content_height = this.box(#height, #content);
function fetchMore(arena) {
var key = arena.itemsBuffer.last.key;
var chunk = arena.parent.dataSource(key, BUFFER_CHUNK_SIZE);
if (chunk.length == 0) return true; // nothing was inserted - no more records. true - to mark end reached
arena.itemsBuffer.push(...chunk);
var vnodes = arena.updateNodes(true);
content_height = arena.box(#height,#content);
// drop first items that exceed BUFFER_SIZE
vnodes.splice(0, arena.itemsBuffer.length - BUFFER_SIZE);
arena.itemsBuffer.splice(0, arena.itemsBuffer.length - BUFFER_SIZE);
arena.merge({vnodes}, #only-children);
arena.update();
var content_height_after = arena.box(#height, #content); // adjust scroll position as
scroll_top += content_height_after - content_height; // we've removed first items
content_height = content_height_after;
arena.postEvent("buffer-change", [arena.itemsBuffer.first.key, arena.itemsBuffer.last.key]);
return false;
}
var scrolly = scrollDelta == #none ? scroll_top : scroll_top + scrollDelta;
if (scrollDelta == #none || (scrollDelta != #none && scroll_bottom < scrollDelta)) {
// we need to pump more items in this virtual list:
this.isOut = fetchMore(this);
if (this.isOut && noOverflow) {
scrolly = content_height - this.scroll(#height);
this.isOut = false;
} else {
scrolly = scroll_top + (scrollDelta == #none ? scroll_bottom : scrollDelta);
}
} else this.isOut = false;
this.scrollTo(scroll_left, scrolly, scrollDelta != #none);
}
function scrollStepUp(scrollDelta, noOverflow = false) {
var (scroll_left, scroll_top, scroll_right, scroll_bottom) = this.scroll(#rect);
function fetchMore(arena) {
var prev_top = arena.first.box(#top, #inner, #parent);
var key = arena.itemsBuffer.first.key;
var chunk = arena.parent.dataSource(key, -BUFFER_CHUNK_SIZE);
if (chunk.length == 0) return true; // nothing was inserted - no more records. true - to mark end reached
arena.itemsBuffer.unshift(...chunk);
if (arena.itemsBuffer.length > BUFFER_SIZE)
arena.itemsBuffer.length = BUFFER_SIZE; // prune the buffer
arena.updateNodes(true);
var new_top = arena[chunk.length].box(#top, #inner, #parent);
scroll_top = new_top - prev_top; // adjust scroll position.
arena.postEvent("buffer-change", [arena.itemsBuffer.first.key, arena.itemsBuffer.last.key]);
return false;
}
var scrolly = scrollDelta == #none ? scroll_top : scroll_top - scrollDelta;
if (scrollDelta == #none || (scrollDelta != #none && scrolly < 0)) {
// we need to pump more items in this virtual list:
this.isOut = fetchMore(this);
if (this.isOut && noOverflow) {
scrolly = 0;
this.isOut = false;
} else {
scrolly = scroll_top - (scrollDelta == #none ? 0 : scrollDelta);
}
} else this.isOut = false;
this.scrollTo(scroll_left, scrolly, scrollDelta != #none);
}
function scrollToTop() {
this.itemsBuffer = this.parent.dataSource(null, BUFFER_SIZE);
this.updateNodes(true);
this.scrollTo(0, 0, false);
if (this.first) {
this.first.execCommand("set-current");
this.currentIndex = this.first.value.toInteger();
this.checkedIndexes = [this.currentIndex];
this.parent.sendEvent("currentchange");
}
}
function scrollToBottom() {
this.itemsBuffer = this.parent.dataSource(null, -BUFFER_SIZE);
this.updateNodes(true);
this.scrollTo(0, 10000, false);
if (this.last) {
this.last.execCommand("set-current");
this.currentIndex = this.last.value.toInteger();
this.checkedIndexes = [this.currentIndex];
this.parent.sendEvent("currentchange");
}
}
function updateBuffer(direction) {
if (direction == #up)
this.scrollStepUp(#none, true);
else if (direction == #down)
this.scrollStepDown(#none, true);
}
function navigateTo(itemNo, align = #top, selectItem = false) {
var (scroll_left, scroll_top, scroll_right, scroll_bottom) = this.scroll(#rect);
var atEnd = false;
if (itemNo > this.parent.dataSource.totalItems - BUFFER_SIZE) {
this.itemsBuffer = this.parent.dataSource(null, -BUFFER_SIZE);
atEnd = true;
} else {
if (align == #middle) {
this.itemsBuffer = this.parent.dataSource(itemNo, -BUFFER_SIZE / 2 + 1);
var chunk = this.parent.dataSource(itemNo.toString(), BUFFER_SIZE - this.itemsBuffer.length);
if (chunk.length > 0) this.itemsBuffer.push(...chunk);
} else if (align == #bottom) {
this.itemsBuffer = this.parent.dataSource(itemNo, -BUFFER_SIZE + 1);
var chunk = this.parent.dataSource(itemNo.toString(), BUFFER_SIZE - this.itemsBuffer.length);
if (chunk.length > 0) this.itemsBuffer.push(...chunk);
} else {
this.itemsBuffer = this.parent.dataSource(itemNo, BUFFER_SIZE - 1);
var chunk = this.parent.dataSource(itemNo.toString(), this.itemsBuffer.length - BUFFER_SIZE);
if (chunk.length > 0) this.itemsBuffer.unshift(...chunk);
}
}
this.updateNodes(true);
if (selectItem) {
var item = this.$([value="{itemNo}"]);
if (item) this.post(:{
item.execCommand("set-current");
item.scrollToView(align != #bottom, false);
var sheight = this.scroll(#height);
var iheight = item.box(#height);
if (align == #middle && iheight < sheight)
this.scrollTo(scroll_left, this.scroll(#top) - sheight / 2 + iheight /2 ,false);
else
this.updateVisible();
});
this.currentIndex = itemNo;
this.checkedIndexes = [itemNo];
this.parent.sendEvent("currentchange");
} else {
if (atEnd) {
var item = this.$([value="{itemNo}"]);
if (item) item.post(:: item.scrollToView(true, false));
} else {
scroll_top = align == #bottom ? this.scroll(#height) : 0;
}
this.scrollTo(scroll_left, scroll_top, false);
}
this.postEvent("buffer-change", [this.itemsBuffer.first.key, this.itemsBuffer.last.key]);
}
function doScrollAnimation() {
if (this.state.animating) return;
function animationStep() {
this.velocity *= this.isOut ? SCROLL_KINETIC_OUT_DECEL : SCROLL_KINETIC_DECEL;
if (Math.abs(this.velocity) < 0.5) return 0;
var v = this.velocity.toInteger();
if (v < 0)
this.scrollStepUp(-v, true);
else
this.scrollStepDown(v, true);
return SCROLL_KINETIC_STEP_DELAY;
}
this.animate(animationStep);
}
function scrollDownNormal(scrollDelta = SCROLL_DELTA) {
return this.scrollStepDown(scrollDelta * this.toPixels(1dip, #height), true);
}
function scrollUpNormal(scrollDelta = SCROLL_DELTA) {
return this.scrollStepUp(scrollDelta * this.toPixels(1dip, #height), true);
}
function scrollDownKinetic(scrollDelta = SCROLL_KINETIC_DELTA) {
var v = (this.velocity || 0.0);
this.velocity = v + scrollDelta * this.toPixels(1dip, #height);
this.doScrollAnimation();
}
function scrollUpKinetic(scrollDelta = SCROLL_KINETIC_DELTA) {
var v = (this.velocity || 0.0);
this.velocity = v - scrollDelta * this.toPixels(1dip, #height);
this.doScrollAnimation();
}
function attached() {
this.subscribe(:e {
var isMultiple = this.@.exists("multiple");
this.currentIndex = e.source.value.toInteger();
//if (this.state.animating) e.source.scrollToView(false, false);
if (isMultiple) {
if ((!e.ctrlKey || e.reason == 1) && !e.shiftKey) {
var anchor = this.$(> :anchor);
this.anchorIndex = anchor ? anchor.value.toInteger() : -1;
this.checkedIndexes = [];
}
if (e.shiftKey) {
this.checkedIndexes = [];
if (this.anchorIndex >= 0) {
var start = this.currentIndex > this.anchorIndex ? this.anchorIndex : this.currentIndex;
var end = this.currentIndex > this.anchorIndex ? this.currentIndex : this.anchorIndex;
for (var index = start; index <= end; index++)
if (this.checkedIndexes.indexOf(index) < 0)
this.checkedIndexes.push(index);
}
for (var item in this)
if (this.checkedIndexes.indexOf(item.value.toInteger()) >= 0)
item.state.checked = true;
} else if (e.source.state.checked) {
if (this.checkedIndexes.indexOf(this.currentIndex) < 0)
this.checkedIndexes.push(this.currentIndex);
} else {
this.checkedIndexes.removeByValue(this.currentIndex);
}
}
const direction = e.source.index <= 0 ? #up : (e.source.index >= this.length - 1 ? #down : #none);
if (direction != #none)
this.updateBuffer(direction);
else
this.updateVisible();
this.parent.sendEvent("currentchange");
}, Event.BEHAVIOR_EVENT, Event.SELECT_SELECTION_CHANGED);
}
function detached() {
this.unsubscribe(Event.BEHAVIOR_EVENT, Event.SELECT_SELECTION_CHANGED);
}
function onSize() {
this.updateVisible();
}
event mousewheel (evt) {
if (this.hasNoScrolling()) return;
if (!this.isOut)
if (evt.wheelDelta < 0)
this.scrollDown();
else
this.scrollUp();
return true;
}
event keydown (evt) {
if (this.parent.dataSource.totalItems > 0)
switch (evt.keyCode) {
case Event.VK_HOME:
this.scrollToTop();
return true;
case Event.VK_END:
this.scrollToBottom();
return true;
case Event.VK_PRIOR: {
var indexTo = this.currentIndex - Integer.max(1, this.lastVisible - this.firstVisible);
if (indexTo < 0) indexTo = 0;
this.navigateTo(indexTo, #top, true);
return true;
}
case Event.VK_NEXT: {
var indexTo = this.currentIndex + Integer.max(1, this.lastVisible - this.firstVisible);
if (indexTo > this.parent.dataSource.totalItems - 1) indexTo = this.parent.dataSource.totalItems - 1;
this.navigateTo(indexTo, #bottom, true);
return true;
}
// case Event.VK_RETURN:
// if (var opt = this.$(> :current)) {
// opt.postEvent("item-click", evt.keyCode);
// return true;
// }
// break;
}
}
}
function this(props) {
//set these:
// vselect.dataSource;
// vselect.itemView;
// vselect.isMultiple;
// vselect.isKinetic;
// vselect.atEnd;
// vselect.atKey;
this.extend(props);
}
function render() {
return ;
}
function update(props = null) {
if (props) {
this.extend(props);
this.initList();
} else super.update();
}
function scrollbarSetup(height, fVis, lVis) {
const nTotal = this.dataSource.totalItems.toFloat();
const sbm = this.scrollbar.box(#top, #margin, #border) + this.scrollbar.box(#bottom, #margin, #border);
const thumb = Float.min(height / 3.0, Float.max(this.toPixels(SB_THUMB_SIZE), height / nTotal * 10.0));
var thumbTopPos = (height - sbm - thumb) * fVis / nTotal;
var thumbBottomPos = (height - sbm) * (lVis + 1) / nTotal;
if (fVis + (lVis - fVis) / 2.0 < nTotal / 2.0)
thumbBottomPos = Float.min(height - sbm, thumbTopPos + thumb);
else
thumbTopPos = Float.max(0, thumbBottomPos - thumb);
this.scrollbar.style.set {
padding-top: px(thumbTopPos),
height: px(thumbBottomPos - thumbTopPos)
};
}
function attached() {
// pre render it
this.merge(this.render(), #only-children);
this.scrollbar << event mousedown(evt) {
var (x1, y1, x2, y2) = this.box(#rect);
if (evt.y > y1 && evt.y < y2) { // click on scrollbar thumb
var off = evt.y - y1;
var height = this.box(#height, #padding);
var height2 = this.box(#height);
var prevptop = 0;
const handler = event mousemove(evt) {
var ptop = this.box(#top, #padding, #inner);
ptop += evt.y - off;
if (prevptop == ptop) return;
prevptop = ptop;
const itemNo = Integer.min(this super super.dataSource.totalItems - 1, Integer.max(0, this super super.dataSource.totalItems * ptop / (height - height2)));
this super super.arena.navigateTo(itemNo);
view.update(true);
};
this << handler;
this.capture(#strict);
view.doEvent(#untilMouseUp);
this.capture(false);
this >> handler;
}
}
this.arena.updateNodes();
this.initList();
}
function initList() {
this.arena.init(this.isMultiple, this.isKinetic, this.atEnd, this.atKey);
}
function focusList() {
if (this.arena) this.arena.post(:: this.state.focus = true);
}
property current(v) {
get return this.arena.currentIndex;
set this.arena.navigateTo(v, #middle, true);
}
property checked(v) {
get return this.arena.checkedIndexes;
set this.arena.checkedIndexes = v;
}
}