Files
daydream/src/components/BookPage.vue
T
2026-09-03 18:28:33 +02:00

176 lines
4.9 KiB
Vue

<script>
import { formatDate, formatTime } from '@scripts/time';
/**
* One page of the book.
*
* A page is dumb on purpose: it is handed a list of already-broken visual lines
* and draws them, the stamps in its margin, and whatever overlay (caret,
* selection) the book has measured for it. All the deciding - where lines
* break, which page they land on, where the caret is - happens in Book.vue,
* because none of it can be decided a page at a time.
*/
export default {
props: {
//Visual lines for this page: {id, start, end, first}
lines: {type: Array, required: true},
//id -> entry, for the text and the margin stamps
entries: {type: Map, required: true},
//Position of this page in the whole book, for the page number
pageIndex: {type: Number, required: true},
side: {type: String, default: 'left'},
//The entry still being written, drawn a shade darker than past pages
openId: {type: Number, default: 0},
lineHeight: {type: Number, default: 0},
linesPerPage: {type: Number, default: 1},
//Set on the page that currently holds the writing caret
writable: {type: Boolean, default: false},
//{line, x} in page coordinates, or null
caret: {type: Object, default: null},
//[{line, left, width}] - the book draws its own selection, since the
//input's native one is invisible
selection: {type: Array, default: () => []},
//Line to hang the "write here" hint on, or -1
placeholderLine: {type: Number, default: -1},
placeholder: {type: String, default: ''}
},
emits: ['pick'],
inject: ['lang'],
computed: {
//Where an entry begins, the margin says when it was written
stamps() {
const asStamps = [];
this.lines.forEach((oLine, iIndex) => {
if(!oLine.first) return;
const oEntry = this.entries.get(oLine.id);
if(!oEntry) return;
asStamps.push({
id: oLine.id,
line: iIndex,
date: formatDate(oEntry.time, oEntry.timezone, this.lang.locale),
time: formatTime(oEntry.time, oEntry.timezone, this.lang.locale)
});
});
return asStamps;
},
pageNumber() {
return this.pageIndex + 1;
}
},
methods: {
lineText(oLine) {
const oEntry = this.entries.get(oLine.id);
return oEntry ? oEntry.content.slice(oLine.start, oLine.end) : '';
},
isOpenLine(oLine) {
return (oLine.id === this.openId);
},
offsetFor(iLine) {
return (iLine * this.lineHeight) + 'px';
},
/* Read back by Book.vue, which needs real geometry to place the caret,
* the selection, the input and the measurer. */
getLinesElement() {
return this.$refs.lines || null;
},
getBodyElement() {
return this.$refs.body || null;
},
/**
* The nth rendered line.
*
* Queried from the DOM rather than read out of a `ref` on the v-for:
* Vue makes no promise that a v-for ref array is in source order, and
* when it is not, the caret gets measured against the wrong line - which
* lands it back at the start of the line instead of after what was
* just typed.
*/
getLineElement(iIndex) {
const oLines = this.$refs.lines;
if(!oLines) return null;
return oLines.querySelectorAll('.page__line')[iIndex] || null;
},
onClick(oEvent) {
if(!this.writable || (this.lineHeight <= 0)) return;
const oLines = this.$refs.lines;
if(!oLines) return;
const oRect = oLines.getBoundingClientRect();
const iLine = Math.floor((oEvent.clientY - oRect.top) / this.lineHeight);
this.$emit('pick', {
pageIndex: this.pageIndex,
lineIndex: Math.max(0, Math.min(this.linesPerPage - 1, iLine)),
clientX: oEvent.clientX,
extend: oEvent.shiftKey
});
}
}
};
</script>
<template>
<section
class="page"
:class="['page--' + side, writable ? 'page--writable' : null]"
@mousedown.prevent="onClick"
>
<div ref="body" class="page__body">
<div class="page__margin">
<span
v-for="oStamp in stamps"
:key="oStamp.id"
class="page__stamp"
:style="{top: offsetFor(oStamp.line)}"
>
<span class="page__stamp-date">{{ oStamp.date }}</span>
<span class="page__stamp-time">{{ oStamp.time }}</span>
</span>
</div>
<div class="page__column">
<div ref="lines" class="page__lines">
<span
v-for="(oSelection, iIndex) in selection"
:key="'sel' + iIndex"
class="page__selection"
:style="{top: offsetFor(oSelection.line), left: oSelection.left + 'px', width: oSelection.width + 'px'}"
></span>
<span
v-for="(oLine, iIndex) in lines"
:key="iIndex"
class="page__line"
:class="isOpenLine(oLine) ? null : 'page__line--closed'"
>{{ lineText(oLine) }}</span>
<span
v-if="placeholderLine >= 0"
class="page__placeholder"
:style="{top: offsetFor(placeholderLine)}"
>{{ placeholder }}</span>
<span
v-if="caret"
class="page__caret"
:style="{top: offsetFor(caret.line), left: caret.x + 'px'}"
></span>
</div>
</div>
</div>
<div class="page__foot">
<span class="page__number">{{ pageNumber }}</span>
</div>
</section>
</template>