forked from mystiq/hydrogen-web
add "jump down" button in timeline
This commit is contained in:
parent
1df12b8c89
commit
e4101ece65
5 changed files with 130 additions and 15 deletions
|
@ -46,6 +46,7 @@ export class TimelineViewModel extends ViewModel {
|
||||||
this._requestedStartTile = null;
|
this._requestedStartTile = null;
|
||||||
this._requestedEndTile = null;
|
this._requestedEndTile = null;
|
||||||
this._requestScheduled = false;
|
this._requestScheduled = false;
|
||||||
|
this._showJumpDown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** if this.tiles is empty, call this with undefined for both startTile and endTile */
|
/** if this.tiles is empty, call this with undefined for both startTile and endTile */
|
||||||
|
@ -75,10 +76,12 @@ export class TimelineViewModel extends ViewModel {
|
||||||
tile.notifyVisible();
|
tile.notifyVisible();
|
||||||
}
|
}
|
||||||
loadTop = startIndex < 10;
|
loadTop = startIndex < 10;
|
||||||
|
this._setShowJumpDown(endIndex < (this._tiles.length - 1));
|
||||||
// console.log("got tiles", startIndex, endIndex, loadTop);
|
// console.log("got tiles", startIndex, endIndex, loadTop);
|
||||||
} else {
|
} else {
|
||||||
// tiles collection is empty, load more at top
|
// tiles collection is empty, load more at top
|
||||||
loadTop = true;
|
loadTop = true;
|
||||||
|
this._setShowJumpDown(false);
|
||||||
// console.log("no tiles, load more at top");
|
// console.log("no tiles, load more at top");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,4 +103,15 @@ export class TimelineViewModel extends ViewModel {
|
||||||
get tiles() {
|
get tiles() {
|
||||||
return this._tiles;
|
return this._tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setShowJumpDown(show) {
|
||||||
|
if (this._showJumpDown !== show) {
|
||||||
|
this._showJumpDown = show;
|
||||||
|
this.emitChange("showJumpDown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get showJumpDown() {
|
||||||
|
return this._showJumpDown;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="17"
|
||||||
|
height="9"
|
||||||
|
viewBox="0 0 17 9"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg839"
|
||||||
|
sodipodi:docname="chevron-down.svg"
|
||||||
|
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview841"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="45.647059"
|
||||||
|
inkscape:cx="8.0509021"
|
||||||
|
inkscape:cy="8.5219072"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg839" />
|
||||||
|
<g
|
||||||
|
clip-path="url(#clip0)"
|
||||||
|
id="g832"
|
||||||
|
transform="rotate(-90,4.3001277,4.8826258)">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M 8.20723,2.70711 C 8.59775,3.09763 8.59878,3.73182 8.20952,4.1236 L 3.27581,9.08934 8.22556,14.0391 c 0.39052,0.3905 0.39155,1.0247 0.00229,1.4165 -0.38926,0.3918 -1.0214,0.3928 -1.41192,0.0023 L 1.15907,9.80101 C 0.768549,9.41049 0.767523,8.7763 1.15678,8.38452 L 6.79531,2.70939 C 7.18457,2.31761 7.8167,2.31658 8.20723,2.70711 Z"
|
||||||
|
fill="#8d99a5"
|
||||||
|
id="path830" />
|
||||||
|
</g>
|
||||||
|
<defs
|
||||||
|
id="defs837">
|
||||||
|
<clipPath
|
||||||
|
id="clip0">
|
||||||
|
<rect
|
||||||
|
width="8"
|
||||||
|
height="17"
|
||||||
|
fill="#ffffff"
|
||||||
|
transform="rotate(180,4.25,8.5)"
|
||||||
|
id="rect834"
|
||||||
|
x="0"
|
||||||
|
y="0" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -15,6 +15,20 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.Timeline_jumpDown {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 32px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 1px solid #8d99a5;
|
||||||
|
background-image: url(icons/chevron-down.svg);
|
||||||
|
background-position: center;
|
||||||
|
background-color: white;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.Timeline_message {
|
.Timeline_message {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template:
|
grid-template:
|
||||||
|
|
|
@ -14,8 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.Timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.RoomView_body > .Timeline {
|
.Timeline_jumpDown {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_scroller {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overscroll-behavior-y: contain;
|
overscroll-behavior-y: contain;
|
||||||
overflow-anchor: none;
|
overflow-anchor: none;
|
||||||
|
@ -23,9 +32,11 @@ limitations under the License.
|
||||||
margin: 0;
|
margin: 0;
|
||||||
/* need to read the offsetTop of tiles relative to this element in TimelineView */
|
/* need to read the offsetTop of tiles relative to this element in TimelineView */
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomView_body > .Timeline > ul {
|
.Timeline_scroller > ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
/* use small horizontal padding so first/last children margin isn't collapsed
|
/* use small horizontal padding so first/last children margin isn't collapsed
|
||||||
at the edge and a scrollbar shows up when setting margin-top to bottom-align
|
at the edge and a scrollbar shows up when setting margin-top to bottom-align
|
||||||
|
|
|
@ -78,8 +78,19 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
||||||
this.restoreScrollPosition();
|
this.restoreScrollPosition();
|
||||||
});
|
});
|
||||||
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
|
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
|
||||||
const root = t.div({className: "Timeline bottom-aligned-scroll", onScroll: () => this.onScroll()}, [
|
const root = t.div({className: "Timeline"}, [
|
||||||
t.view(this.tilesView)
|
t.div({
|
||||||
|
className: "Timeline_scroller bottom-aligned-scroll",
|
||||||
|
onScroll: () => this.onScroll()
|
||||||
|
}, t.view(this.tilesView)),
|
||||||
|
t.button({
|
||||||
|
className: {
|
||||||
|
"Timeline_jumpDown": true,
|
||||||
|
hidden: vm => !vm.showJumpDown
|
||||||
|
},
|
||||||
|
title: "Jump down",
|
||||||
|
onClick: () => this.jumpDown()
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (typeof ResizeObserver === "function") {
|
if (typeof ResizeObserver === "function") {
|
||||||
|
@ -92,6 +103,16 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get scroller() {
|
||||||
|
return this.root().firstElementChild as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private jumpDown() {
|
||||||
|
const {scroller} = this;
|
||||||
|
this.stickToBottom = true;
|
||||||
|
scroller.scrollTop = scroller.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
public unmount() {
|
public unmount() {
|
||||||
super.unmount();
|
super.unmount();
|
||||||
if (this.resizeObserver) {
|
if (this.resizeObserver) {
|
||||||
|
@ -101,10 +122,10 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private restoreScrollPosition() {
|
private restoreScrollPosition() {
|
||||||
const timeline = this.root() as HTMLElement;
|
const {scroller} = this;
|
||||||
const tiles = this.tilesView!.root() as HTMLElement;
|
const tiles = this.tilesView!.root() as HTMLElement;
|
||||||
|
|
||||||
const missingTilesHeight = timeline.clientHeight - tiles.clientHeight;
|
const missingTilesHeight = scroller.clientHeight - tiles.clientHeight;
|
||||||
if (missingTilesHeight > 0) {
|
if (missingTilesHeight > 0) {
|
||||||
tiles.style.setProperty("margin-top", `${missingTilesHeight}px`);
|
tiles.style.setProperty("margin-top", `${missingTilesHeight}px`);
|
||||||
// we don't have enough tiles to fill the viewport, so set all as visible
|
// we don't have enough tiles to fill the viewport, so set all as visible
|
||||||
|
@ -113,23 +134,20 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
||||||
} else {
|
} else {
|
||||||
tiles.style.removeProperty("margin-top");
|
tiles.style.removeProperty("margin-top");
|
||||||
if (this.stickToBottom) {
|
if (this.stickToBottom) {
|
||||||
timeline.scrollTop = timeline.scrollHeight;
|
scroller.scrollTop = scroller.scrollHeight;
|
||||||
} else if (this.anchoredNode) {
|
} else if (this.anchoredNode) {
|
||||||
const newAnchoredBottom = bottom(this.anchoredNode!);
|
const newAnchoredBottom = bottom(this.anchoredNode!);
|
||||||
if (newAnchoredBottom !== this.anchoredBottom) {
|
if (newAnchoredBottom !== this.anchoredBottom) {
|
||||||
const bottomDiff = newAnchoredBottom - this.anchoredBottom;
|
const bottomDiff = newAnchoredBottom - this.anchoredBottom;
|
||||||
console.log(`restore: scroll by ${bottomDiff} as height changed`);
|
|
||||||
// scrollBy tends to create less scroll jumps than reassigning scrollTop as it does
|
// scrollBy tends to create less scroll jumps than reassigning scrollTop as it does
|
||||||
// not depend on reading scrollTop, which might be out of date as some platforms
|
// not depend on reading scrollTop, which might be out of date as some platforms
|
||||||
// run scrolling off the main thread.
|
// run scrolling off the main thread.
|
||||||
if (typeof timeline.scrollBy === "function") {
|
if (typeof scroller.scrollBy === "function") {
|
||||||
timeline.scrollBy(0, bottomDiff);
|
scroller.scrollBy(0, bottomDiff);
|
||||||
} else {
|
} else {
|
||||||
timeline.scrollTop = timeline.scrollTop + bottomDiff;
|
scroller.scrollTop = scroller.scrollTop + bottomDiff;
|
||||||
}
|
}
|
||||||
this.anchoredBottom = newAnchoredBottom;
|
this.anchoredBottom = newAnchoredBottom;
|
||||||
} else {
|
|
||||||
// console.log("restore: bottom didn't change, must be below viewport");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: should we be updating the visible range here as well as the range might have changed even though
|
// TODO: should we be updating the visible range here as well as the range might have changed even though
|
||||||
|
@ -138,8 +156,8 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onScroll(): void {
|
private onScroll(): void {
|
||||||
const timeline = this.root() as HTMLElement;
|
const {scroller} = this;
|
||||||
const {scrollHeight, scrollTop, clientHeight} = timeline;
|
const {scrollHeight, scrollTop, clientHeight} = scroller;
|
||||||
const tiles = this.tilesView!.root() as HTMLElement;
|
const tiles = this.tilesView!.root() as HTMLElement;
|
||||||
|
|
||||||
let bottomNodeIndex;
|
let bottomNodeIndex;
|
||||||
|
|
Loading…
Reference in a new issue