add "jump down" button in timeline

This commit is contained in:
Bruno Windels 2021-09-15 18:30:08 +02:00
parent 1df12b8c89
commit e4101ece65
5 changed files with 130 additions and 15 deletions

View File

@ -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;
}
} }

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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;