176 lines
4.9 KiB
Vue
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>
|