Добавлено отображение примечаний на месте, по клику на примечании (#50)

This commit is contained in:
Book Pauk
2024-07-28 17:23:16 +07:00
parent afd4d02dad
commit 0430105061
3 changed files with 187 additions and 16 deletions

View File

@@ -39,6 +39,7 @@ export default class DrawHelper {
let center = false;
let space = 0;
let j = 0;
const pad = this.fontSize/2;
//формируем строку
for (const part of line.parts) {
let tOpen = '';
@@ -46,7 +47,12 @@ export default class DrawHelper {
tOpen += (part.style.italic ? '<i>' : '');
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
tOpen += (part.style.note ? `<span style="position: relative;">` +
`<span style="position: absolute; background-color: ${this.textColor}; opacity: 0.1; cursor: pointer; pointer-events: auto; ` +
`height: ${this.fontSize + pad*2}px; padding: ${pad}px; left: -${pad}px; top: -${pad*0.9}px; border-radius: ${this.fontSize}px;" ` +
`onclick="onNoteClickLiberama('${part.style.note.id}', ${part.style.note.orig ? 1 : 0})"><span style="visibility: hidden;">__TEXT</span></span>` : '');
let tClose = '';
tClose += (part.style.note ? '</span>' : '');
tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : '');
@@ -64,6 +70,9 @@ export default class DrawHelper {
if (text && text.trim() == '')
text = `<span style="white-space: pre">${text}</span>`;
if (part.style.note)
tOpen = tOpen.replace('__TEXT', text);
lineText += `${tOpen}${text}${tClose}`;
center = center || part.style.center;

View File

@@ -4,12 +4,12 @@
<div class="absolute" v-html="background"></div>
<div class="absolute" v-html="pageDivider"></div>
</div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollBox1" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div @copy.prevent="copyText" v-html="page1"></div>
</div>
</div>
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollBox2" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div @copy.prevent="copyText" v-html="page2"></div>
</div>
@@ -24,14 +24,9 @@
@wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
>
<div
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div>
<div
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
v-show="showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
@@ -40,6 +35,29 @@
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
<!-- Примечание -->
<Dialog ref="dialog1" v-model="noteDialogVisible">
<!--template #header>
Примечание
</template-->
<div class="column col" style="line-height: 20px; max-width: 400px; max-height: 200px; overflow-x: hidden; overflow-y: auto">
<div v-html="noteHtml"></div>
</div>
<template #footer>
<div class="row col">
<q-btn class="q-px-md q-mr-sm" color="btn2" text-color="app" dense no-caps @click="goToNotes">
В примечаниях
</q-btn>
</div>
<q-btn class="q-px-md" color="btn2" text-color="app" dense no-caps @click="noteDialogVisible = false">
OK
</q-btn>
</template>
</Dialog>
</div>
</template>
@@ -51,6 +69,7 @@ import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import he from 'he';
import Dialog from '../../share/Dialog.vue';
import './TextPage.css';
import * as utils from '../../../share/utils';
@@ -62,7 +81,19 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100;
//обработчик кликов по примечаниям, см. DrawHelper
//коряво, но иначе придется сильно усложнять рендеринг страниц (через Vue)
window.onNoteClickLiberama = (noteId, orig) => {
const textPage = window.textPageLiberama;
if (textPage) {
textPage.showNote(noteId, orig);
}
}
const componentOptions = {
components: {
Dialog
},
watch: {
bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -90,6 +121,7 @@ class TextPage {
_options = componentOptions;
showStatusBar = false;
statusBarClickOpen = false;
clickControl = true;
background = null;
@@ -114,6 +146,10 @@ class TextPage {
meta = null;
noteDialogVisible = false;
noteId = '';
noteHtml = '';
created() {
this.drawHelper = new DrawHelper();
@@ -153,6 +189,8 @@ class TextPage {
await utils.sleep(200);
this.$nextTick(this.onResize);
});
window.textPageLiberama = this;
}
mounted() {
@@ -297,6 +335,8 @@ class TextPage {
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
let page1 = this.$refs.scrollBox1.style;
let page2 = this.$refs.scrollBox2.style;
page1.pointerEvents = page2.pointerEvents = (this.clickControl ? 'none' : 'auto');
page1.perspective = page2.perspective = '3072px';
@@ -1209,6 +1249,46 @@ class TextPage {
event.clipboardData.setData('text/plain', filtered);
}
showNote(noteId, orig) {
const note = this.parsed.notes[noteId];
if (note) {
if (orig) {//show dialog
this.noteId = noteId;
const pad = (note.para.length > 1 ? 20 : 0);
this.noteHtml = note.para.map(p => `<p style="margin: 0; padding-left: ${pad}px">${p}</p>`).join('');
this.noteDialogVisible = true;
} else {//go to orig
this.goToOrigNote(noteId);
}
}
}
goToNotes() {
const note = this.parsed.notes[this.noteId];
if (note && note.noteParaIndex >= 0) {
const para = this.parsed.parsePara(note.noteParaIndex);
this.userBookPosChange = true;
this.bookPos = para.lines[0].begin;
this.noteDialogVisible = false;
}
}
goToOrigNote(noteId) {
const note = this.parsed.notes[noteId];
if (note && note.noteParaIndex >= 0) {
const para = this.parsed.parsePara(note.linkParaIndex);
this.userBookPosChange = true;
this.bookPos = para.lines[0].begin;
this.noteDialogVisible = false;
}
}
}
export default vueComponent(TextPage);
@@ -1244,7 +1324,7 @@ export default vueComponent(TextPage);
}
.events {
z-index: 20;
z-index: 9;
background-color: rgba(0,0,0,0);
}

View File

@@ -86,17 +86,23 @@ export default class BookParser {
let binaryType = '';
let dimPromises = [];
this.coverPageId = '';
this.images = [];
let imageNum = 0;
//примечания
this.notes = {};
let inNote = false;
let noteId = '';
let inNotesBody = false;
//оглавление
this.contents = [];
this.images = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false;
let inSubtitle = false;
let sectionLevel = 0;
let bodyIndex = 0;
let imageNum = 0;
let paraIndex = -1;
let paraOffset = 0;
@@ -289,7 +295,7 @@ export default class BookParser {
if (attrs.href && attrs.href.value) {
const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href);
const {id, local} = this.hrefToId(href);
if (local) {//local
imageNum++;
@@ -322,6 +328,23 @@ export default class BookParser {
}
}
if (tag == 'a') {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value && attrs.type && attrs.type.value === 'note') {//note
const href = attrs.href.value;
const {id, local} = this.hrefToId(href);
if (local) {
inNote = true;
growParagraph(`<note href="${id}" orig="1">`, 0);
if (!this.notes[id]) {
this.notes[id] = {id, linkParaIndex: paraIndex};
}
}
}
}
if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author)
fb2.author = [];
@@ -350,6 +373,11 @@ export default class BookParser {
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
let attrs = sax.getAttrsSync(tail);
if (attrs.name && attrs.name.value === 'notes') {//notes
inNotesBody = true;
}
if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => {
@@ -389,6 +417,23 @@ export default class BookParser {
newParagraph();
isFirstSection = false;
sectionLevel++;
if (inNotesBody) {
let attrs = sax.getAttrsSync(tail);
if (attrs.id && attrs.id.value) {//notes
const id = attrs.id.value;
let note = this.notes[id];
if (!note) {
note = {id};
this.notes[id] = note;
}
note.noteParaIndex = paraIndex;
note.para = [];
noteId = id;
}
}
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
@@ -401,6 +446,14 @@ export default class BookParser {
if (tag == 'p') {
inPara = true;
isFirstTitlePara = false;
if (inNotesBody && noteId) {
if (!inTitle) {
this.notes[noteId].para.push('');
} else {
growParagraph(`<note href="${noteId}">`, 0);
}
}
}
}
@@ -440,11 +493,20 @@ export default class BookParser {
const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars
tag = elemName;
if (tag == 'a' && inNote) {
growParagraph('</note>', 0);
inNote = false;
}
if (tag == 'binary') {
binaryId = '';
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
inNotesBody = false;
}
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
@@ -462,6 +524,10 @@ export default class BookParser {
if (tag == 'p') {
inPara = false;
if (inTitle && inNotesBody && noteId) {
growParagraph('</note>', 0);
}
}
if (tag == 'subtitle') {
@@ -570,6 +636,12 @@ export default class BookParser {
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else
growParagraph(' ', 1);
if (!inTitle && inPara && inNotesBody && noteId) {
const p = this.notes[noteId].para;
if (p.length)
p[p.length - 1] = p[p.length - 1] + text;
}
}
};
@@ -602,7 +674,7 @@ export default class BookParser {
return {fb2};
}
imageHrefToId(id) {
hrefToId(id) {
let local = false;
if (id[0] == '#') {
id = id.substr(1);
@@ -635,7 +707,7 @@ export default class BookParser {
splitToStyle(s) {
let result = [];/*array of {
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number, note: Object},
image: {local: Boolean, inline: Boolean, id: String},
text: String,
}*/
@@ -686,7 +758,7 @@ export default class BookParser {
case 'image': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
image = this.imageHrefToId(attrs.href.value);
image = this.hrefToId(attrs.href.value);
image.inline = false;
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
}
@@ -695,7 +767,7 @@ export default class BookParser {
case 'image-inline': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
const img = this.imageHrefToId(attrs.href.value);
const img = this.hrefToId(attrs.href.value);
img.inline = true;
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
result.push({
@@ -706,6 +778,13 @@ export default class BookParser {
}
break;
}
case 'note': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
style.note = {id: attrs.href.value, orig: attrs.orig?.value};
}
break;
}
}
};
@@ -734,6 +813,9 @@ export default class BookParser {
break;
case 'image-inline':
break;
case 'note':
style.note = false;
break;
}
};