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.
555 lines
18 KiB
Plaintext
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 |
|
}
|
|
|
|
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( |
|
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( |
|
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;
|
|
}
|
|
} |