mz700-js
Version:
MZ-700 Full JavaScript Emulator
429 lines (391 loc) • 11.4 kB
JavaScript
"use strict";
const doLater = require("../do-later.js");
const jquery_plugin_class = require("./jquery_plugin_class");
jquery_plugin_class("vscrlist");
/**
* jquery plug-in.
* Row oriented list view widget.
*
* @constructor
* @param {Element} element The DOM element
*/
function vscrlist(element) {
this._opt = {
rowHeight: 13,
rowCount: 20,
headerRow: null,
createRow: index => {
return $("<div/>").html(index).get(0);
},
scrollRows: 1, // lines scrolled by arrowUp/Dn
scrollPageRows: null, // lines scrolled by pgUp/pgDn
keyRepeatBufferCount: 1, // Key-repeat buffering count
};
this._element = element;
this._items = [];
this._visible_row_count = null;
this._top_row_index = null;
this._header_container = null;
this._container = null;
this._keyRepeatCount = 0;
}
window.vscrlist = vscrlist;
module.exports = vscrlist;
/**
* Create vscrlist.
* @param {object} opt
* The parameters
* @returns {undefined}
*/
vscrlist.prototype.create = function(opt) {
opt = opt || {};
Object.keys(this._opt).forEach( key => {
if(key in opt) {
this._opt[key] = opt[key];
}
});
// The scroll paging size default is same as the row count.
if(this._opt.scrollPageRows == null) {
this._opt.scrollPageRows = this._opt.rowCount;
}
this._visible_row_count = this._opt.rowCount;
// Data row's Container
this._row_container = $("<div/>")
.addClass("scrollee")
.css("position", "relative");
// V-Scrollee
this._scroll = $("<div/>")
.addClass("scroll")
.append(this._row_container);
// Header row
if(!this._opt.headerRow) {
this._header_container = null;
this._scroller = $(this._element);
} else {
this._header_container = $("<div/>")
.css("width", "100%").css("overflow", "hidden")
.append(this._opt.headerRow.css("position", "relative"));
this._scroller = $("<div/>");
this._container = $(this._element)
.append(this._header_container)
.append(this._scroller);
}
this._scroller.addClass("vscrlist")
.addClass("scroller")
.css("height", "calc(100% - 22px)")
.css("overflow", "auto")
.append(this._scroll);
// Handle scroll event, and redraw assemble list.
let scrollee = this._row_container.get(0);
let h_scrollee = null;
if(this._opt.headerRow) {
h_scrollee = this._opt.headerRow.get(0);
}
let scroller = this._scroller.get(0);
scroller.addEventListener("scroll", event => {
event.stopPropagation();
scrollee.style.top = scroller.scrollTop + "px";
if(h_scrollee) {
h_scrollee.style.left = (-scroller.scrollLeft) + "px";
}
doLater(() => {
let index = Math.round(scroller.scrollTop / this._opt.rowHeight);
if(this._listTopIndex(index)) {
$(this._element).trigger("scroll");
this.createList();
}
}, 30);
}, false);
this.setupKeyEventListener();
};
// The key code converted when the control key pressed.
const eventCodePromotedByCtrlKey = {
/* [Ctrl] + */ "ArrowUp": /* is to */ "Home",
/* [Ctrl] + */ "ArrowDown": /* is to */ "End",
};
// The key code converted when the shift key pressed.
const eventCodePromotedByShiftKey = {
/* [Shift] + */ "ArrowUp": /* is to */ "PageUp",
/* [Shift] + */ "ArrowDown": /* is to */ "PageDown",
/* [Shift] + */ "PageUp": /* is to */ "Home",
/* [Shift] + */ "PageDown": /* is to */ "End",
};
/**
* Retrieve key code from the key event.
*
* @param {KeyboardEvent} event
* The key event.
*
* @returns {string} key code that might be promoted by the event's state.
*/
function getEventCode() {
let code = event.code;
if(event.ctrlKey && code in eventCodePromotedByCtrlKey) {
code = eventCodePromotedByCtrlKey[code];
} else if(event.shiftKey && code in eventCodePromotedByShiftKey) {
code = eventCodePromotedByShiftKey[code];
}
return code;
}
/**
* Invoke the scroll handler specified by the event and may close the
* key-repeat.
*
* @param {KeyboardEvent} event
* The key event.
*
* @returns {undefined}
*/
vscrlist.prototype.flashBufferedKeys = function() {
this.scrollByKeyCode(getEventCode(event), this._keyRepeatCount);
this._keyRepeatCount = 0;
};
/**
* Count up the event until it will over the threshold,
* and then, scroll the list and clear the counter.
*
* @param {KeyboardEvent} event
* The key event.
*
* @returns {undefined}
*/
vscrlist.prototype.countKeyRepeat = function() {
this._keyRepeatCount++;
if(this._keyRepeatCount >= this._opt.keyRepeatBufferCount) {
this.flashBufferedKeys(event);
}
};
/**
* Setup key event listener to scroll by the keys of
* arrow, page up, page down, home and end.
*
* @returns {undefined}
*/
vscrlist.prototype.setupKeyEventListener = function() {
// Setup to accept keys only the mouse is hovering this element.
let scroll = this._scroll.get(0);
scroll.addEventListener("mouseenter", event => {
event.stopPropagation();
if(!this._element.classList.contains("listen-key")) {
document.activeElement.blur();
this._element.classList.add("listen-key");
}
});
scroll.addEventListener("mouseleave", event => {
event.stopPropagation();
if(this._element.classList.contains("listen-key")) {
if(this._keyRepeatCount > 0) {
this.flashBufferedKeys(event);
}
}
this._element.classList.remove("listen-key");
});
// Handle key down event.
document.addEventListener("keydown", event => {
if(this._element.classList.contains("listen-key")) {
event.stopPropagation();
if(event.repeat) {
// The event may buffered until the UI updated.
this.countKeyRepeat(event);
} else {
// Update actually.
this.scrollByKeyCode(getEventCode(event), 1);
this._keyRepeatCount = 0;
}
}
});
// Handle key up event to clear the key-repeat.
document.addEventListener("keyup", event => {
if(this._element.classList.contains("listen-key")) {
event.stopPropagation();
if(this._keyRepeatCount > 0) {
this.flashBufferedKeys(event);
}
}
});
};
/**
* Scroll by key code.
*
* @param {string} keycode
* The key code.
*
* @param {number} n
* relative scroll rows
*
* @returns {undefined}
*/
vscrlist.prototype.scrollByKeyCode = function(keycode, n) {
switch(keycode) {
case "ArrowUp":
this.scroll(n * -this._opt.scrollRows);
break;
case "ArrowDown":
this.scroll(n * this._opt.scrollRows);
break;
case "PageUp":
this.scroll(n * -this._opt.scrollPageRows);
break;
case "PageDown":
this.scroll(n * this._opt.scrollPageRows);
break;
case "Home":
this.listTopIndex(0);
break;
case "End":
this.listTopIndex(this._items.length);
break;
default:
break;
}
};
/**
* Scroll relative.
*
* @param {number} relativeRows
* The relative rows to scroll.
*
* @returns {undefined}
*/
vscrlist.prototype.scroll = function(relativeRows) {
this.listTopIndex(this._top_row_index + relativeRows);
};
/**
* Log the key event.
*
* @param {KeyboardEvent} event
* The event to be outputted to the console.
*
* @returns {undefined}
*/
vscrlist.prototype.reportKbEvent = function(event) {
let a = [event.type];
if(event.ctrlKey) { a.push("[CTRL]+"); }
if(event.shiftKey) { a.push("[SHIFT]+"); }
if(event.altKey) { a.push("[ALT]+"); }
if(event.metaKey) { a.push("[META]+"); }
if(event.key !== "Unidentified") {
a.push("[", event.key, "]");
a.push("(", event.code, ")");
} else {
a.push("(Unidentified)");
}
if(event.repeat) {
a.push("+");
}
console.log(a.join(""));
};
/**
* Set or get the list items.
* @param {Array|undefined} items
* An array of list items to set.
* @returns {undefined|Array}
* Returns list items if the parameter item is null.
*/
vscrlist.prototype.items = function(items) {
if(items == null) {
return this._items;
}
items = items || [];
this._items = items;
this._scroll.css("height", (items.length * this._opt.rowHeight) + "px");
this.createList();
this.syncScrollTop();
};
/**
* Check the rows are not rendered or else.
* @returns {boolean} true if rows are not rendered, otherwise false.
*/
vscrlist.prototype.isEmpty = function() {
return this._row_container.children().length == 0;
};
/**
* Remove all visible rows.
* @returns {undefined}
*/
vscrlist.prototype.empty = function() {
return this._row_container.empty();
};
/**
* Select visible rows.
* @param {string|undefined} selector
* jQuery selector. To get all rows, do not specify.
* @returns {jQueryObject} selected rows.
*/
vscrlist.prototype.visibleRows = function(selector) {
return this._row_container.children(selector);
};
/**
* Set or get list top index.
* @param {number|undefined} index
* Top row index.
* @returns {undefined|number}
* returns top row index, if the parameter is not provided.
*/
vscrlist.prototype.listTopIndex = function(index) {
if(index == null) {
return this._top_row_index;
}
if(this._listTopIndex(index)) {
this.createList();
this.syncScrollTop();
}
};
/**
* Set the top row index.
*
* @param {number} index
* The new index.
*
* @returns {boolean} status whether the index was updated.
*/
vscrlist.prototype._listTopIndex = function(index) {
const max_top_row_index = this._maxTopRowIndex();
if(index <= 0) {
index = 0;
} else if(index >= max_top_row_index) {
index = max_top_row_index;
}
if(this._top_row_index == index) {
return false;
}
this._top_row_index = index;
return true;
};
/**
* @returns {number}
* A max index of items to display at the top row.
*/
vscrlist.prototype._maxTopRowIndex = function() {
const max_top_row_index = this._items.length - this._visible_row_count;
if(max_top_row_index < 0) {
return 0;
}
return max_top_row_index;
};
/**
* Create assemble list.
* @returns {undefined}
*/
vscrlist.prototype.createList = function() {
this._row_container.empty();
let topIndex = this._top_row_index || 0;
for(let i = 0; i < this._visible_row_count; i++) {
let index = topIndex + i;
let row = this._opt.createRow(index);
if(row) {
this._row_container.append($(row));
}
}
};
/**
* Synchronize scroll top of the list.
* @returns {undefined}
*/
vscrlist.prototype.syncScrollTop = function() {
let scrollee = this._row_container.get(0);
let scroller = this._scroller.get(0);
let scrollTop = this._top_row_index * this._opt.rowHeight;
scroller.scrollTop = scrollTop;
scrollee.style.top = scrollTop + "px";
};