first draft of generic popup and menu views
This commit is contained in:
parent
6fd10b63e5
commit
9bb521986b
6 changed files with 645 additions and 6 deletions
378
prototypes/menu-relative.html
Normal file
378
prototypes/menu-relative.html
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template: "left middle" 1fr /
|
||||||
|
200px 1fr;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .left {
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"welcome" auto
|
||||||
|
"rooms" 1fr /
|
||||||
|
1fr;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .middle {
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"header" auto
|
||||||
|
"timeline" 1fr
|
||||||
|
"composer" auto /
|
||||||
|
1fr;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left { grid-area: left;}
|
||||||
|
.left p {
|
||||||
|
grid-area welcome;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.left ul {
|
||||||
|
grid-area: rooms;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle { grid-area: middle;}
|
||||||
|
.middle .header { grid-area: header;}
|
||||||
|
.middle .timeline {
|
||||||
|
grid-area: timeline;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.middle .composer {
|
||||||
|
grid-area: composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer input {
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 2px 2px 10px rgba(0,0,0,0.5);
|
||||||
|
padding: 16px;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 1;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="left">
|
||||||
|
<p>Welcome!<button>⋮</button></p>
|
||||||
|
<ul>
|
||||||
|
<li>Room xyz <button>⋮</button></li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz <button>⋮</button></li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz</li>
|
||||||
|
<li>Room xyz <button>⋮</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="middle">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Room xyz</h2>
|
||||||
|
<button>⋮</button>
|
||||||
|
</div>
|
||||||
|
<ul class="timeline">
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc <button>⋮</button></li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc <button>⋮</button></li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc</li>
|
||||||
|
<li>Message abc <button>⋮</button></li>
|
||||||
|
</ul>
|
||||||
|
<div class="composer">
|
||||||
|
<input type="text" name="">
|
||||||
|
<button>⋮</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
let menu;
|
||||||
|
|
||||||
|
function createMenu(options) {
|
||||||
|
const menu = document.createElement("ul");
|
||||||
|
menu.className = "menu";
|
||||||
|
for (const o of options) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.innerText = o;
|
||||||
|
menu.appendChild(li);
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showMenu(evt) {
|
||||||
|
if (menu) {
|
||||||
|
menu = menu.close();
|
||||||
|
} else if (evt.target.tagName.toLowerCase() === "button") {
|
||||||
|
menu = showPopup(evt.target, createMenu(["Send file", "Save contact", "Send picture", "Foo the bar"]), {
|
||||||
|
horizontal: {
|
||||||
|
relativeTo: "end",
|
||||||
|
align: "start",
|
||||||
|
after: 0,
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
relativeTo: "end",
|
||||||
|
align: "end",
|
||||||
|
after: 10,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMenuInScroller(evt) {
|
||||||
|
if (!menu && evt.target.tagName.toLowerCase() === "button") {
|
||||||
|
evt.stopPropagation();
|
||||||
|
menu = showPopup(evt.target, createMenu(["Show reactions", "Share"]), {
|
||||||
|
horizontal: {
|
||||||
|
relativeTo: "start",
|
||||||
|
align: "end",
|
||||||
|
after: 10,
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
relativeTo: "start",
|
||||||
|
align: "center",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.addEventListener("click", showMenu, false);
|
||||||
|
document.querySelector(".middle ul").addEventListener("click", showMenuInScroller, false);
|
||||||
|
document.querySelector(".left ul").addEventListener("click", showMenuInScroller, false);
|
||||||
|
|
||||||
|
function showPopup(target, popup, arrangement) {
|
||||||
|
targetAxes = elementToAxes(target);
|
||||||
|
if (!arrangement) {
|
||||||
|
arrangement = getAutoArrangement(targetAxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.offsetParent.appendChild(popup);
|
||||||
|
|
||||||
|
const popupAxes = elementToAxes(popup);
|
||||||
|
const scrollerAxes = elementToAxes(findScrollParent(target));
|
||||||
|
const offsetParentAxes = elementToAxes(target.offsetParent);
|
||||||
|
|
||||||
|
function reposition() {
|
||||||
|
if (scrollerAxes && !isVisibleInScrollParent(targetAxes.vertical, scrollerAxes.vertical)) {
|
||||||
|
popupObj.close();
|
||||||
|
}
|
||||||
|
applyArrangement(
|
||||||
|
popupAxes.vertical,
|
||||||
|
targetAxes.vertical,
|
||||||
|
offsetParentAxes.vertical,
|
||||||
|
scrollerAxes?.vertical,
|
||||||
|
arrangement.vertical
|
||||||
|
);
|
||||||
|
applyArrangement(
|
||||||
|
popupAxes.horizontal,
|
||||||
|
targetAxes.horizontal,
|
||||||
|
offsetParentAxes.horizontal,
|
||||||
|
scrollerAxes?.horizontal,
|
||||||
|
arrangement.horizontal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
reposition();
|
||||||
|
|
||||||
|
document.body.addEventListener("scroll", reposition, true);
|
||||||
|
|
||||||
|
const popupObj = {
|
||||||
|
close() {
|
||||||
|
document.body.removeEventListener("scroll", reposition, true);
|
||||||
|
popup.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return popupObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementToAxes(element) {
|
||||||
|
if (element) {
|
||||||
|
return {
|
||||||
|
horizontal: new HorizontalAxis(element),
|
||||||
|
vertical: new VerticalAxis(element),
|
||||||
|
element
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findScrollParent(el) {
|
||||||
|
let parent = el;
|
||||||
|
do {
|
||||||
|
parent = parent.parentElement;
|
||||||
|
if (parent.scrollHeight > parent.clientHeight) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
} while (parent !== el.offsetParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleInScrollParent(targetAxis, scrollerAxis) {
|
||||||
|
// clipped at start?
|
||||||
|
if ((targetAxis.offsetStart + targetAxis.clientSize) < (
|
||||||
|
scrollerAxis.offsetStart +
|
||||||
|
scrollerAxis.scrollOffset
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// clipped at end?
|
||||||
|
if (targetAxis.offsetStart > (
|
||||||
|
scrollerAxis.offsetStart +
|
||||||
|
scrollerAxis.clientSize +
|
||||||
|
scrollerAxis.scrollOffset
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyArrangement(elAxis, targetAxis, offsetParentAxis, scrollerAxis, {relativeTo, align, before, after}) {
|
||||||
|
if (relativeTo === "end") {
|
||||||
|
let end = offsetParentAxis.clientSize - targetAxis.offsetStart;
|
||||||
|
if (align === "end") {
|
||||||
|
end -= elAxis.offsetSize;
|
||||||
|
} else if (align === "center") {
|
||||||
|
end -= ((elAxis.offsetSize / 2) - (targetAxis.offsetSize / 2));
|
||||||
|
}
|
||||||
|
if (typeof before === "number") {
|
||||||
|
end += before;
|
||||||
|
} else if (typeof after === "number") {
|
||||||
|
end -= (targetAxis.offsetSize + after);
|
||||||
|
}
|
||||||
|
elAxis.end = end;
|
||||||
|
} else if (relativeTo === "start") {
|
||||||
|
let scrollOffset = scrollerAxis?.scrollOffset || 0;
|
||||||
|
let start = targetAxis.offsetStart - scrollOffset;
|
||||||
|
if (align === "start") {
|
||||||
|
start -= elAxis.offsetSize;
|
||||||
|
} else if (align === "center") {
|
||||||
|
start -= ((elAxis.offsetSize / 2) - (targetAxis.offsetSize / 2));
|
||||||
|
}
|
||||||
|
if (typeof before === "number") {
|
||||||
|
start -= before;
|
||||||
|
} else if (typeof after === "number") {
|
||||||
|
start += (targetAxis.offsetSize + after);
|
||||||
|
}
|
||||||
|
elAxis.start = start;
|
||||||
|
} else {
|
||||||
|
throw new Error("unknown relativeTo: " + relativeTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HorizontalAxis {
|
||||||
|
constructor(el) {
|
||||||
|
this.element = el;
|
||||||
|
}
|
||||||
|
get scrollOffset() {return this.element.scrollLeft;}
|
||||||
|
get clientSize() {return this.element.clientWidth;}
|
||||||
|
get offsetSize() {return this.element.offsetWidth;}
|
||||||
|
get offsetStart() {return this.element.offsetLeft;}
|
||||||
|
set start(value) {this.element.style.left = `${value}px`;}
|
||||||
|
set end(value) {this.element.style.right = `${value}px`;}
|
||||||
|
}
|
||||||
|
class VerticalAxis {
|
||||||
|
constructor(el) {
|
||||||
|
this.element = el;
|
||||||
|
}
|
||||||
|
get scrollOffset() {return this.element.scrollTop;}
|
||||||
|
get clientSize() {return this.element.clientHeight;}
|
||||||
|
get offsetSize() {return this.element.offsetHeight;}
|
||||||
|
get offsetStart() {return this.element.offsetTop;}
|
||||||
|
set start(value) {this.element.style.top = `${value}px`;}
|
||||||
|
set end(value) {this.element.style.bottom = `${value}px`;}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -96,6 +96,8 @@ main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* otherwise we don't get scrollbars and the content grows as large as it can */
|
/* otherwise we don't get scrollbars and the content grows as large as it can */
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
/* make popups relative to this element so changing the left panel width doesn't affect their position */
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomView {
|
.RoomView {
|
||||||
|
@ -163,3 +165,8 @@ main {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
|
@ -762,4 +762,31 @@ button.link {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 2px 2px 10px rgba(0,0,0,0.5);
|
||||||
|
padding: 4px;
|
||||||
|
background-color: white;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu button {
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 32px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu button:focus {
|
||||||
|
background-color: #03B381;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu button:hover {
|
||||||
|
background-color: #03B381;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
49
src/platform/web/ui/general/Menu.js
Normal file
49
src/platform/web/ui/general/Menu.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {TemplateView} from "./TemplateView.js";
|
||||||
|
|
||||||
|
export class Menu extends TemplateView {
|
||||||
|
static option(label, callback) {
|
||||||
|
return new MenuOption(label, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super();
|
||||||
|
this._options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(t) {
|
||||||
|
return t.ul({className: "menu", role: "menu"}, this._options.map(o => {
|
||||||
|
return t.li({
|
||||||
|
className: o.icon ? `icon ${o.icon}` : "",
|
||||||
|
}, t.button({onClick: o.callback}, o.label));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuOption {
|
||||||
|
constructor(label, callback) {
|
||||||
|
this.label = label;
|
||||||
|
this.callback = callback;
|
||||||
|
this.icon = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIcon(className) {
|
||||||
|
this.icon = className;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
174
src/platform/web/ui/general/Popup.js
Normal file
174
src/platform/web/ui/general/Popup.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HorizontalAxis = {
|
||||||
|
scrollOffset(el) {return el.scrollLeft;},
|
||||||
|
size(el) {return el.offsetWidth;},
|
||||||
|
offsetStart(el) {return el.offsetLeft;},
|
||||||
|
setStart(el, value) {el.style.left = `${value}px`;},
|
||||||
|
setEnd(el, value) {el.style.right = `${value}px`;},
|
||||||
|
};
|
||||||
|
const VerticalAxis = {
|
||||||
|
scrollOffset(el) {return el.scrollTop;},
|
||||||
|
size(el) {return el.offsetHeight;},
|
||||||
|
offsetStart(el) {return el.offsetTop;},
|
||||||
|
setStart(el, value) {el.style.top = `${value}px`;},
|
||||||
|
setEnd(el, value) {el.style.bottom = `${value}px`;},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Popup {
|
||||||
|
constructor(view) {
|
||||||
|
this._view = view;
|
||||||
|
this._target = null;
|
||||||
|
this._arrangement = null;
|
||||||
|
this._scroller = null;
|
||||||
|
this._fakeRoot = null;
|
||||||
|
this._trackingTemplateView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInTemplateView(templateView) {
|
||||||
|
this._trackingTemplateView = templateView;
|
||||||
|
this._trackingTemplateView.addSubView(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
showRelativeTo(target, arrangement) {
|
||||||
|
this._target = target;
|
||||||
|
this._arrangement = arrangement;
|
||||||
|
this._scroller = findScrollParent(this._target);
|
||||||
|
this._view.mount();
|
||||||
|
this._target.offsetParent.appendChild(this._popup);
|
||||||
|
this._applyArrangementAxis(HorizontalAxis, this._arrangement.horizontal);
|
||||||
|
this._applyArrangementAxis(VerticalAxis, this._arrangement.vertical);
|
||||||
|
if (this._scroller) {
|
||||||
|
document.body.addEventListener("scroll", this, true);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.addEventListener("click", this, false);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._view.unmount();
|
||||||
|
this._trackingTemplateView.removeSubView(this);
|
||||||
|
if (this._scroller) {
|
||||||
|
document.body.removeEventListener("scroll", this, true);
|
||||||
|
}
|
||||||
|
document.body.removeEventListener("click", this, false);
|
||||||
|
this._popup.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
get _popup() {
|
||||||
|
return this._view.root();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent(evt) {
|
||||||
|
if (evt.type === "scroll") {
|
||||||
|
this._onScroll();
|
||||||
|
} else if (evt.type === "click") {
|
||||||
|
this._onClick(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onScroll() {
|
||||||
|
if (this._scroller && !this._isVisibleInScrollParent(VerticalAxis)) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
this._applyArrangementAxis(HorizontalAxis, this._arrangement.horizontal);
|
||||||
|
this._applyArrangementAxis(VerticalAxis, this._arrangement.vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyArrangementAxis(axis, {relativeTo, align, before, after}) {
|
||||||
|
if (relativeTo === "end") {
|
||||||
|
let end = axis.size(this._target.offsetParent) - axis.offsetStart(this._target);
|
||||||
|
if (align === "end") {
|
||||||
|
end -= axis.size(this._popup);
|
||||||
|
} else if (align === "center") {
|
||||||
|
end -= ((axis.size(this._popup) / 2) - (axis.size(this._target) / 2));
|
||||||
|
}
|
||||||
|
if (typeof before === "number") {
|
||||||
|
end += before;
|
||||||
|
} else if (typeof after === "number") {
|
||||||
|
end -= (axis.size(this._target) + after);
|
||||||
|
}
|
||||||
|
axis.setEnd(this._popup, end);
|
||||||
|
} else if (relativeTo === "start") {
|
||||||
|
let scrollOffset = this._scroller ? axis.scrollOffset(this._scroller) : 0;
|
||||||
|
let start = axis.offsetStart(this._target) - scrollOffset;
|
||||||
|
if (align === "start") {
|
||||||
|
start -= axis.size(this._popup);
|
||||||
|
} else if (align === "center") {
|
||||||
|
start -= ((axis.size(this._popup) / 2) - (axis.size(this._target) / 2));
|
||||||
|
}
|
||||||
|
if (typeof before === "number") {
|
||||||
|
start -= before;
|
||||||
|
} else if (typeof after === "number") {
|
||||||
|
start += (axis.size(this._target) + after);
|
||||||
|
}
|
||||||
|
axis.setStart(this._popup, start);
|
||||||
|
} else {
|
||||||
|
throw new Error("unknown relativeTo: " + relativeTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isVisibleInScrollParent(axis) {
|
||||||
|
// clipped at start?
|
||||||
|
if ((axis.offsetStart(this._target) + axis.size(this._target)) < (
|
||||||
|
axis.offsetStart(this._scroller) +
|
||||||
|
axis.scrollOffset(this._scroller)
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// clipped at end?
|
||||||
|
if (axis.offsetStart(this._target) > (
|
||||||
|
axis.offsetStart(this._scroller) +
|
||||||
|
axis.size(this._scroller) +
|
||||||
|
axis.scrollOffset(this._scroller)
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fake UIView api, so it can be tracked by a template view as a subview */
|
||||||
|
root() {
|
||||||
|
return this._fakeRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
this._fakeRoot = document.createComment("popup");
|
||||||
|
return this._fakeRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findScrollParent(el) {
|
||||||
|
let parent = el;
|
||||||
|
do {
|
||||||
|
parent = parent.parentElement;
|
||||||
|
if (parent.scrollHeight > parent.clientHeight) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
} while (parent !== el.offsetParent);
|
||||||
|
}
|
|
@ -44,9 +44,6 @@ export class TemplateView {
|
||||||
this._render = render;
|
this._render = render;
|
||||||
this._eventListeners = null;
|
this._eventListeners = null;
|
||||||
this._bindings = null;
|
this._bindings = null;
|
||||||
// this should become _subViews and also include templates.
|
|
||||||
// How do we know which ones we should update though?
|
|
||||||
// Wrapper class?
|
|
||||||
this._subViews = null;
|
this._subViews = null;
|
||||||
this._root = null;
|
this._root = null;
|
||||||
this._boundUpdateFromValue = null;
|
this._boundUpdateFromValue = null;
|
||||||
|
@ -57,7 +54,7 @@ export class TemplateView {
|
||||||
}
|
}
|
||||||
|
|
||||||
_subscribe() {
|
_subscribe() {
|
||||||
if (typeof this._value.on === "function") {
|
if (typeof this._value?.on === "function") {
|
||||||
this._boundUpdateFromValue = this._updateFromValue.bind(this);
|
this._boundUpdateFromValue = this._updateFromValue.bind(this);
|
||||||
this._value.on("change", this._boundUpdateFromValue);
|
this._value.on("change", this._boundUpdateFromValue);
|
||||||
}
|
}
|
||||||
|
@ -146,12 +143,19 @@ export class TemplateView {
|
||||||
this._bindings.push(bindingFn);
|
this._bindings.push(bindingFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
_addSubView(view) {
|
addSubView(view) {
|
||||||
if (!this._subViews) {
|
if (!this._subViews) {
|
||||||
this._subViews = [];
|
this._subViews = [];
|
||||||
}
|
}
|
||||||
this._subViews.push(view);
|
this._subViews.push(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeSubView(view) {
|
||||||
|
const idx = this._subViews.indexOf(view);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this._subViews.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// what is passed to render
|
// what is passed to render
|
||||||
|
@ -288,7 +292,7 @@ class TemplateBuilder {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return errorToDOM(err);
|
return errorToDOM(err);
|
||||||
}
|
}
|
||||||
this._templateView._addSubView(view);
|
this._templateView.addSubView(view);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue