ebt-vue3
Version:
Vue3 Library for SuttaCentral Voice EBT-Sites
827 lines (741 loc) • 23.6 kB
JavaScript
import { logger } from 'log-instance/index.mjs';
import { v4 as uuidv4 } from 'uuid';
import { AuthorsV2, SuttaRef } from 'scv-esm/main.mjs';
import { default as Playlist } from './playlist.mjs';
import {
DBG,
DBG_CLICK,
DBG_OPEN_CARD,
DBG_VIEWPORT, DBG_GRAPH,
} from './defines.mjs';
export const CONTEXT_WIKI = "wiki";
export const CONTEXT_SEARCH = "search";
export const CONTEXT_SUTTA = "sutta";
export const CONTEXT_DEBUG = "debug";
export const CONTEXT_GRAPH = "graph";
export const CONTEXT_PLAY = "play";
export const CONTEXT_PALI = "pali";
const CONTEXTS = {
[CONTEXT_PALI]: {
icon: "mdi-book-information-variant",
alt1Icon: "mdi-magnify",
iconTitle: "Pāli",
},
[CONTEXT_WIKI]: {
icon: "mdi-wikipedia",
alt1Icon: "mdi-home",
iconTitle: "Wiki",
},
[CONTEXT_SEARCH]: {
icon: "mdi-magnify",
alt1Icon: "mdi-account-voice",
iconTitle: "Search",
},
[CONTEXT_PLAY]: {
icon: "mdi-file-document-multiple-outline",
},
[CONTEXT_SUTTA]: {
icon: "mdi-file-document-outline",
alt1Icon: "mdi-graph-outline",
iconTitle: "Dhamma",
},
[CONTEXT_DEBUG]: {
icon: "mdi-tools",
iconTitle: "Debug",
},
[CONTEXT_GRAPH]: {
icon: "mdi-graph-outline",
alt1Icon: "mdi-file-document-outline",
iconTitle: "Graph",
},
}
const API_ENDPOINT = 'https://www.api.sc-voice.net/scv/ebt-site';
export default class EbtCard {
constructor(opts = {}) {
let msg = 'ebt-card.ctor() ';
let dbg = DBG.ADD_CARD;
let {
id,
context,
location=[],
isOpen,
data = undefined,
langTrans, // factory prop
titleHref,
playlist,
} = opts;
if (context == null || context === '') {
context = CONTEXT_WIKI;
}
context = context.toLowerCase();
if (id == null) {
id = context===CONTEXT_WIKI
? 'home-card-id'
: uuidv4().split('-').pop();
}
if (typeof location === 'string') {
location = [location];
}
if (!(location instanceof Array)) {
throw new Error('Expected location array');
}
let contextLoc = [context, ...location].join('/');
if (playlist && !(playlist instanceof Playlist)) {
playlist = new Playlist(playlist);
dbg && console.log(msg, '[1]playlist',
JSON.stringify(playlist, null, 2));
}
switch (context) {
case CONTEXT_WIKI:
isOpen = isOpen === undefined ? false : isOpen;
dbg && console.log(msg, `[2]${context}`, {isOpen, contextLoc});
break;
case CONTEXT_DEBUG:
if (location[0] == null) {
location[0] = 'Debug';
dbg && console.log(msg, `[3]${context}`, location);
}
break;
case CONTEXT_SEARCH:
if (location[0] == null) {
location[0] = '';
}
if (location.length === 1) {
dbg && console.log(msg, `[4]${context}`, {langTrans});
langTrans && location.push(langTrans);
}
break;
case CONTEXT_SUTTA:
location[1] == null && (location[1] = langTrans);
location[2] == null &&
(location[2] = AuthorsV2.langAuthor(location[1]));
dbg && console.log(msg, `[5]${context}`, location);
titleHref = titleHref ||
`https://suttacentral.net/${location[0]}`;
break;
case CONTEXT_GRAPH:
location[0] = location[0] || 'mn44';
location[1] = location[1] || langTrans;
dbg && console.log(msg, `[6]${context}`, location);
break;
case CONTEXT_PLAY: {
location[1] == null && (location[1] = langTrans);
location[2] == null &&
(location[2] = AuthorsV2.langAuthor(location[1]));
titleHref = titleHref ||
`https://suttacentral.net/${location[0]}`;
if (dbg) {
let { suttaRefs } = playlist || {};
console.log(msg, `[7]${context}`, {
location,
playlist,
suttaRefs,
});
}
} break;
case CONTEXT_PALI:
dbg && console.log(msg, `[8]${context}`, location);
break;
}
Object.assign(this, {// primary properties
id,
location,
context,
data,
isOpen: isOpen === undefined ? true : isOpen,
titleHref,
playlist,
});
// secondary properties
//logger.info(msg, `${context} ${id} ${location[0]}`);
}
static get CONTEXT_WIKI() { return CONTEXT_WIKI; }
static get CONTEXT_SEARCH() { return CONTEXT_SEARCH; }
static get CONTEXT_SUTTA() { return CONTEXT_SUTTA; }
static get CONTEXT_PLAY() { return CONTEXT_PLAY; }
static get CONTEXT_DEBUG() { return CONTEXT_DEBUG; }
static get CONTEXT_GRAPH() { return CONTEXT_GRAPH; }
static get CONTEXT_PALI() { return CONTEXT_PALI; }
static routeSuttaRef(route, langTrans='en') {
const msg = 'ebt-card.routeSuttaRef()';
let routeParts = route.split(`#/${CONTEXT_SUTTA}`);
//console.log(msg, {route, langTrans, routeParts});
if (routeParts.length !== 2) {
routeParts = route.split(`#/${CONTEXT_PLAY}`);
}
if (routeParts.length !== 2) {
return null;
}
let refStr = routeParts[1].slice(1);
return SuttaRef.create(refStr, langTrans)
}
static pathToCard(args) {
const msg = 'ebt-card.pathToCard()';
const dbg = DBG.CARD_PATH;
let {
path='/', cards=[], addCard, defaultLang, isOpen,
} = args;
path = path.replace(/^.*\/#/, ''); // ignore non-hash part of path
let [ ignored, context, ...location ] = path.split('/');
location = location.map(loc => decodeURIComponent(loc));
let card = cards.find(card => card.matchPath({path, defaultLang}));
dbg && console.log(msg, '[1]find', {card, path});
if (card == null) {
if (addCard === undefined) {
throw new Error(msg+"addCard is required");
}
if (context) {
dbg && console.log(msg, '[2]addCard',
{context,location,isOpen});
card = addCard ? addCard({context, location, isOpen}) : null;
}
} else {
dbg && console.log(msg, '[3]existing', card.debugString);
}
if (card) { // context already matches, so check location
switch (card.context) {
case CONTEXT_WIKI: {
let newLocation = path.split('/').slice(2);
if (newLocation.length) {
card.location = newLocation;
dbg && console.log(msg, '[4]newLocation', card.debugString,
newLocation);
}
} break;
case CONTEXT_PLAY:
case CONTEXT_SUTTA: {
if (location[0].indexOf(':') >= 0) { // different scid
dbg && console.log(msg, '[5]location', card.debugString,
location[0]);
card.location[0] = location[0];
}
} break;
}
}
return card;
}
get alt1Id() {
return `${this.id}-alt1`;
}
get tab1Id() {
return `${this.id}-tab1`;
}
get firstTabId() {
return this.alt1Icon
? this.alt1Id
: this.tab1Id;
}
get containerId() {
return `${this.id}-container`;
}
get deleteId() {
return `${this.id}-delete`;
}
get autofocusId() {
return `${this.id}-autofocus`;
}
get icon() {
return CONTEXTS[this.context]?.icon || "mdi-alert-icon";
}
get iconTitle() {
let { context, id } = this;
let title = CONTEXTS[context]?.iconTitle || `${context}?`;
return `${title} card #${id.substring(0,4)}\u2026`;
}
get alt1Icon() {
return CONTEXTS[this.context]?.alt1Icon || "mdi-alert-icon";
}
get topAnchor() {
return `${this.id}-top`;
}
get titleAnchor() {
return `${this.id}-title`;
}
get isSuttaCard() {
let { context } = this;
switch (context) {
case CONTEXT_PLAY:
case CONTEXT_SUTTA:
return true;
break;
}
return false;
}
alt1Disabled() {
const msg = "EbtCard.alt1Disabled()";
let { context, data } = this;
let disabled = false;
switch (context) {
case CONTEXT_SEARCH:
disabled = !data || data.length<2;
break;
}
//console.log(msg, {disabled, context});
return disabled;
}
get currentElementId() {
const msg = "ebt-data.currentElementId()";
const dbg = DBG_CLICK;
let { titleAnchor, tab1Id, deleteId, context, location } = this;
let aeId = document?.activeElement?.id;
if (this.isSuttaCard) {
return location.length>0 && location[0].includes(':')
? this.segmentElementId()
: titleAnchor;
}
return this.titleAnchor;
}
get debugString() {
let { isOpen, id, context, location} = this;
let separator = isOpen ? '+' : '-';
return `${id}${separator}${context}`;
}
hasFocus(appFocus) {
const msg = "ebt-card.hasFocus()";
const dbg = DBG.FOCUS && DBG.VERBOSE;
let { containerId } = this;
let hasFocus = false;
for (let elt=appFocus; elt; elt=elt.parentElement) {
if (elt.id === containerId) {
hasFocus = true;
break;
}
}
dbg && console.log(msg, '[1] =>', hasFocus);
return hasFocus;
}
open(value=true) {
const msg = 'ebt-card.open()';
const dbg = DBG_OPEN_CARD;
let { isOpen, debugString, } = this;
if (isOpen === value) {
dbg && console.log(msg, `[1]isOpen`, debugString);
return false;
}
dbg && console.log(msg, `[2]isOpen<=${value}`, debugString);
this.isOpen = value;
return true;
}
onAfterMounted({settings, volatile}) {
const msg = "ebt-card.onAfterMounted()";
const dbg = DBG.ROUTE || DBG.MOUNTED;
let { langTrans, } = settings;
let { id } = this;
let route = window.location.hash.split('#')[1] || '';
if (this.matchPath({path:route, defaultLang:langTrans})) {
let aeId = document?.activeElement?.id;
if (volatile.routeCard?.id !== id) {
dbg && console.log(msg, `[1]setRouteCard`, this.debugString);
volatile.setRouteCard(this);
}
dbg && console.log(msg, `[2]focusCardElementId ${id}`,
{route, aeId});
volatile.focusCardElementId(this, route);
}
}
routeHash(dstPath) {
const msg = "EbtCard.routeHash";
const dbg = DBG.ROUTE_HASH;
let { context, location } = this;
let hash;
switch (context) {
case CONTEXT_SEARCH:{
hash = location.reduce((a,v) => {
return `${a}/${encodeURIComponent(v)}`;
}, `#/${context}`);
dbg && console.log(msg, '[1]search', hash);
} break;
case CONTEXT_SUTTA: {
if (dstPath) {
let [
ignored, ctx, suttaSeg, lang, author
] = dstPath.split('/');
location[0] = suttaSeg;
}
let [ suttaSeg, lang, author ] = location;
// NOTE: See segmentElementId()
hash = `#/sutta/${suttaSeg}/${lang}/${author}`;
dbg && console.log(msg, '[2]sutta', hash);
} break;
case CONTEXT_PLAY: {
if (dstPath) {
let [
ignored, ctx, suttaSeg, lang, author
] = dstPath.split('/');
location[0] = suttaSeg;
}
let [ suttaSeg, lang, author, pattern ] = location;
// NOTE: See segmentElementId()
hash = `#/play/${suttaSeg}/${lang}/${author}/${pattern}`;
dbg && console.log(msg, '[3]play', hash);
} break;
default: {
hash = location.reduce((a,v) => {
return `${a}/${encodeURIComponent(v)}`;
}, `#/${context}`);
dbg && console.log(msg, '[4]other', hash);
return hash;
}
}
return hash;
}
chipTitle($t=((k)=>k)) {
let { location, context } = this;
if (location.length) {
switch (context) {
case CONTEXT_PLAY: {
let { playlist={} } = this;
let { index, length } = playlist;
let position = `${index+1}/${length}`;
return `${location[0]} ${position}`;
}
case CONTEXT_SEARCH:
return location[0];
default:
return location.join('/');
}
}
return $t(`ebt.no-location-${context}`);
}
matchPathSutta({opts, context, location, cardLocation, }) {
const msg = "ebt-card.matchPathSutta()";
const dbg = DBG.ROUTE && DBG.VERBOSE;
let { path, defaultLang } = opts;
let loc = location.join('/');
let cardLoc = cardLocation.join('/');
if (loc === '') {
let result = cardLoc === loc;
dbg && console.log(msg, `[1]true ${path} => ${result}`,
{cardLoc, loc});
return result;
}
if (cardLoc === '') {
dbg && console.log(msg, `[2]false ${path}`, {cardLoc, loc});
return false;
}
let msStart = Date.now();
let pathRef = SuttaRef.create(loc, defaultLang);
if (pathRef == null) {
dbg && console.log(msg, `[3]false (${path})`, {loc});
return false;
}
let cardRef = SuttaRef.create(cardLoc, defaultLang);
if (pathRef.sutta_uid !== cardRef.sutta_uid) {
dbg && console.log(msg, `[4]false (${path})`,
pathRef.suid, cardRef.suid);
return false;
}
if (pathRef.lang && pathRef.lang !== cardRef.lang) {
dbg && console.log(msg, `[5]false (${path}, ${defaultLang})`,
pathRef.lang, cardRef.lang);
return false;
}
if (pathRef.author && pathRef.author !== cardRef.author) {
dbg && console.log(msg, `[6]false (${path})`,
pathRef.author, cardRef.author);
return false;
}
dbg && console.log(`[7]match(${path})`,
pathRef.toString(), '~=', cardRef.toString());
return true;
}
matchPath(strOrObj) {
const msg = 'ebt-card.matchPath() ';
const dbg = DBG.CARD_PATH;
const dbgv = DBG.VERBOSE && dbg;
let opts = typeof strOrObj === 'string'
? { path: strOrObj }
: strOrObj;
let { path } = opts;
let { id } = this;
path = path.toLowerCase().replace(/^#/, '');
let [ blank, context="", ...location ] = path.split('/');
if (blank !== '') {
dbg && console.log(msg, `[1]F initial "/"`, path, {blank});
return false;
}
while (location.length && location[location.length-1] === '') {
location.pop();
}
context = context && context.toLowerCase();
if (context === this.context && context===CONTEXT_WIKI) {
// all wiki locations are owned by home card singleton
dbg && console.log(msg, '[2]T CONTEXT_WIKI', id, strOrObj);
return true;
}
location = location
? location.map(loc => loc && decodeURIComponent(loc.toLowerCase()))
: [];
let cardLocation = this.location instanceof Array
? this.location
: (this.location == null ? [] : [this.location]);
if (context !== this.context) {
dbgv && console.log(msg, `[3]F context`, id, path, this.context);
return false;
}
if (this.isSuttaCard) {
let matchArgs = {opts, context, location, cardLocation};
if ( this.matchPathSutta(matchArgs)) {
dbg && console.log(msg, `[6]T sutta`, id, path);
return true;
}
dbg && console.log(msg, `[7]F sutta`, id, path);
return false;
}
if (location.length !== cardLocation.length) {
if (context === CONTEXT_SEARCH) {
if (location.length === 0) {
location.push('');
}
if (cardLocation[0].toLowerCase() === location[0] &&
location.length<2)
{
dbg && console.log(msg, '[8]T location', id, path);
return true; // empty search path without langTrans
}
}
dbg && console.log(msg, [
'[8]F location',
id,
path,
`location:${JSON.stringify(location)}`,
`!=`,
`cardLocation:${JSON.stringify(cardLocation)}`].join(' '));
return false;
}
let match = location.reduce((a,v,i) => {
let vDecoded = decodeURIComponent(v.toLowerCase());
let match = a && (vDecoded === cardLocation[i].toLowerCase());
if (!match) {
dbgv && console.log(msg, `[9]F location`, id, path, location,
cardLocation);
return false;
}
dbgv && console.log(msg, `[10]T location`, id, path, location);
return match;
}, true);
dbg && console.log(msg, `[11]${match?'T':'F'} location`,
id, path, location);
return match;
}
nextLocation({segments, delta=1}) {
let { context } = this;
let [...location] = this.location;
if (this.isSuttaCard) {
let [ scid, lang, author ] = location;
let iSeg = segments.findIndex(seg=>seg.scid === scid);
if (iSeg < 0) {
iSeg = 0;
}
let iSegNext = iSeg + delta;
if (iSeg<0 || iSegNext<0 || segments.length<=iSegNext) {
logger.debug("next segment out of bounds",
{iSeg, iSegNext, delta});
return null;
}
location[0] = segments[iSegNext].scid;
return {
location,
iSegment: iSegNext,
};
}
}
incrementLocation({segments, delta=1}) {
let { context } = this;
let result = this.nextLocation({segments, delta});
if (result) {
let { location:nextLocation, iSegment } = result;
if (this.location.join('/') !== nextLocation.join('/')) {
this.location = nextLocation;
}
}
return result;
}
setLocation({segments, delta=0}) {
const msg = 'ebt-card.setLocation() ';
let { context } = this;
let [...newLocation] = this.location;
let result = null;
if (this.isSuttaCard) {
if (segments.length <= 0) {
//logger.info(msg, "no segments");
return result;
}
let iSegNext = delta >= 0 ? delta : segments.length+delta;
newLocation[0] = segments[iSegNext].scid;
if (this.location.join('/') !== newLocation.join('/')) {
this.location = newLocation;
result = {
location: newLocation,
iSegment: iSegNext,
};
}
}
return result;
}
segGroup(scid) {
let segnum = scid.split(':')[1];
return segnum.split('.')[0];
}
groupStartIndex({segments=[], iSegCur=0}) {
const msg = 'ebt-card.groupStartIndex() ';
let { context, } = this;
if (this.isSuttaCard) {
if (segments.length <= 0) {
return 0;
}
} else {
return 0;
}
let scid = segments[iSegCur].scid;
let curGroup = this.segGroup(scid);
iSegCur = iSegCur < 0 ? 0 : iSegCur;
let iSegPrev = iSegCur;
let iSegNext = Math.min(segments.length-1, Math.max(0, iSegPrev-1));
let nextScid = segments[iSegNext].scid;
let nextGroup = this.segGroup(nextScid);
while (iSegPrev !== iSegNext && curGroup === nextGroup) {
iSegPrev = iSegNext;
iSegNext = Math.min(segments.length-1, Math.max(0, iSegPrev-1));
nextScid = segments[iSegNext].scid;
nextGroup = this.segGroup(nextScid);
}
return iSegPrev;
}
incrementGroup({segments=[], delta=1}) {
const msg = 'ebt-card.incrementGroup() ';
let result = null;
let { context } = this;
let [...location] = this.location;
if (this.isSuttaCard) {
if (segments.length <= 0) {
return result;
}
} else {
return result;
}
let scid = this.location[0];
let curGroup = this.segGroup(scid);
let iSegCur = segments.findIndex(seg=>seg.scid === scid);
iSegCur = iSegCur < 0 ? 0 : iSegCur;
let iSegPrev = iSegCur;
let iSegNext = Math.min(segments.length-1,
Math.max(0, iSegPrev+delta));
let iSegment = iSegNext;
if (delta < 0) {
iSegment = this.groupStartIndex({segments, iSegCur});
if (iSegment === iSegCur) {
iSegment = this.groupStartIndex({segments, iSegCur: iSegNext});
}
if (iSegment !== iSegCur) {
location[0] = segments[iSegment].scid;
result = { location, iSegment }
}
}
while (result == null && iSegPrev !== iSegment) {
let nextScid = segments[iSegment].scid;
let nextGroup = this.segGroup(nextScid);
if (nextGroup !== curGroup) {
location[0] = nextScid;
result = { location, iSegment };
break;
}
iSegPrev = iSegment;
iSegment = Math.min(segments.length-1, Math.max(0, iSegPrev+delta));
}
if (result) {
this.location = result.location;
}
return result;
}
scidToSCUrl(scid, scEndpoint="https://suttacentral.net") {
const msg = 'EbtCard.scidToSCUrl()';
let { id, context, location } = this;
if (!this.isSuttaCard) {
let emsg = `${msg} cannot be called for context:${context}`;
throw new Error(emsg);
}
let sref = SuttaRef.create(scid);
let { sutta_uid, segnum } = sref;
let [ defaultScid, lang, author ] = location;
if (author === 'ebt-deepl') {
return `${scEndpoint}/${sutta_uid}`;
}
let hash = segnum ? `#${segnum}` : '';
let sla = `${sutta_uid}/${lang}/${author}`;
return `https://suttacentral.net/${sla}${hash}`;
}
scidToDocUrl(scid) {
const msg = 'EbtCard.scidToDocUrl()';
let { id, context, location } = this;
if (!this.isSuttaCard) {
let emsg = `${msg} cannot be called for context:${context}`;
throw new Error(emsg);
}
let [ defaultScid, lang, author ] = location;
scid = scid || defaultScid;
let sref = SuttaRef.create(`${scid}/${lang}/${author}`);
let { sutta_uid } = sref;
let { origin } = window.location;
let apiEndpoint = `${origin}/#/sutta`;
return `${apiEndpoint}/${sutta_uid}/${lang}/${author}`;
}
scidToApiUrl(scid, apiEndpoint=API_ENDPOINT) {
const msg = 'EbtCard.scidToApiUrl()';
let { id, context, location } = this;
if (!this.isSuttaCard) {
let emsg = `${msg} cannot be called for context:${context}`;
throw new Error(emsg);
}
let [ defaultScid, lang, author ] = location;
scid = scid || defaultScid;
return `${apiEndpoint}/${scid}/${lang}/${author}`;
}
segmentElementId(scid) {
let { id, context, location } = this;
if (!this.isSuttaCard) {
scid = scid || 'no-segment';
return `${id}:${scid}`;
}
scid = scid || location[0];
let [ ignore, lang, author ] = location;
// NOTE: routeHash() and segmentElementId() must differ
// to prevent the browser from auto-navigating
// to segmentElementId's when the route changes
return `seg-${scid}/${lang}/${author}`;
}
segmentCardId(scid) {
let { id } = this;
let rawId = `${scid}_CARD${id.substring(0,8)}`;
return rawId.replaceAll('.',"_").replaceAll(/:/g,"__");
}
/* HACK:
* The viewport element is obscurable by the app bar
* and is above the viewed element by the height of the app bar.
* Therefore the viewed element will always be viewable if the
* viewport element is within the top half of the viewport.
* The focus element may or may not be the viewed element
*/
viewportElement(focusElt) {
const msg = 'ebt-card.viewportElement';
const dbg = DBG_VIEWPORT;
let focusId = focusElt?.id;
let viewportId = focusId;
let {
autofocusId, context, topAnchor, tab1Id, deleteId, location
} = this;
if (focusId === tab1Id) {
viewportId = topAnchor;
} else if (focusId === deleteId) {
viewportId = deleteId;
} else if (focusId === autofocusId) {
if (this.isSuttaCard) {
let [ scid, lang, author ] = location;
viewportId = this.segmentElementId(scid);
}
}
let viewportElt = document.getElementById(viewportId) || focusElt;
dbg && console.log(msg, '[1]', viewportElt?.id);
return viewportElt;
}
}