Skip to content

Commit

Permalink
Select Scripture when you click a check result
Browse files Browse the repository at this point in the history
  • Loading branch information
tjcouch-sil committed Nov 26, 2024
1 parent 830cca4 commit 4bb2b0b
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 27 deletions.
186 changes: 179 additions & 7 deletions extensions/src/platform-scripture-editor/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import papi, { logger } from '@papi/backend';
import papi, { logger, WebViewFactory } from '@papi/backend';
import type {
ExecutionActivationContext,
GetWebViewOptions,
IWebViewProvider,
SavedWebViewDefinition,
WebViewDefinition,
} from '@papi/core';
import { formatReplacementString, LanguageStrings } from 'platform-bible-utils';
import {
formatReplacementString,
LanguageStrings,
serialize,
UsjReaderWriter,
} from 'platform-bible-utils';
import {
EditorWebViewMessage,
PlatformScriptureEditorWebViewController,
} from 'platform-scripture-editor';
import { Canon, VerseRef } from '@sillsdev/scripture';
import platformScriptureEditorWebView from './platform-scripture-editor.web-view?inline';
import platformScriptureEditorWebViewStyles from './platform-scripture-editor.web-view.scss?inline';

Expand Down Expand Up @@ -100,14 +110,18 @@ async function open(
};
// REVIEW: If an editor is already open for the selected project, we open another.
// This matches the current behavior in P9, though it might not be what we want long-term.
return papi.webViews.getWebView(scriptureEditorWebViewType, undefined, options);
return papi.webViews.openWebView(scriptureEditorWebViewType, undefined, options);
}
return undefined;
}

/** Simple web view provider that provides Resource web views when papi requests them */
const scriptureEditorWebViewProvider: IWebViewProvider = {
async getWebView(
class ScriptureEditorWebViewFactory extends WebViewFactory<typeof scriptureEditorWebViewType> {
constructor() {
super(scriptureEditorWebViewType);
}

override async getWebViewDefinition(
savedWebView: SavedWebViewDefinition,
getWebViewOptions: PlatformScriptureEditorOptions,
): Promise<WebViewDefinition | undefined> {
Expand Down Expand Up @@ -145,8 +159,166 @@ const scriptureEditorWebViewProvider: IWebViewProvider = {
},
projectId,
};
},
};
}

override async createWebViewController(
webViewDefinition: WebViewDefinition,
webViewNonce: string,
): Promise<PlatformScriptureEditorWebViewController> {
let currentWebViewDefinition: SavedWebViewDefinition = webViewDefinition;
const unsubFromWebViewUpdates = papi.webViews.onDidUpdateWebView(({ webView }) => {
if (webView.id === currentWebViewDefinition.id) currentWebViewDefinition = webView;
});
return {
async selectRange(range) {
try {
logger.debug(
`Platform Scripture Editor Web View Controller ${currentWebViewDefinition.id} received request to selectRange ${serialize(range)}`,
);
if (!currentWebViewDefinition.projectId)
throw new Error(`webViewDefinition.projectId is empty!`);

let targetScrRef = { bookNum: 0, chapterNum: 0, verseNum: 0 };

// Figure out the book and chapter
if ('jsonPath' in range.start && 'jsonPath' in range.end) {
// Use the chapter and verse number from the range
if (
range.start.bookNum !== range.end.bookNum ||
range.start.chapterNum !== range.end.chapterNum
) {
throw new Error(
'Could not get targetScrRef from jsonPaths! Selection range cannot (yet) span chapters or books',
);
}

targetScrRef.bookNum = range.start.bookNum;
targetScrRef.chapterNum = range.start.chapterNum;
} else {
// At least one range location is USFM specification. Will convert to USJ for jsonPath
if (
'scrRef' in range.start &&
'scrRef' in range.end &&
(range.start.scrRef.bookNum !== range.end.scrRef.bookNum ||
range.start.scrRef.chapterNum !== range.end.scrRef.chapterNum)
) {
throw new Error(
'Could not get targetScrRef from scrRefs! Selection range cannot (yet) span chapters or books',
);
}

// Establish the book and chapter we're working with by what the range says
if ('scrRef' in range.start) {
targetScrRef = range.start.scrRef;
} else if ('scrRef' in range.end) {
targetScrRef = range.end.scrRef;
} else
throw new Error('Could not determine target scrRef to convert scrRef to jsonPath');
}

// Get the USJ chapter we're on so we can determine some things
const pdp = await papi.projectDataProviders.get(
'platformScripture.USJ_Chapter',
currentWebViewDefinition.projectId,
);
const usjChapter = await pdp.getChapterUSJ(
new VerseRef(targetScrRef.bookNum, targetScrRef.chapterNum, targetScrRef.verseNum),
);

if (!usjChapter)
throw new Error(
`USJ Chapter for project id ${currentWebViewDefinition.projectId} target scrRef ${serialize(targetScrRef)} is undefined!`,
);

const usjRW = new UsjReaderWriter(usjChapter);

// Convert the range now - easy conversion if already jsonPath, but need to run conversion
// if in USFM verse ref
let startJsonPath: string;
let endJsonPath: string;
// Assume offset is 0 if not provided
let startOffset = 0;
let endOffset = 0;

if ('scrRef' in range.start) {
const startContentLocation = usjRW.verseRefToUsjContentLocation(
new VerseRef(
range.start.scrRef.bookNum,
range.start.scrRef.chapterNum,
range.start.scrRef.verseNum,
),
range.start.offset,
);
startJsonPath = startContentLocation.jsonPath;
startOffset = startContentLocation.offset;
} else {
startJsonPath = range.start.jsonPath;
if (range.start.offset !== undefined) startOffset = range.start.offset;
}

if ('scrRef' in range.end) {
const endContentLocation = usjRW.verseRefToUsjContentLocation(
new VerseRef(
range.end.scrRef.bookNum,
range.end.scrRef.chapterNum,
range.end.scrRef.verseNum,
),
range.end.offset,
);
endJsonPath = endContentLocation.jsonPath;
endOffset = endContentLocation.offset;

if (endOffset < (range.end.offset ?? 0) - 50) {
logger.warn(
`Platform Scripture Editor Web View Controller ${currentWebViewDefinition.id} converted range to jsonPath, and calculated endOffset ${endOffset} was over 50 less than the original ${range.end.offset ?? 0}! Setting end position to start position`,
);
endJsonPath = startJsonPath;
endOffset = startOffset + 1;
}
} else {
endJsonPath = range.end.jsonPath;
if (range.end.offset !== undefined) endOffset = range.end.offset;
else if (range.start.offset !== undefined) endOffset = range.start.offset;
}

const convertedRange = {
start: { jsonPath: startJsonPath, offset: startOffset },
end: { jsonPath: endJsonPath, offset: endOffset },
};

// Figure out which verse we're on using the jsonPath
// Note: we could just use the verse if we receive a scrRef in the range, but our
// verseRefToUsjContentLocation doesn't always get the conversion right. So might as well
// use whatever verse it ends up on
const targetScrRefFromJsonPath = usjRW.jsonPathToVerseRefAndOffset(
convertedRange.start.jsonPath,
Canon.bookNumberToId(targetScrRef.bookNum),
);
targetScrRef.verseNum = targetScrRefFromJsonPath.verseRef.verseNum;

const message: EditorWebViewMessage = {
method: 'selectRange',
scrRef: targetScrRef,
range: convertedRange,
};
await papi.webViewProviders.postMessageToWebView(
currentWebViewDefinition.id,
webViewNonce,
message,
);
} catch (e) {
const message = `Platform Scripture Editor Web View Controller ${currentWebViewDefinition.id} threw while running selectRange! ${e}`;
logger.warn(message);
throw new Error(message);
}
},
async dispose() {
return unsubFromWebViewUpdates();
},
};
}
}
const scriptureEditorWebViewProvider: IWebViewProvider = new ScriptureEditorWebViewFactory();

export async function activate(context: ExecutionActivationContext): Promise<void> {
logger.info('Scripture editor is activating!');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ import { JSX, useCallback, useEffect, useMemo, useRef } from 'react';
import type { WebViewProps } from '@papi/core';
import { logger } from '@papi/frontend';
import { useProjectData, useProjectSetting, useSetting } from '@papi/frontend/react';
import { deepClone, ScriptureReference, UsjReaderWriter } from 'platform-bible-utils';
import {
compareScrRefs,
deepClone,
ScriptureReference,
serialize,
UsjReaderWriter,
} from 'platform-bible-utils';
import { Button } from 'platform-bible-react';
import { LegacyComment } from 'legacy-comment-manager';
import { EditorWebViewMessage, SelectionRange } from 'platform-scripture-editor';
import {
convertEditorCommentsToLegacyComments,
convertLegacyCommentsToEditorThreads,
Expand Down Expand Up @@ -83,7 +90,43 @@ globalThis.webViewComponent = function PlatformScriptureEditor({
// Using react's ref api which uses null, so we must use null
// eslint-disable-next-line no-null/no-null
const editorRef = useRef<EditorRef | MarginalRef | null>(null);
const [scrRef, setScrRefInternal] = useWebViewScrollGroupScrRef();
const [scrRef, setScrRefWithScroll] = useWebViewScrollGroupScrRef();

const nextSelectionRange = useRef<SelectionRange | undefined>(undefined);

// listen to messages from the web view controller
useEffect(() => {
const webViewMessageListener = ({
data: { method, scrRef: targetScrRef, range },
}: MessageEvent<EditorWebViewMessage>) => {
switch (method) {
case 'selectRange':
logger.debug(`selectRange targetScrRef ${serialize(targetScrRef)} ${serialize(range)}`);

if (compareScrRefs(scrRef, targetScrRef) !== 0) {
// Need to update scr ref, let the editor load the Scripture text at the new scrRef,
// and scroll to the new scrRef before setting the range. Set the nextSelectionRange
// which will set the range after a short wait time in a `useEffect` below
setScrRefWithScroll(targetScrRef);
nextSelectionRange.current = range;
}
// We're on the right scr ref. Go ahead and set the selection
else editorRef.current?.setSelection(range);

break;
default:
// Unknown method name
logger.debug(`Received event with unknown method ${method}`);
break;
}
};

window.addEventListener('message', webViewMessageListener);

return () => {
window.removeEventListener('message', webViewMessageListener);
};
}, [scrRef, setScrRefWithScroll]);

const [commentsEnabled] = useSetting('platform.commentsEnabled', false);

Expand All @@ -93,12 +136,12 @@ globalThis.webViewComponent = function PlatformScriptureEditor({
*/
const internallySetScrRefRef = useRef<ScriptureReference | undefined>(undefined);

const setScrRef = useCallback(
const setScrRefNoScroll = useCallback(
(newScrRef: ScriptureReference) => {
internallySetScrRefRef.current = newScrRef;
return setScrRefInternal(newScrRef);
return setScrRefWithScroll(newScrRef);
},
[setScrRefInternal],
[setScrRefWithScroll],
);

/**
Expand Down Expand Up @@ -273,7 +316,7 @@ globalThis.webViewComponent = function PlatformScriptureEditor({
}
}, [usjFromPdp, scrRef]);

// Scroll the selected verse into view
// Scroll the selected verse and selection range into view
useEffect(() => {
// If we made this latest scrRef change, don't scroll
if (
Expand All @@ -288,6 +331,11 @@ globalThis.webViewComponent = function PlatformScriptureEditor({

let highlightedVerseElement: HTMLElement | undefined;

// Queue up the next selection range to be set and clear it so we don't accidentally set the
// range to the wrong thing
const nextRange = nextSelectionRange.current;
nextSelectionRange.current = undefined;

// Wait before scrolling to make sure there is time for the editor to load
// TODO: hook into the editor and detect when it has loaded somehow
const scrollTimeout = setTimeout(() => {
Expand All @@ -296,6 +344,9 @@ globalThis.webViewComponent = function PlatformScriptureEditor({
highlightedVerseElement?.classList.add('highlighted');

internallySetScrRefRef.current = undefined;

// Set the selection if the selection was set to something as part of this scr ref change
if (nextRange) editorRef.current?.setSelection(nextRange);
}, EDITOR_LOAD_DELAY_TIME);

return () => {
Expand Down Expand Up @@ -327,7 +378,7 @@ globalThis.webViewComponent = function PlatformScriptureEditor({
<Editorial
ref={editorRef}
scrRef={scrRef}
onScrRefChange={setScrRef}
onScrRefChange={setScrRefNoScroll}
options={options}
logger={logger}
/>
Expand All @@ -342,7 +393,7 @@ globalThis.webViewComponent = function PlatformScriptureEditor({
<Marginal
ref={editorRef}
scrRef={scrRef}
onScrRefChange={setScrRef}
onScrRefChange={setScrRefNoScroll}
onUsjChange={onUsjAndCommentsChange}
onCommentChange={saveCommentsToPdp}
options={options}
Expand All @@ -358,7 +409,7 @@ globalThis.webViewComponent = function PlatformScriptureEditor({
<Editorial
ref={editorRef}
scrRef={scrRef}
onScrRefChange={setScrRef}
onScrRefChange={setScrRefNoScroll}
onUsjChange={saveUsjToPdp}
options={options}
logger={logger}
Expand Down
Loading

0 comments on commit 4bb2b0b

Please sign in to comment.