Web: Improve touch support and layout.

This commit is contained in:
verylowfreq 2022-08-08 21:27:36 +09:00 committed by ruevs
parent b5cde57bb6
commit 4981570844
4 changed files with 366 additions and 72 deletions

View File

@ -375,8 +375,8 @@ public:
double x = 0; double x = 0;
double y = 0; double y = 0;
for (int i = 0; i < emEvent.numTouches; i++) { for (int i = 0; i < emEvent.numTouches; i++) {
x += emEvent.touches[i].clientX; x += emEvent.touches[i].targetX;
y += emEvent.touches[i].clientY; y += emEvent.touches[i].targetY;
} }
dst_x = x / emEvent.numTouches; dst_x = x / emEvent.numTouches;
dst_y = y / emEvent.numTouches; dst_y = y / emEvent.numTouches;
@ -386,10 +386,10 @@ public:
if (emEvent.numTouches < 2) { if (emEvent.numTouches < 2) {
return; return;
} }
double x1 = emEvent.touches[0].clientX; double x1 = emEvent.touches[0].targetX;
double y1 = emEvent.touches[0].clientY; double y1 = emEvent.touches[0].targetY;
double x2 = emEvent.touches[1].clientX; double x2 = emEvent.touches[1].targetX;
double y2 = emEvent.touches[1].clientY; double y2 = emEvent.touches[1].targetY;
dst_distance = std::sqrt(std::pow(x1 - x2, 2) + std::pow(y1 - y2, 2)); dst_distance = std::sqrt(std::pow(x1 - x2, 2) + std::pow(y1 - y2, 2));
} }
@ -574,10 +574,14 @@ public:
val htmlContainer; val htmlContainer;
val htmlEditor; val htmlEditor;
val scrollbarHelper;
std::function<void()> editingDoneFunc; std::function<void()> editingDoneFunc;
std::shared_ptr<MenuBarImplHtml> menuBar; std::shared_ptr<MenuBarImplHtml> menuBar;
bool useWorkaround_devicePixelRatio = false;
WindowImplHtml(val htmlContainer, std::string emCanvasSel) : WindowImplHtml(val htmlContainer, std::string emCanvasSel) :
emCanvasSel(emCanvasSel), emCanvasSel(emCanvasSel),
htmlContainer(htmlContainer), htmlContainer(htmlContainer),
@ -591,7 +595,34 @@ public:
} }
}; };
htmlEditor.call<void>("addEventListener", val("trigger"), Wrap(&editingDoneFunc)); htmlEditor.call<void>("addEventListener", val("trigger"), Wrap(&editingDoneFunc));
htmlContainer.call<void>("appendChild", htmlEditor); htmlContainer["parentElement"].call<void>("appendChild", htmlEditor);
std::string scrollbarElementQuery = emCanvasSel + "scrollbar";
dbp("scrollbar element query: \"%s\"", scrollbarElementQuery.c_str());
val scrollbarElement = val::global("document").call<val>("querySelector", val(scrollbarElementQuery));
if (scrollbarElement == val::null()) {
// dbp("scrollbar element is null.");
this->scrollbarHelper = val::null();
} else {
dbp("scrollbar element OK.");
this->scrollbarHelper = val::global("window")["ScrollbarHelper"].new_(val(scrollbarElementQuery));
static std::function<void()> onScrollCallback = [this] {
// dbp("onScrollCallback std::function this=%p", (void*)this);
if (this->onScrollbarAdjusted) {
double newpos = this->scrollbarHelper.call<double>("getScrollbarPosition");
// dbp(" call onScrollbarAdjusted(%f)", newpos);
this->onScrollbarAdjusted(newpos);
}
this->Invalidate();
};
this->scrollbarHelper.set("onScrollCallback", Wrap(&onScrollCallback));
}
//FIXME(emscripten): In Chrome for Android on tablet device, devicePixelRatio should not be multiplied.
std::string userAgent = val::global("navigator")["userAgent"].as<std::string>();
bool is_smartphone = userAgent.find("Mobile") != std::string::npos;
bool is_android_device = userAgent.find("Android") != std::string::npos;
this->useWorkaround_devicePixelRatio = is_android_device && !is_smartphone;
sscheck(emscripten_set_resize_callback( sscheck(emscripten_set_resize_callback(
EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false, EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false,
@ -618,21 +649,18 @@ public:
emCanvasSel.c_str(), this, /*useCapture=*/false, emCanvasSel.c_str(), this, /*useCapture=*/false,
WindowImplHtml::MouseCallback)); WindowImplHtml::MouseCallback));
{
std::string altCanvasSelector = "#canvas0";
sscheck(emscripten_set_touchstart_callback( sscheck(emscripten_set_touchstart_callback(
altCanvasSelector.c_str(), this, /*useCapture=*/false, emCanvasSel.c_str(), this, /*useCapture=*/false,
WindowImplHtml::TouchCallback)); WindowImplHtml::TouchCallback));
sscheck(emscripten_set_touchmove_callback( sscheck(emscripten_set_touchmove_callback(
altCanvasSelector.c_str(), this, /*useCapture=*/false, emCanvasSel.c_str(), this, /*useCapture=*/false,
WindowImplHtml::TouchCallback)); WindowImplHtml::TouchCallback));
sscheck(emscripten_set_touchend_callback( sscheck(emscripten_set_touchend_callback(
altCanvasSelector.c_str(), this, /*useCapture=*/false, emCanvasSel.c_str(), this, /*useCapture=*/false,
WindowImplHtml::TouchCallback)); WindowImplHtml::TouchCallback));
sscheck(emscripten_set_touchcancel_callback( sscheck(emscripten_set_touchcancel_callback(
altCanvasSelector.c_str(), this, /*useCapture=*/false, emCanvasSel.c_str(), this, /*useCapture=*/false,
WindowImplHtml::TouchCallback)); WindowImplHtml::TouchCallback));
}
sscheck(emscripten_set_wheel_callback( sscheck(emscripten_set_wheel_callback(
emCanvasSel.c_str(), this, /*useCapture=*/false, emCanvasSel.c_str(), this, /*useCapture=*/false,
@ -903,14 +931,24 @@ public:
double width, height; double width, height;
std::string htmlContainerSel = "#" + htmlContainer["id"].as<std::string>(); std::string htmlContainerSel = "#" + htmlContainer["id"].as<std::string>();
sscheck(emscripten_get_element_css_size(htmlContainerSel.c_str(), &width, &height)); sscheck(emscripten_get_element_css_size(htmlContainerSel.c_str(), &width, &height));
width *= emscripten_get_device_pixel_ratio(); // sscheck(emscripten_get_element_css_size(emCanvasSel.c_str(), &width, &height));
height *= emscripten_get_device_pixel_ratio();
int curWidth, curHeight; if (this->useWorkaround_devicePixelRatio) {
sscheck(emscripten_get_canvas_element_size(emCanvasSel.c_str(), &curWidth, &curHeight)); // Workaround is to skip applying devicePixelRatio.
if(curWidth != (int)width || curHeight != (int)curHeight) { // So NOP here.
dbp("Canvas %s: resizing to (%g,%g)", emCanvasSel.c_str(), width, height); } else {
sscheck(emscripten_set_canvas_element_size( double devicePixelRatio = emscripten_get_device_pixel_ratio();
emCanvasSel.c_str(), (int)width, (int)height)); width *= devicePixelRatio;
height *= devicePixelRatio;
}
int currentWidth = 0, currentHeight = 0;
sscheck(emscripten_get_canvas_element_size(emCanvasSel.c_str(), &currentWidth, &currentHeight));
if ((int)width != currentWidth || (int)height != currentHeight) {
// dbp("Canvas %s container current size: (%d, %d)", emCanvasSel.c_str(), (int)currentWidth, (int)currentHeight);
// dbp("Canvas %s: resizing to (%d, %d)", emCanvasSel.c_str(), (int)width, (int)height);
sscheck(emscripten_set_canvas_element_size(emCanvasSel.c_str(), (int)width, (int)height));
} }
} }
@ -977,13 +1015,13 @@ public:
std::static_pointer_cast<MenuBarImplHtml>(menuBar); std::static_pointer_cast<MenuBarImplHtml>(menuBar);
this->menuBar = menuBarImpl; this->menuBar = menuBarImpl;
val htmlBody = val::global("document")["body"]; val htmlMain = val::global("document").call<val>("querySelector", val("main"));
val htmlCurrentMenuBar = htmlBody.call<val>("querySelector", val(".menubar")); val htmlCurrentMenuBar = htmlMain.call<val>("querySelector", val(".menubar"));
if(htmlCurrentMenuBar.as<bool>()) { if(htmlCurrentMenuBar.as<bool>()) {
htmlCurrentMenuBar.call<void>("remove"); htmlCurrentMenuBar.call<void>("remove");
} }
htmlBody.call<void>("insertBefore", menuBarImpl->htmlMenuBar, htmlMain.call<void>("insertBefore", menuBarImpl->htmlMenuBar,
htmlBody["firstChild"]); htmlMain["firstChild"]);
ResizeCanvasElement(); ResizeCanvasElement();
} }
@ -1026,8 +1064,11 @@ public:
void ShowEditor(double x, double y, double fontHeight, double minWidth, void ShowEditor(double x, double y, double fontHeight, double minWidth,
bool isMonospace, const std::string &text) override { bool isMonospace, const std::string &text) override {
htmlEditor["style"].set("display", val("")); htmlEditor["style"].set("display", val(""));
htmlEditor["style"].set("left", std::to_string(x - 4) + "px"); val canvasClientRect = val::global("document").call<val>("querySelector", val(this->emCanvasSel)).call<val>("getBoundingClientRect");
htmlEditor["style"].set("top", std::to_string(y - fontHeight - 2) + "px"); double canvasLeft = canvasClientRect["left"].as<double>();
double canvasTop = canvasClientRect["top"].as<double>();
htmlEditor["style"].set("left", std::to_string(canvasLeft + x - 4) + "px");
htmlEditor["style"].set("top", std::to_string(canvasTop + y - fontHeight - 2) + "px");
htmlEditor["style"].set("fontSize", std::to_string(fontHeight) + "px"); htmlEditor["style"].set("fontSize", std::to_string(fontHeight) + "px");
htmlEditor["style"].set("minWidth", std::to_string(minWidth) + "px"); htmlEditor["style"].set("minWidth", std::to_string(minWidth) + "px");
htmlEditor["style"].set("fontFamily", isMonospace ? "monospace" : "sans"); htmlEditor["style"].set("fontFamily", isMonospace ? "monospace" : "sans");
@ -1040,22 +1081,54 @@ public:
} }
void SetScrollbarVisible(bool visible) override { void SetScrollbarVisible(bool visible) override {
// FIXME(emscripten): implement // dbp("SetScrollbarVisible(): visible=%d", visible ? 1 : 0);
if (this->scrollbarHelper == val::null()) {
// dbp("scrollbarHelper is null.");
return;
}
if (!visible) {
this->scrollbarHelper.call<void>("setScrollbarEnabled", val(false));
}
} }
double scrollbarPos = 0.0; double scrollbarPos = 0.0;
double scrollbarMin = 0.0;
double scrollbarMax = 0.0;
double scrollbarPageSize = 0.0;
void ConfigureScrollbar(double min, double max, double pageSize) override { void ConfigureScrollbar(double min, double max, double pageSize) override {
// dbp("ConfigureScrollbar(): min=%f, max=%f, pageSize=%f", min, max, pageSize);
if (this->scrollbarHelper == val::null()) {
// dbp("scrollbarHelper is null.");
return;
}
// FIXME(emscripten): implement // FIXME(emscripten): implement
this->scrollbarMin = min;
this->scrollbarMax = max;
this->scrollbarPageSize = pageSize;
this->scrollbarHelper.call<void>("setRange", this->scrollbarMin, this->scrollbarMax);
this->scrollbarHelper.call<void>("setPageSize", pageSize);
} }
double GetScrollbarPosition() override { double GetScrollbarPosition() override {
// FIXME(emscripten): implement // dbp("GetScrollbarPosition()");
if (this->scrollbarHelper == val::null()) {
// dbp("scrollbarHelper is null.");
return 0;
}
this->scrollbarPos = this->scrollbarHelper.call<double>("getScrollbarPosition");
// dbp(" GetScrollbarPosition() returns %f", this->scrollbarPos);
return scrollbarPos; return scrollbarPos;
} }
void SetScrollbarPosition(double pos) override { void SetScrollbarPosition(double pos) override {
// FIXME(emscripten): implement // dbp("SetScrollbarPosition(): pos=%f", pos);
if (this->scrollbarHelper == val::null()) {
// dbp("scrollbarHelper is null.");
return;
}
this->scrollbarHelper.call<void>("setScrollbarPosition", pos);
scrollbarPos = pos; scrollbarPos = pos;
} }
@ -1170,15 +1243,15 @@ public:
Response RunModal() { Response RunModal() {
// ssassert(false, "RunModal not supported on Emscripten"); // ssassert(false, "RunModal not supported on Emscripten");
dbp("MessageDialog::RunModal() called."); // dbp("MessageDialog::RunModal() called.");
this->ShowModal(); this->ShowModal();
//FIXME(emscripten): use val::await() with JavaScript's Promise //FIXME(emscripten): use val::await() with JavaScript's Promise
while (true) { while (true) {
if (this->is_shown) { if (this->is_shown) {
dbp("MessageDialog::RunModal(): is_shown == true"); // dbp("MessageDialog::RunModal(): is_shown == true");
emscripten_sleep(2000); emscripten_sleep(2000);
} else { } else {
dbp("MessageDialog::RunModal(): break due to is_shown == false"); // dbp("MessageDialog::RunModal(): break due to is_shown == false");
break; break;
} }
} }
@ -1187,7 +1260,7 @@ public:
return this->latestResponse; return this->latestResponse;
} else { } else {
// FIXME(emscripten): // FIXME(emscripten):
dbp("MessageDialog::RunModal(): Cannot get Response."); // dbp("MessageDialog::RunModal(): Cannot get Response.");
return this->latestResponse; return this->latestResponse;
} }
} }
@ -1289,7 +1362,7 @@ public:
bool RunModal() override { bool RunModal() override {
//FIXME(emscripten): //FIXME(emscripten):
dbp("FileOpenDialogImplHtml::RunModal()"); dbp("FileOpenDialogImplHtml::RunModal()");
this->filename = "untitled.slvs"; this->filename = "/untitled.slvs";
this->fileUploadHelper.call<void>("showDialog"); this->fileUploadHelper.call<void>("showDialog");
//FIXME(emscripten): use val::await() with JavaScript's Promise //FIXME(emscripten): use val::await() with JavaScript's Promise
@ -1298,7 +1371,7 @@ public:
while (true) { while (true) {
bool is_shown = this->fileUploadHelper["is_shown"].as<bool>(); bool is_shown = this->fileUploadHelper["is_shown"].as<bool>();
if (!is_shown) { if (!is_shown) {
dbp("FileOpenDialogImplHtml: break due to is_shown == false"); // dbp("FileOpenDialogImplHtml: break due to is_shown == false");
break; break;
} else { } else {
// dbp("FileOpenDialogImplHtml: sleep 100msec... (%d)", is_shown); // dbp("FileOpenDialogImplHtml: sleep 100msec... (%d)", is_shown);
@ -1309,7 +1382,7 @@ public:
val selectedFilenameVal = this->fileUploadHelper["currentFilename"]; val selectedFilenameVal = this->fileUploadHelper["currentFilename"];
if (selectedFilenameVal == val::null()) { if (selectedFilenameVal == val::null()) {
dbp("selectedFilenameVal is null"); // dbp("selectedFilenameVal is null");
return false; return false;
} else { } else {
std::string selectedFilename = selectedFilenameVal.as<std::string>(); std::string selectedFilename = selectedFilenameVal.as<std::string>();
@ -1356,10 +1429,12 @@ public:
void AddFilter(std::string name, std::vector<std::string> extensions) override { void AddFilter(std::string name, std::vector<std::string> extensions) override {
this->filters = ""; this->filters = "";
for (auto extension : extensions) { for (size_t i = 0; i < extensions.size(); i++) {
this->filters = "." + extension; if (i != 0) {
this->filters += ","; this->filters += ",";
} }
this->filters = "." + extensions[i];
}
dbp("filter=%s", this->filters.c_str()); dbp("filter=%s", this->filters.c_str());
} }
@ -1381,13 +1456,13 @@ public:
FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) {
// FIXME(emscripten): implement // FIXME(emscripten): implement
dbp("CreateOpenFileDialog()"); // dbp("CreateOpenFileDialog()");
return std::shared_ptr<FileOpenDialogImplHtml>(new FileOpenDialogImplHtml()); return std::shared_ptr<FileOpenDialogImplHtml>(new FileOpenDialogImplHtml());
} }
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
// FIXME(emscripten): implement // FIXME(emscripten): implement
dbp("CreateSaveFileDialog()"); // dbp("CreateSaveFileDialog()");
return std::shared_ptr<FileSaveDummyDialogImplHtml>(new FileSaveDummyDialogImplHtml()); return std::shared_ptr<FileSaveDummyDialogImplHtml>(new FileSaveDummyDialogImplHtml());
} }
@ -1415,7 +1490,7 @@ std::vector<std::string> InitGui(int argc, char **argv) {
val::global("window").call<void>("addEventListener", val("beforeunload"), val::global("window").call<void>("addEventListener", val("beforeunload"),
Wrap(&onBeforeUnload)); Wrap(&onBeforeUnload));
dbp("Set onSaveFinished"); // dbp("Set onSaveFinished");
SS.OnSaveFinished = OnSaveFinishedCallback; SS.OnSaveFinished = OnSaveFinishedCallback;
// FIXME(emscripten): get locale from user preferences // FIXME(emscripten): get locale from user preferences

View File

@ -21,13 +21,21 @@
<progress id="progress" value="0" max="100" hidden="1"></progress> <progress id="progress" value="0" max="100" hidden="1"></progress>
</div> </div>
</div><!-- </div><!--
--><main><!--
FIXME(emscripten): without this, a window resize is required in Chrome FIXME(emscripten): without this, a window resize is required in Chrome
to get the layout to update and canvas size to match up. What? to get the layout to update and canvas size to match up. What?
--><ul class="menu menubar" style="visibility: hidden"><li>None</li></ul><!-- --><ul class="menu menubar" style="visibility: hidden"><li>None</li></ul><!--
--><div id="container"><!-- --><div id="container"><!--
--><div id="container0"><canvas id="canvas0"></canvas></div><!-- --><div id="container0"><canvas id="canvas0"></canvas></div><!--
--><div id="view_separator"></div><!--
--><div id="container1parent"><!--
--><div id="container1"><canvas id="canvas1"></canvas></div><!-- --><div id="container1"><canvas id="canvas1"></canvas></div><!--
--><div id="canvas1scrollbarbox"><!--
--><div id="canvas1scrollbar"></div><!--
--></div><!-- --></div><!--
--></div><!--
--></div><!--
--></main><!--
--><script type="text/javascript"> --><script type="text/javascript">
var splashElement = document.getElementById('splash'); var splashElement = document.getElementById('splash');
var spinnerElement = document.getElementById('spinner'); var spinnerElement = document.getElementById('spinner');

View File

@ -7,6 +7,7 @@ html, body {
background: black; background: black;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
} }
html, body, canvas, #splash, #container { html, body, canvas, #splash, #container {
margin: 0; margin: 0;
@ -53,6 +54,16 @@ body {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Grid layout for main */
main {
height: 100%;
/* Use CSS Grid layout for vertical placement. */
display: grid;
/* Row 0 for menubar (fit to content), Row 1 for canvas0, canvas1 (rest of space) */
grid-template-rows: auto 1fr;
}
/* Buttons */ /* Buttons */
.button { .button {
border: 1px solid hsl(0, 0%, 60%); border: 1px solid hsl(0, 0%, 60%);
@ -70,8 +81,8 @@ body {
/* Editors */ /* Editors */
.editor { .editor {
position: absolute; position: fixed;
padding: 1px 0; padding: 0;
border: none; border: none;
} }
@ -259,19 +270,52 @@ canvas {
#container { #container {
display: flex; display: flex;
height: 100%; overflow: hidden;
} }
/* FIXME(emscripten): this should be dynamically adjustable, not hardcoded in CSS */ /* FIXME(emscripten): this should be dynamically adjustable, not hardcoded in CSS */
#container0 { #container0 {
flex-basis: 80%; flex-basis: 80%;
height: 100%; height: 100%;
position: relative; position: relative;
overflow: hidden;
} }
#container1 {
#container1parent {
flex-basis: 20%; flex-basis: 20%;
height: 100%; height: 100%;
position: relative; position: relative;
overflow: hidden;
min-width: 410px;
display: grid;
grid-template-columns: auto 19px;
grid-template-rows: 100%;
} }
#canvas1 {
min-width: 400px; #container1 {
height: 100%;
}
#canvas1scrollbarbox {
/* 19px is a magic number for scrollbar width (Yes, this is platform-dependent value but looks almost working.) */
width: 19px;
min-width: 19px;
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
background-color: lightgray;
-webkit-overflow-scrolling: auto;
}
#canvas1scrollbar {
/* 0px will disable the scrollbar by browser. */
width: 1px;
/* Disable scrollbar as default. This value will be overwritten by program. */
height: 100%;
}
#view_separator {
width: 4px;
background: hsl(0, 0%, 20%);
} }

View File

@ -2,13 +2,7 @@ function isModal() {
var hasModal = !!document.querySelector('.modal'); var hasModal = !!document.querySelector('.modal');
var hasMenuBar = !!document.querySelector('.menubar .selected'); var hasMenuBar = !!document.querySelector('.menubar .selected');
var hasPopupMenu = !!document.querySelector('.menu.popup'); var hasPopupMenu = !!document.querySelector('.menu.popup');
var hasEditor = false; return hasModal || hasMenuBar || hasPopupMenu;
document.querySelectorAll('.editor').forEach(function(editor) {
if(editor.style.display == "") {
hasEditor = true;
}
});
return hasModal || hasMenuBar || hasPopupMenu || hasEditor;
} }
/* String helpers */ /* String helpers */
@ -78,6 +72,16 @@ function setLabelWithMnemonic(element, labelText) {
} }
} }
/** Touchevent helper
* @param {TouchEvent} event
* @return {boolean} true if same element is target of touchstart and touchend
*/
function isSameElementOnTouchstartAndTouchend(event) {
const elementOnTouchStart = event.target;
const elementOnTouchEnd = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
return elementOnTouchStart == elementOnTouchEnd;
}
/* Button helpers */ /* Button helpers */
function isButton(element) { function isButton(element) {
return hasClass(element, 'button'); return hasClass(element, 'button');
@ -103,6 +107,15 @@ window.addEventListener('click', function(event) {
button.dispatchEvent(new Event('trigger')); button.dispatchEvent(new Event('trigger'));
} }
}); });
window.addEventListener("touchend", (event) => {
if (!isSameElementOnTouchstartAndTouchend(event)) {
return;
}
const button = getButton(event.target);
if (button) {
button.dispatchEvent(new Event('trigger'));
}
});
window.addEventListener('keydown', function(event) { window.addEventListener('keydown', function(event) {
var selected = document.querySelector('.button.selected'); var selected = document.querySelector('.button.selected');
@ -267,6 +280,40 @@ window.addEventListener('click', function(event) {
}); });
} }
}); });
window.addEventListener("touchend", (event) => {
if (!isSameElementOnTouchstartAndTouchend(event)) {
return;
}
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu && isMenubar(menu)) {
if(hasClass(menuItem, 'selected')) {
removeClass(menuItem, 'selected');
} else {
selectMenuItem(menuItem);
}
event.stopPropagation();
event.preventDefault();
} else if(menu) {
if(!hasSubmenu(menuItem)) {
triggerMenuItem(menuItem);
} else {
addClass(menuItem, "selected");
addClass(menuItem, "hover");
}
event.stopPropagation();
} else {
document.querySelectorAll('.menu .selected, .menu .hover')
.forEach(function(menuItem) {
deselectMenuItem(menuItem);
event.stopPropagation();
});
document.querySelectorAll('.menu.popup')
.forEach(function(menu) {
menu.remove();
});
}
});
window.addEventListener('mouseover', function(event) { window.addEventListener('mouseover', function(event) {
var menuItem = getMenuItem(event.target); var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem); var menu = getMenu(menuItem);
@ -643,3 +690,123 @@ function saveFileDone(filename, isSaveAs, isAutosave) {
fileDownloadHelper.showDialog(); fileDownloadHelper.showDialog();
console.log(`shoDialog() finished.`); console.log(`shoDialog() finished.`);
} }
class ScrollbarHelper {
/**
* @param {HTMLElement} elementquery CSS query string for the element that has scrollbar.
*/
constructor(elementquery) {
this.target = document.querySelector(elementquery);
this.rangeMin = 0;
this.rangeMax = 0;
this.currentRatio = 0;
this.onScrollCallback = null;
this.onScrollCallbackTicking = false;
if (this.target) {
// console.log("addEventListner scroll");
this.target.parentElement.addEventListener('scroll', () => {
if (this.onScrollCallbackTicking) {
return;
}
window.requestAnimationFrame(() => {
if (this.onScrollCallback) {
this.onScrollCallback();
}
this.onScrollCallbackTicking = false;
});
this.onScrollCallbackTicking = true;
});
}
}
/**
*
* @param {number} ratio how long against to the viewport height (1.0 to exact same as viewport's height)
*/
setScrollbarSize(ratio) {
// if (isNaN(ratio)) {
// console.warn(`setScrollbarSize(): ratio is Nan = ${ratio}`);
// }
// if (ratio < 0 || ratio > 1) {
// console.warn(`setScrollbarSize(): ratio is out of range 0-1 but ${ratio}`);
// }
// console.log(`ScrollbarHelper.setScrollbarSize(): ratio=${ratio}`);
this.target.style.height = `${100 * ratio}%`;
}
getScrollbarPosition() {
const scrollbarElem = this.target.parentElement;
const scrollTopMin = 0;
const scrollTopMax = scrollbarElem.scrollHeight - scrollbarElem.clientHeight;
const ratioOnScrollbar = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
this.currentRatio = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
let pos = this.currentRatio * (this.rangeMax - this.pageSize - this.rangeMin) + this.rangeMin;
// console.log(`ScrollbarHelper.getScrollbarPosition(): ratio=${ratioOnScrollbar}, pos=${pos}, scrollTop=${scrollbarElem.scrollTop}, scrollTopMin=${scrollTopMin}, scrollTopMax=${scrollTopMax}, rangeMin=${this.rangeMin}, rangeMax=${this.rangeMax}, pageSize=${this.pageSize}`);
if (isNaN(pos)) {
return 0;
} else {
return pos;
}
}
/**
* @param {number} value in range of rangeMin and rangeMax
*/
setScrollbarPosition(position) {
const positionMin = this.rangeMin;
const positionMax = this.rangeMax - this.pageSize;
const currentPositionRatio = (position - positionMin) / (positionMax - positionMin);
const scrollbarElement = this.target.parentElement;
const scrollTopMin = 0;
const scrollTopMax = scrollbarElement.scrollHeight - scrollbarElement.clientHeight;
const scrollWidth = scrollTopMax - scrollTopMin;
const newScrollTop = currentPositionRatio * scrollWidth;
scrollbarElement.scrollTop = currentPositionRatio * scrollWidth;
// console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, currentPositionRatio=${currentPositionRatio}, calculated scrollTop=${newScrollTop}`);
if (false) {
// const ratio = (position - this.rangeMin) * ((this.rangeMax - this.pageSize) - this.rangeMin);
const scrollTopMin = 0;
const scrollTopMax = this.target.scrollHeight - this.target.clientHeight;
const scrollWidth = scrollTopMax - scrollTopMin;
const newScrollTop = ratio * scrollWidth;
// this.target.parentElement.scrollTop = ratio * scrollWidth;
this.target.scrollTop = ratio * scrollWidth;
console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, ratio=${ratio}, calculated scrollTop=${newScrollTop}`);
}
}
/** */
setRange(min, max, pageSize) {
this.rangeMin = min;
this.rangeMax = max;
this.currentRatio = 0;
this.setPageSize(pageSize);
}
setPageSize(pageSize) {
if (this.rangeMin == this.rangeMax) {
// console.log(`ScrollbarHelper::setPageSize(): size=${size}, but rangeMin == rangeMax`);
return;
}
this.pageSize = pageSize;
const ratio = (this.rangeMax - this.rangeMin) / this.pageSize;
// console.log(`ScrollbarHelper::setPageSize(): pageSize=${pageSize}, ratio=${ratio}`);
this.setScrollbarSize(ratio);
}
setScrollbarEnabled(enabled) {
if (!enabled) {
this.target.style.height = "100%";
}
}
};
window.ScrollbarHelper = ScrollbarHelper;