Commit 38b13e87 authored by Robert Knight's avatar Robert Knight

Allow host page to signal to client when content is ready to anchor

Allow the host page to pass a `contentReady` promise to the client, which it
will resolve once the content is rendered and the client should be able to
anchor annotations. This enables the host page to load the client concurrently
with fetching document content, only blocking the client at the point where it
needs to anchor annotations.

Fixes https://github.com/hypothesis/client/issues/5568
parent 963a5b15
...@@ -39,6 +39,7 @@ function configurationKeys(context: Context): string[] { ...@@ -39,6 +39,7 @@ function configurationKeys(context: Context): string[] {
annotator: [ annotator: [
'clientUrl', 'clientUrl',
'contentInfoBanner', 'contentInfoBanner',
'contentReady',
'subFrameIdentifier', 'subFrameIdentifier',
'sideBySide', 'sideBySide',
], ],
...@@ -120,6 +121,11 @@ const configDefinitions: ConfigDefinitionMap = { ...@@ -120,6 +121,11 @@ const configDefinitions: ConfigDefinitionMap = {
defaultValue: null, defaultValue: null,
getValue: getHostPageSetting, getValue: getHostPageSetting,
}, },
contentReady: {
allowInBrowserExt: true,
defaultValue: null,
getValue: getHostPageSetting,
},
enableExperimentalNewNoteButton: { enableExperimentalNewNoteButton: {
allowInBrowserExt: false, allowInBrowserExt: false,
defaultValue: null, defaultValue: null,
......
...@@ -81,6 +81,7 @@ describe('annotator/config/index', () => { ...@@ -81,6 +81,7 @@ describe('annotator/config/index', () => {
bucketContainerSelector: null, bucketContainerSelector: null,
clientUrl: 'fakeValue', clientUrl: 'fakeValue',
contentInfoBanner: null, contentInfoBanner: null,
contentReady: 'fakeValue',
enableExperimentalNewNoteButton: null, enableExperimentalNewNoteButton: null,
externalContainerSelector: null, externalContainerSelector: null,
focus: null, focus: null,
...@@ -116,6 +117,7 @@ describe('annotator/config/index', () => { ...@@ -116,6 +117,7 @@ describe('annotator/config/index', () => {
bucketContainerSelector: 'fakeValue', bucketContainerSelector: 'fakeValue',
clientUrl: 'fakeValue', clientUrl: 'fakeValue',
contentInfoBanner: 'fakeValue', contentInfoBanner: 'fakeValue',
contentReady: 'fakeValue',
enableExperimentalNewNoteButton: 'fakeValue', enableExperimentalNewNoteButton: 'fakeValue',
externalContainerSelector: 'fakeValue', externalContainerSelector: 'fakeValue',
focus: 'fakeValue', focus: 'fakeValue',
...@@ -175,6 +177,7 @@ describe('annotator/config/index', () => { ...@@ -175,6 +177,7 @@ describe('annotator/config/index', () => {
bucketContainerSelector: null, bucketContainerSelector: null,
clientUrl: null, clientUrl: null,
contentInfoBanner: null, contentInfoBanner: null,
contentReady: null,
enableExperimentalNewNoteButton: null, enableExperimentalNewNoteButton: null,
externalContainerSelector: null, externalContainerSelector: null,
focus: null, focus: null,
...@@ -233,6 +236,7 @@ describe('annotator/config/index', () => { ...@@ -233,6 +236,7 @@ describe('annotator/config/index', () => {
expectedKeys: [ expectedKeys: [
'clientUrl', 'clientUrl',
'contentInfoBanner', 'contentInfoBanner',
'contentReady',
'subFrameIdentifier', 'subFrameIdentifier',
'sideBySide', 'sideBySide',
], ],
......
...@@ -105,6 +105,12 @@ export type GuestConfig = { ...@@ -105,6 +105,12 @@ export type GuestConfig = {
/** Configures a banner or other indicators showing where the content has come from. */ /** Configures a banner or other indicators showing where the content has come from. */
contentInfoBanner?: ContentInfoConfig; contentInfoBanner?: ContentInfoConfig;
/**
* Promise that the guest should wait for before it attempts to anchor
* annotations.
*/
contentReady?: Promise<void>;
sideBySide?: SideBySideOptions; sideBySide?: SideBySideOptions;
}; };
...@@ -191,6 +197,12 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable { ...@@ -191,6 +197,12 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable {
/** Promise that resolves when feature flags are received from the sidebar. */ /** Promise that resolves when feature flags are received from the sidebar. */
private _featureFlagsReceived: Promise<void>; private _featureFlagsReceived: Promise<void>;
/**
* Promise that the guest will wait for before attempting to anchor
* annotations.
*/
private _contentReady?: Promise<void>;
private _adder: Adder; private _adder: Adder;
private _clusterToolbar?: HighlightClusterController; private _clusterToolbar?: HighlightClusterController;
private _hostFrame: Window; private _hostFrame: Window;
...@@ -252,6 +264,7 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable { ...@@ -252,6 +264,7 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable {
super(); super();
this.element = element; this.element = element;
this._contentReady = config.contentReady;
this._hostFrame = hostFrame; this._hostFrame = hostFrame;
this._highlightsVisible = false; this._highlightsVisible = false;
this._isAdderVisible = false; this._isAdderVisible = false;
...@@ -603,6 +616,11 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable { ...@@ -603,6 +616,11 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable {
* re-anchoring the annotation. * re-anchoring the annotation.
*/ */
async anchor(annotation: AnnotationData): Promise<Anchor[]> { async anchor(annotation: AnnotationData): Promise<Anchor[]> {
if (this._contentReady) {
await this._contentReady;
this._contentReady = undefined;
}
/** /**
* Resolve an annotation's selectors to a concrete range. * Resolve an annotation's selectors to a concrete range.
*/ */
......
...@@ -1336,6 +1336,27 @@ describe('Guest', () => { ...@@ -1336,6 +1336,27 @@ describe('Guest', () => {
assert.lengthOf(guest.anchors, 0); assert.lengthOf(guest.anchors, 0);
}); });
it('waits for content to be ready before anchoring', async () => {
const events = [];
fakeIntegration.anchor = async () => {
events.push('fakeIntegration.anchor');
return range;
};
const contentReady = delay(1).then(() => {
events.push('contentReady');
});
const guest = createGuest({ contentReady });
const annotation = {
$tag: 'tag1',
target: [{ selector: [{ type: 'TextQuoteSelector', exact: 'hello' }] }],
};
await guest.anchor(annotation);
assert.deepEqual(events, ['contentReady', 'fakeIntegration.anchor']);
});
}); });
describe('#detach', () => { describe('#detach', () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment