Files
replayweb.page/src/story.js
T
Ilya Kreymer 4068a0265d misc usability fixes:
- links: make all replay links valid hashtags instead of empty "#"
- replaybar: fix padding on dropdown menu button
- a11y/focus: only set focus on replay frame if no sidebar is visible
bump electron to latest 10.1.2
set version to 1.1.0!
2020-09-16 13:27:10 -07:00

394 lines
9.1 KiB
JavaScript

import { LitElement, html, css, unsafeCSS } from 'lit-element';
import { wrapCss } from './misc';
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import marked from 'marked';
import { getTS, getReplayLink } from './pageutils';
import Split from 'split.js'
// ===========================================================================
class Story extends LitElement
{
constructor() {
super();
this.collInfo = null;
this.curatedPageMap = {};
this.currList = 0;
this.active = false;
this.lastST = 0;
this.clickTime = 0;
this.isSidebar = false;
this.splitDirection = false;
}
static get properties() {
return {
collInfo: { type: Object },
active: { type: Boolean },
curatedPageMap: { type: Object },
currList: { type: Number },
isSidebar: { type: Boolean },
splitDirection: { type: Boolean }
}
}
recalcSplitter(width) {
this.splitDirection = this.isSidebar || width < 769 ? "vertical" : "horizontal";
}
firstUpdated() {
this.recalcSplitter(document.documentElement.clientWidth);
this.obs = new ResizeObserver((entries, observer) => {
this.recalcSplitter(entries[0].contentRect.width);
});
this.obs.observe(this)
}
updated(changedProperties) {
if (changedProperties.has("collInfo")) {
this.doLoadCurated();
}
if (changedProperties.has("collInfo") || changedProperties.has("isSidebar")) {
this.recalcSplitter(document.documentElement.clientWidth);
}
if (changedProperties.has("splitDirection")) {
this.configureSplitter();
}
if (changedProperties.has("currList") && this.active) {
this.sendChangeEvent({
currList: this.currList,
});
}
}
configureSplitter() {
const sidebar = this.renderRoot.querySelector(".sidebar");
const content = this.renderRoot.querySelector(".main-content");
if (this.splitter) {
try {
this.splitter.destroy();
} catch (e) {}
this.splitter = null;
}
if (sidebar && content && !this.splitter) {
const opts = {
sizes: [20, 80],
gutterSize: 4,
direction: this.splitDirection
}
this.splitter = Split([sidebar, content], opts);
}
}
async doLoadCurated() {
this.curatedPageMap = {};
const pageMap = {};
for (const page of this.collInfo.pages) {
pageMap[page.id] = page;
}
for (const curated of this.collInfo.curatedPages) {
if (!this.curatedPageMap[curated.list]) {
this.curatedPageMap[curated.list] = [];
}
const page = pageMap[curated.page];
if (!page) {
console.log("No Page with id: " + page);
continue;
}
const url = page.url;
const ts = page.ts;
const title = page.title || page.url;
const desc = curated.desc;
this.curatedPageMap[curated.list].push({url, ts, title, desc});
}
this.scrollToList();
}
static get styles() {
return wrapCss(css`
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
min-width: 0px;
justify-content: flex-start;
align-items: center;
}
:host(.sidebar) .columns {
display: flex !important;
flex-direction: column;
}
:host(.sidebar) .column.sidebar.is-one-fifth {
width: 100% !important;
}
${Story.sidebarStyles(unsafeCSS(`:host(.sidebar)`))}
.desc p {
margin-bottom: 1.0em;
}
.columns {
width: 100%;
height: 100%;
justify-self: stretch;
margin: 1.0em 0 0 0 !important;
min-height: 0;
}
.column.main-content {
min-height: 0;
display: flex;
flex-direction: column;
padding: 0px;
margin-left: 0.75em;
}
.column.main-content.main-scroll {
padding-right: 0.75em;
word-break: break-all;
}
ul.menu-list a.is-active {
background-color: #55be6f;
}
ol {
margin-left: 30px;
}
@media screen and (min-width: 769px) {
.columns {
margin-top: 0.75em;
}
.column.sidebar {
max-height: 100%;
overflow-y: auto;
}
}
@media screen and (max-width: 768px) {
${Story.sidebarStyles()}
}
.gutter.gutter-vertical:hover {
cursor: row-resize;
}
.gutter.gutter-horizontal:hover {
cursor: col-resize;
}
`);
}
static sidebarStyles(prefix = css``) {
return css`
${prefix} .columns {
position: relative;
}
${prefix} .column.sidebar {
overflow-y: auto;
margin-top: 0.75em;
}
${prefix} .column.main-content {
position: relative;
overflow-y: auto;
border-top: 1px solid black;
height: 100%;
}
${prefix} .menu {
font-size: 0.80rem;
}`;
}
render() {
const currListNum = this.currList;
return html`
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "2": "1"}">
Story for ${this.collInfo.title}
</div>
<div class="columns">
<div class="column sidebar is-one-fifth">
<aside class="menu">
<ul class="menu-list">
<li>
<a href="#list-0" data-list="0" class="${currListNum === 0 ? 'is-active' : ''} menu-label is-size-4"
@click=${this.onClickScroll}>${this.collInfo.title}</a>
<ul class="menu-list">${this.collInfo.lists.map(list => html`
<li>
<a @click=${this.onClickScroll} href="#list-${list.id}"
data-list="${list.id}"
class="${currListNum === list.id ? 'is-active' : ''}">${list.title}</a>
</li>`)}
</ul>
</li>
</ul>
</aside>
</div>
<div @scroll=${this.onScroll} class="column main-content main-scroll">
<section id="list-0" class="desc">
<h2 class="has-text-centered title is-3">${this.collInfo.title}</h2>
${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ''}
</section>
${this.renderLists()}
</div>
</div>
`;
}
renderLists() {
return html`
${this.collInfo.lists.map((list, i) => html`
<article id="list-${list.id}">
<div class="content">
<hr/>
<h3>${list.title}</h3>
<p>${list.desc}</p>
<ol>
${this.curatedPageMap[list.id] ? this.curatedPageMap[list.id].map((p) => this.renderCPage(p)) : html``}
</ol>
</div>
</article>
`)}`;
}
renderCPage(p) {
const date = new Date(p.ts);
const ts = getTS(date.toISOString());
return html`
<li>
<div class="content">
<a @click="${this.onReplay}" data-url="${p.url}" data-ts="${ts}"
href="${getReplayLink("story", p.url, ts)}">
<p class="is-size-6 has-text-weight-bold has-text-link">${p.title}</p>
<p class="has-text-dark">${p.url}</p>
</a>
<p>${date.toLocaleString()}</p>
<p>${p.desc}</p>
</div>
<hr/>
</li>`;
}
onReplay(event) {
event.preventDefault();
const data = {
url: event.currentTarget.getAttribute("data-url"),
ts: event.currentTarget.getAttribute("data-ts"),
};
this.sendChangeEvent(data);
return false;
}
sendChangeEvent(data) {
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
}
onClickScroll(event) {
event.preventDefault();
//this.pageView = false;
this.currList = Number(event.currentTarget.getAttribute("data-list"));
this.scrollToList();
return false;
}
scrollToList() {
// lists are 1 based, 0 is header, 1 is first list
if (this.currList > this.collInfo.lists.length) {
this.currList = 0;
}
const opts = {behavior: "smooth", block: "nearest", inline: "nearest"};
this.clickTime = new Date().getTime();
const curr = this.renderRoot.getElementById("list-" + this.currList);
if (curr) {
curr.scrollIntoView(opts);
}
}
onScroll(event) {
const scrollable = event.currentTarget;
const curr = this.renderRoot.getElementById("list-" + this.currList);
if (!curr) {
return;
}
let next = curr;
const target = scrollable.offsetTop;
const currST = scrollable.scrollTop;
if (currST > this.lastST) {
while (next.nextElementSibling && next.nextElementSibling.getBoundingClientRect().top < target) {
next = next.nextElementSibling;
}
} else {
while (next.previousElementSibling && next.previousElementSibling.getBoundingClientRect().bottom >= target) {
next = next.previousElementSibling;
}
}
this.lastST = currST;
if (next && next != curr) {
if (next.id.startsWith("list-")) {
this.currList = Number(next.id.slice(5));
}
}
if ((new Date().getTime() - this.clickTime) < 1000) {
return;
}
const sel = this.renderRoot.querySelector(`a[data-list="${this.currList}"]`);
if (sel) {
const opts = {behavior: "smooth", block: "nearest", inline: "nearest"};
sel.scrollIntoView(opts);
}
}
}
customElements.define("wr-coll-story", Story);
export { Story };