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._requestedEndTile = null;
|
||||
this._requestScheduled = false;
|
||||
this._showJumpDown = false;
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
loadTop = startIndex < 10;
|
||||
this._setShowJumpDown(endIndex < (this._tiles.length - 1));
|
||||
// console.log("got tiles", startIndex, endIndex, loadTop);
|
||||
} else {
|
||||
// tiles collection is empty, load more at top
|
||||
loadTop = true;
|
||||
this._setShowJumpDown(false);
|
||||
// console.log("no tiles, load more at top");
|
||||
}
|
||||
|
||||
|
@ -100,4 +103,15 @@ export class TimelineViewModel extends ViewModel {
|
|||
get 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.
|
||||
*/
|
||||
|
||||
.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 {
|
||||
display: grid;
|
||||
grid-template:
|
||||
|
|
|
@ -14,8 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.Timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.RoomView_body > .Timeline {
|
||||
.Timeline_jumpDown {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.Timeline_scroller {
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior-y: contain;
|
||||
overflow-anchor: none;
|
||||
|
@ -23,9 +32,11 @@ limitations under the License.
|
|||
margin: 0;
|
||||
/* need to read the offsetTop of tiles relative to this element in TimelineView */
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.RoomView_body > .Timeline > ul {
|
||||
.Timeline_scroller > ul {
|
||||
list-style: none;
|
||||
/* 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
|
||||
|
|
|
@ -78,8 +78,19 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
|||
this.restoreScrollPosition();
|
||||
});
|
||||
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
|
||||
const root = t.div({className: "Timeline bottom-aligned-scroll", onScroll: () => this.onScroll()}, [
|
||||
t.view(this.tilesView)
|
||||
const root = t.div({className: "Timeline"}, [
|
||||
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") {
|
||||
|
@ -92,6 +103,16 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
|||
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() {
|
||||
super.unmount();
|
||||
if (this.resizeObserver) {
|
||||
|
@ -101,10 +122,10 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
|||
}
|
||||
|
||||
private restoreScrollPosition() {
|
||||
const timeline = this.root() as HTMLElement;
|
||||
const {scroller} = this;
|
||||
const tiles = this.tilesView!.root() as HTMLElement;
|
||||
|
||||
const missingTilesHeight = timeline.clientHeight - tiles.clientHeight;
|
||||
const missingTilesHeight = scroller.clientHeight - tiles.clientHeight;
|
||||
if (missingTilesHeight > 0) {
|
||||
tiles.style.setProperty("margin-top", `${missingTilesHeight}px`);
|
||||
// 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 {
|
||||
tiles.style.removeProperty("margin-top");
|
||||
if (this.stickToBottom) {
|
||||
timeline.scrollTop = timeline.scrollHeight;
|
||||
scroller.scrollTop = scroller.scrollHeight;
|
||||
} else if (this.anchoredNode) {
|
||||
const newAnchoredBottom = bottom(this.anchoredNode!);
|
||||
if (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
|
||||
// not depend on reading scrollTop, which might be out of date as some platforms
|
||||
// run scrolling off the main thread.
|
||||
if (typeof timeline.scrollBy === "function") {
|
||||
timeline.scrollBy(0, bottomDiff);
|
||||
if (typeof scroller.scrollBy === "function") {
|
||||
scroller.scrollBy(0, bottomDiff);
|
||||
} else {
|
||||
timeline.scrollTop = timeline.scrollTop + bottomDiff;
|
||||
scroller.scrollTop = scroller.scrollTop + bottomDiff;
|
||||
}
|
||||
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
|
||||
|
@ -138,8 +156,8 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
|||
}
|
||||
|
||||
private onScroll(): void {
|
||||
const timeline = this.root() as HTMLElement;
|
||||
const {scrollHeight, scrollTop, clientHeight} = timeline;
|
||||
const {scroller} = this;
|
||||
const {scrollHeight, scrollTop, clientHeight} = scroller;
|
||||
const tiles = this.tilesView!.root() as HTMLElement;
|
||||
|
||||
let bottomNodeIndex;
|
||||
|
|
Loading…
Reference in a new issue