Last date modified: 2026-Jul-14
Content Highlighting Framework
This page contains the following information:
- Content Highlighting Framework
- Overview
- Render functions
- Highlighting
- Content highlighting cards
- Syncing text highlights across viewer types
Overview
The Content Highlighting Framework streamlines the workflow for building consistent, maintainable, and performant, highlighting features that enrich the document review experience with both text-based and coordinate-based highlights on document content.
Content highlighting features
Review extensions can leverage the Content Highlighting Framework to build content highlighting "features". A content highlighting feature configures the Review Interface to retrieve and render custom highlights on document content and, optionally, to display additional metadata about those highlights in a card next to the document content.
There are three main components of a content highlighting feature:
1. Feature configuration
The feature configuration defines basic details for the feature, such as its unique ID, the user-friendly name, which viewer types it supports, whether or not the feature is enabled by default, and whether or not the feature's highlights should be visible by default.
2. Render function
At the core of a content highlighting feature is the "render function." The render function is a JavaScript function defined as a property on the feature configuration that is responsible for:
- Retrieving any data necessary to perform highlighting on the current document
- This typically involves making an HTTP call to a REST API to retrieve data about what text or document coordinates to render a highlight at.
- Leveraging Review JavaScript APIs to create highlights on the document content
The Review Interface will invoke these render functions at the appropriate time, passing them the necessary parameters to render highlights.
Render functions are explored in more detail in the Render functions section below.
3. Content highlighting card (optional)
Content highlighting features have the option to define a custom highlighting card if they wish to augment document content highlights with additional metadata in the UI. The Content Highlighting Framework streamlines the ability to keep this card's content in sync with both the current document and the highlights rendered by the content highlighting feature. Care should be taken when designing these user interfaces to ensure a consistent user experience across content highlighting feature cards. See Cards for general information about Review Interface cards and Content highlighting cards below for information about developing content highlighting cards.
Getting Started
Review extensions can register one or more content highlighting features by defining the content highlighting feature configuration objects (IContentHighlightingFeatureConfig) in the contentHighlighting property of the extension configuration object (IExtensionConfig).
Here is an example of a basic content highlighting feature registration:
(function(params) {
const contentHighlightingFeatureConfig = {
id: "acme-highlighting-feature",
name: "ACME Highlighting Feature",
renderFn: async (params) => {
// If the viewer doesn't support the highlighting API, do nothing
if (!params.highlightingApi) {
return;
}
const highlightConfig = {
fillColor: "rgba(255,255,0,0.8)",
textColor: "rgba(0,0,0,1)",
};
const textSearchConfig = {
caseSensitive: false,
normalizeAccentsForSearch: true,
normalizeEmoji: false,
searchMode: "literal",
stemAll: false,
stemmer: "none",
};
// Highlight any occurrences of the term "needle"
params.highlightingApi.highlightTerm("needle", highlightConfig, textSearchConfig);
// Wait for highlighting to complete
await params.highlightingApi.waitForTermHighlightingToComplete();
},
};
return {
id: "acme.extension",
name: "ACME Extension",
contentHighlighting: [ contentHighlightingFeatureConfig ],
};
}(parameters));
TypeScript note:searchMode and stemmer are declare enum types (TextSearchMode and TextSearchStemmer). The reviewapi package has no JavaScript exports, so these enums cannot be imported as values. Use import type and cast the string values:
import type { TextSearchMode, TextSearchStemmer } from 'reviewapi';
const textSearchConfig = {
caseSensitive: false,
normalizeAccentsForSearch: true,
normalizeEmoji: false,
searchMode: 'literal' as unknown as TextSearchMode,
stemAll: false,
stemmer: 'none' as unknown as TextSearchStemmer,
};
Controlling feature availability
All content highlighting features are enabled by default, but the Content Highlighting Framework allows features to be registered as disabled by default and then enabled later, in response to some custom checks. This can be useful in the following scenarios:
- Controlling the release of a new content highlighting feature in development
- Limiting the availability of the feature to users with specific custom Relativity permissions
- Performing checks to ensure any necessary dependencies are available in the current workspace or environment
- Limiting the availability of the feature to specific customers
To implement this conditional enablement, the feature first needs to be configured to be disabled by default. This can be done by setting the enabled property to false on the content highlighting feature configuration object. This will ensure the extension is initially disabled.
Next, implement logic to perform the check. If the check determines that the feature should be enabled, the enableFeature() method on the Content Highlighting Service (IContentHighlightingService) can be used to enable the feature on-the-fly. Typically, the check will involve some sort of asynchronous network call. It is extremely important to implement these checks correctly so as to not slow down the parsing of the extension script, and as a result, the Review Interface as a whole. Typically, it is recommended to perform the check in the extension's ready lifecycle event handler.
Here is an example of this type of check:
(function(params) {
const FEATURE_ID = "acme-highlighting-feature";
const contentHighlightingFeatureConfig = {
id: FEATURE_ID,
name: "ACME Highlighting Feature",
enabled: false,
renderFn: async (params) => {
//...render logic here
},
};
return {
id: "acme.extension",
name: "ACME Extension",
contentHighlighting: [ contentHighlightingFeatureConfig ],
lifecycle: {
ready: async (api) => {
const shouldEnable = await checkFeatureFlagAndPermissions(api);
if (shouldEnable) {
api.contentHighlights.enableFeature(FEATURE_ID);
}
},
},
};
}(parameters));
Enabling features in specific viewer types
By default, content highlighting features are enabled in all registered viewer types, including the Relativity Translation viewer and third-party custom viewer types. Content highlighting features can define which specific viewer types are supported by specifying an array of supported viewer types in the supportedViewerTypes property on the content highlighting feature configuration:
{
id: "acme-highlighting-feature",
name: "ACME Highlighting Feature",
supportedViewerTypes: ["native", "text"],
renderFn: async (params) => {
// ...render logic here
},
}
Most content highlighting features enable support in all supported viewer types by leaving this property undefined in their feature configuration. For these content highlighting features, it is common for the feature to require syncing text highlights across multiple viewer types. See Syncing text highlights across viewer types for more information on how to manage and optimize this scenario.
NOTE: The Content Highlighting API is not available in most custom viewer types. See Determining if the highlighting API is available for more information about how to handle these scenarios.
Render functions
This section discusses developing render functions, including how to perform data retrieval and content highlighting.
Render function basics
At its core, a render function is an asynchronous JavaScript function that takes a IRenderContentHighlightsParameters object as a parameter. Any value returned from the function is ignored.
The Review Interface will initiate unique render function invocations for each viewer type that a given document is loaded into. These invocations can run in parallel with one another. The render function will not be invoked twice for a given document in a given viewer type unless either of the following occurs:
- The user navigates away from the document to another document in the review queue and back to the original document
- The content highlighting feature forces a re-render
- The document is force reloaded after failing to load on initial document open
Render functions are responsible for leveraging the provided Content Highlighting API, IContentHighlightingApi, to render content highlights on the current document. If necessary, render functions are responsible for retrieving any data that is needed in order to perform highlighting.
Render function data retrieval
Content highlighting features often need to retrieve some document-specific (or even user-specific) metadata in order to determine which document content should be highlighted. This section will explore how that data retrieval should be implemented in content highlighting feature render functions.
Making HTTP calls
The recommended approach for making HTTP calls from within render functions is to use the native fetch() API. Besides being available in all of Relativity's supported browsers, it has support for cancellation, via the AbortSignal interface.
The following example uses fetch to retrieve data from an HTTP endpoint:
const workspaceId = params.api.configuration.workspaceId;
const documentId = params.content?.item?.artifactId;
const url = `/acme/api/${workspaceId}/${params.documentId}/highlights`;
const response = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: {
["Content-Type"]: "application/json",
["X-CSRF-Header"]: "-",
},
signal: params.abortSignal,
});
const data = await response.json();
Cancellation
Often, users will navigate through documents in the review queue very quickly. In these circumstances, invocations of content highlighting render functions are canceled and any in-flight HTTP requests generated by that invocation of the render function should also be canceled. This will help to ensure that TCP connections, a limited resource in certain circumstances, will not be tied up retrieving data that will never be used.
The render function parameters object includes an AbortSignal in the abortSignal property. This should be passed to the fetch() call in the options parameter.
When the render function is canceled, this will also cancel the fetch() request, freeing up the TCP connection. Note that attempting to run the following code after the fetch() call has been aborted will throw an AbortError, which may need to be handled by the render function:
// The following will throw an `AbortError` if the fetch() call that returned `response` was aborted
const data = await response.json();
Determining what data should be retrieved
In general, render functions should retrieve the minimal amount of data required to perform highlighting, however there is some gray area when the content highlighting feature includes a content highlighting card. If the card associated with the feature requires some small amount of additional data for each highlight, then it is best practice to just retrieve this data as part of the same HTTP request for the highlight data. This information can then be provided to the card, through the Highlighting API. See Passing custom data from render functions to highlighting cards for more information. If the additional data can be very large, then it is best to make a separate HTTP request for the additional information from within the card itself.
For scenarios where the amount of data used to render highlights can grow to be very large, it is best to make a series of smaller HTTP requests in serial.
For example, if the render function needs to retrieve 1000 rows of terms to search and highlight within the document, the render function could implement the data retrieval and highlight rendering in batches to parallelize some of the data retrieval and text searching/highlighting:
const batchSize = 100;
let offset = 0;
let totalTermCount;
let termsRetrieved = 0;
do {
// Retrieve new page of results
const response = await getTermsFromApi(renderParams, offset, batchSize);
termsRetrieved += response.data.length;
totalTermCount = response.totalCount;
// Start searching and highlighting terms in document content
startHighlightingTerms(renderParams, response.data);
offset += batchSize;
} while(termsRetrieved < totalTermCount);
// Wait for all term highlighting to complete
await renderParams.highlightingApi.waitForTermHighlightingToComplete();
Render function highlighting
Once the render function has retrieved the necessary data to decide where to render highlights, it uses the Content Highlighting API, IContentHighlightingApi, instance provided in the highlightingApi property of the render parameters to render highlights on the document content.
The instance of the Content Highlighting API provided is unique to the current document content being displayed in the viewer type specified in params.content.viewerType and to the feature the render function belongs to.
The Content Highlighting API can be used to create both location-based and search-based highlights. For more information on how to use the Content Highlighting API, see Highlighting.
Determining if the highlighting API is available
There are a few scenarios when the Content Highlighting API may not be available to the render function. In these scenarios, the render function will still be invoked, but the highlightingApi property on the render function parameters will be undefined:
- The viewer type does not support the Content Highlighting API
- NOTE: The Content Highlighting API is available on all of Relativity's viewer types, including the Relativity Translation viewer, but it will not be available on most custom viewer types.
- The type of content being shown in the viewer is not a document.
- When the viewer opened a document it does not support, is showing an error, or when the review queue is empty, no document has been loaded and there is no Content Highlighting API to provide. Render functions can inspect the content type via
params.content.type.
- When the viewer opened a document it does not support, is showing an error, or when the review queue is empty, no document has been loaded and there is no Content Highlighting API to provide. Render functions can inspect the content type via
There may be scenarios where a render function should perform some actions when the Content Highlighting API is unavailable, but most features are designed to simply short-circuit the render function invocation in scenarios where the API is unavailable:
async function render(params) {
// Retrieve data here...
if (!params.highlightingApi) {
return;
}
// Highlighting logic here...
}
Note that in the above example, the API check is happening AFTER data retrieval. This is an optimization used by content highlighting features that are syncing highlights across viewer types. See Syncing text highlights across viewer types for more information on supporting multiple viewer types.
Highlighting
The Content Highlighting API, IContentHighlightingApi, can be used to create both location-based and search-based highlights on document content.
Location-based highlights are created at specific document locations based on the the highlight x and y coordinates, starting/ending character indexes (text-based documents), cell ranges (spreadsheet documents), line ranges (transcription text), or time ranges (short message documents).
Search-based highlights are text highlights that are created wherever provided search terms appear in the document content.
Highlight sets
Highlight sets, represented as HighlightSetMemento objects, define a collection of related highlights.
Creating highlight sets
Creating highlight sets is performed using different API methods, depending on whether the highlights within that set will be location-based or search-based.
When creating location-based highlights, the highlight set must be created before highlights can be created:
const highlightSetId = params.highlightingApi.createSet();
When creating search-based highlights, the highlight set is created when the first term search is initiated:
const highlightSetId = params.highlightingApi.highlightTerm("needle", {
// ...highlight config here
}, {
// ...search config here
});
Retrieving highlight sets
Retrieve a single highlight set using the serialize() method:
const highlightSet = params.highlightingApi.serialize(highlightSetId);
Retrieve all highlight sets as an array using the getSets() method:
const highlightSets = params.highlightingApi.getSets();
Removing highlight sets
Remove a highlight set using the removeHighlightSet() method:
params.highlightingApi.removeHighlightSet(highlightSetId);
Highlights
Highlights, represented as HighlightMemento objects, define a single highlight in the document content.
The range property of highlight objects describes the location of the highlight in the document content. This location can be represented by any of the following object types, depending on the document type:
Creating location-based highlights
Location-based highlights create highlights at specific locations in a document, defined by a "range" object. Different range objects can be used to define locations in different types of documents.
Location-based highlighting is powerful, but should be used with some caution. By default, content highlighting features are enabled across all available viewer types (i.e. Native, Text, Image, Production, PDF), including the Relativity Translation viewer and any installed third-party custom viewers. Location-based highlights created in one viewer type will rarely end up in the correct place in other viewer types, due to differences in document content rendering across different viewers. Typically, location-based highlighting is only used when highlights are restricted to a single viewer type.
Create a new location-based highlight using the addHighlight() method:
const highlightConfig = {
fillColor: "rgba(255,255,0,0.7)",
textColor: "#000000",
};
const pageIndex = 0;
const x1 = 500;
const x2 = 700;
const y1 = 10;
const y2 = 20;
const rectRange = params.viewer.createRectRange(pageIndex, x1, y1, x2, y2);
const coordinateBasedHighlight = await params.highlightingApi.addHighlight(highlightSetId, rectRange, highlightConfig);
const startingCharacterIndex = 19;
const endingCharacterIndex = 64;
const textRange = params.viewer.createTextRange(startingCharacterIndex, endingCharacterIndex);
const textLocationBasedHighlight = await params.highlightingApi.addHighlight(highlightSetId, textRange, highlightConfig);
const c1 = 4;
const c2 = 9;
const r1 = 0;
const r2 = 1;
const cellRange = params.viewer.createCellRange(pageIndex, c1, r1, c2, r2);
const spreadsheetLocationBasedHighlight = await params.highlightingApi.addHighlight(highlightSetId, cellRange, highlightConfig);
Create range objects with the createCellRange(), createRectRange(), and createTextRange() methods on the individual viewer APIs.
Note that the coordinate systems used for rect ranges in the Review Interface may differ from that of the original native files stored in the fileshare. This is an artifact of the processes that allow native documents to be rendered in web browsers. Depending on what source is used to generate the coordinates at which to create highlights, it may be necessary to scale the generated coordinates to match the coordinates in the Review Interface application running in the browser. To do that, it will be necessary to access the height and width of the page where highlights will be created. Retrieve the height and width of individual document content pages using the getPageHeight() and getPageWidth() methods on the individual viewer APIs:
const pageOneHeight = params.viewer.getPageHeight(0);
const pageSixWidth = params.viewer.getPageWidth(5);
Creating search-based highlights
Search-based highlighting uses term highlighting to search document text for occurrences of a search term and to create highlights for each occurrence. Search-based highlighting is ideal for scenarios where the exact location of the text to highlight is not known or when there is a need to perform text-based highlighting across viewer types.
Create a new term highlight search using the highlightTerm() method:
const searchTerm = "needle";
const highlightConfig = {
fillColor: "rgba(255,255,0,0.7)",
textColor: "#000000",
};
const searchConfig = {
caseSensitive: false,
normalizeAccentsForSearch: true,
normalizeEmoji: false,
searchMode: "literal",
stemAll: false,
stemmer: "none",
};
const highlightSetId = params.highlightingApi.highlightTerm(searchTerm, highlightConfig, searchConfig);
This will create the highlight set, initiate the first term search for the newly created highlight set, and return the highlight set ID.
See PartialHighlightConfig and HighlightConfig for more information about the available highlight config options. See TextSearchConfig for more information about the available search config options.
Initiate an additional term search in the same highlight set using the addTerm() method:
const searchTerm = "haystack";
await params.highlightingApi.addTerm(highlightSetId, searchTerm);
This will initiate an additional term search in the provided highlight set. The additional term search can run in parallel with any existing term searches. Note that the returned Promise will resolve before the term search is complete. See Waiting for searching to complete for information about how to wait for term highlighting to complete.
Sometimes, a search term may appear in multiple locations within document content. In cases where a single occurrence should be highlighted, use the highlightTermWithContext() and addTermWithContext() methods to highlight specific instances of the search term that appear in a larger context search string.
For example, imagine a document with the following text:
You're not looking for a needle in a haystack. You're looking for a needle in a pile of needles.
Use the following code to highlight only the first occurrence of the term needle:
const searchTerm = "needle";
const context = "a needle in a haystack";
const highlightConfig = {
fillColor: "rgba(255,255,0,0.7)",
textColor: "#000000",
};
const searchConfig = {
caseSensitive: false,
normalizeAccentsForSearch: true,
normalizeEmoji: false,
searchMode: "literal",
stemAll: false,
stemmer: "none",
};
const highlightSetId = params.highlightingApi.highlightTermWithContext(searchTerm, context, highlightConfig, searchConfig);
To access occurrences of the highlights created via term highlighting, see onhighlightadd.
The following is a complete example of how to use term highlighting:
// The first term search creates the highlight set and returns the ID
const highlightSetId = params.highlightingApi.highlightTerm("needle", {
fillColor: "rgba(255, 255, 0, 1)",
textColor: "#000000",
}, {
caseSensitive: false,
normalizeAccentsForSearch: true,
normalizeEmoji: false,
searchMode: "literal",
stemAll: false,
stemmer: "none",
});
// Additional terms can be added to an existing term highlight set
await params.highlightingApi.addTerm(highlightSetId, "haystack", {
caseSensitive: true,
normalizeAccentsForSearch: true,
normalizeEmoji: true,
});
// The Content Highlighting API raises events whenever term searches find hits and highlights are created
params.highlightingApi.onhighlightadd(event => {
console.log(`${event.highlights.length} highlight(s) added to highlight set ${event.highlightSet.id}`);
});
// Term searching and highlighting happens asynchronously. Render functions should wait for term highlighting to complete before returning if term highlighting was used during the render
await params.highlightingApi?.waitForTermHighlightingToComplete();
Retrieving highlights
Retrieve the highlights in a given highlight set as an array using the highlights property on the highlight set:
const highlights = highlightSet.highlights;
Retrieve all highlights from all highlight sets as an array using the getHighlights() method:
const highlights = params.highlightingApi.getHighlights();
Removing highlights
Remove a highlight from a specific highlight set using the removeHighlight() method:
const highlightId = highlight.id;
params.highlightingApi.removeHighlight(highlightSetId, highlight);
Waiting for searching to complete
Search-based term highlighting is performed in the background via a Web Worker.
Wait for term highlighting to complete using the `waitForTermHighlightingToComplete() method:
await params.highlightingApi.waitForTermHighlightingToComplete();
Check if term highlighting is in progress for a specific highlight set using the isTermHighlightingInProgress() method:
const highlightSetIsSearching = params.highlightingApi.isTermHighlightingInProgress(highlightSetId);
Check if term highlighting is in progress for any highlight set using the isTermHighlightingInProgress() method, without providing a highlight set ID:
const anyHighlightSetIsSearching = params.highlightingApi.isTermHighlightingInProgress();
Controlling highlight visibility
Highlight visibility can be controlled at the highlight or highlight set level. Setting the visibility of a highlight set, will override the visibility of all the highlights within the set.
To set the visibility of a highlight, use the setHighlightVisibility() or toggleHighlightVisibility() methods:
const highlightId = highlight.id;
params.highlightingApi.setHighlightVisibility(highlightSetId, highlightId, false);
params.highlightingApi.toggleHighlightVisibility(highlightSetId, highlightId);
To set the visibility of a highlight set, use the setHighlightSetVisibility() or toggleHighlightSetVisibility() methods:
params.highlightingApi.setHighlightSetVisibility(highlightSetId, false);
params.highlightingApi.toggleHighlightSetVisibility(highlightSetId);
To set the visibility of all highlight sets at once, use the setAllHighlightSetsVisibility() method:
params.highlightingApi.setAllHighlightSetsVisibility(false);
NOTE: Setting the visibility of a highlight set that belongs to a set group, will result in the visibility value being propagated to all highlights sets within that group. See 2. Use set groups to sync highlight visibility across viewer types for more information on using set groups.
The current implementations of the highlighting API visible property as well as the show(), hide(), setVisibility(), and toggleVisibility() methods are likely to change in the future.
Highlighting events
The IContentHighlightingApi emits a variety of events that content highlighting features can register handlers for when highlights and highlight sets are created, updated, and removed.
onsetadd
Listen for the creation of new highlight sets with the onsetadd() method:
params.highlightingApi.onsetadd(e => {
const highlightSet = e.highlightSet;
const isTermHighlightSet = e.isTermHighlightSet;
const searchTermIfAvailable = e.term;
const termContextIfAvailable = e.termContext;
});
onsetremove
Listen for the removal of highlight sets with the onsetremove() method:
params.highlightingApi.onsetremove(e => {
const removedHighlightSet = e.highlightSet;
});
onsetvisiblechange
Listen for highlight set visibility changes with the onsetvisiblechange() method:
params.highlightingApi.onsetvisiblechange(e => {
const highlightSet = e.highlightSet;
const isVisible = e.visible;
});
onsearch
Listen for the start of a new term search with the onsearch() method:
params.highlightingApi.onsearch(e => {
const highlightSet = e.highlightSet;
const term = e.term;
const isFirstTermSearchForHighlightSet = e.isFirstTerm;
const termContextIfAvailable = e.termContext;
});
onsearchcomplete
Listen for the completion of a term search with the onsearchcomplete() method:
params.highlightingApi.onsearchcomplete(e => {
const highlightSet = e.highlightSet;
});
onhighlightadd
Listen for the creation of a new highlight with the onhighlightadd() method. This event will be emitted when new location-based highlights are created and when term searches find instances
of the term for search-based highlighting:
params.highlightingApi.onhighlightadd(e => {
const highlightSet = e.highlightSet;
const arrayOfHighlightsCreated = e.highlights;
});
onhighlightremove
Listen for the removal of highlights with the onhighlightremove() method:
params.highlightingApi.onhighlightremove(e => {
const removedHighlightSet = e.highlightSet;
const arrayOfRemovedHighlights = e.highlights;
});
onhighlightmove
Listen for highlights being moved with the onhighlightmove() method:
params.highlightingApi.onhighlightmove(e => {
const highlightSet = e.highlightSet;
const movedHighlight = e.highlight;
const oldLocation = e.oldRange; // Note: the object exposed on this property will change in the future.
const newLocation = movedHighlight.range;
});
onhighlightactivechange
Highlights are "active" when they have been clicked or navigated to. The styling of active highlights in the UI changes to indicate active highlights to the user.
Listen for changes to the set of active highlights with the onhighlightactivechange() method:
params.highlightingApi.onhighlightactivechange(e => {
const arrayOfActiveHighlights = e.highlights;
});
onhighlightclick
Listen for click events on highlights with the onhighlightclick() method:
params.highlightingApi.onhighlightclick(e => {
const clickedHighlight = e.highlight;
const clickedHighlightIsActive = e.active;
});
Custom events
The IContentHighlightingApi can also be used to emit custom events. These events can then be listened for elsewhere in content highlight feature code, often times within a content highlighting card. This can be useful when passing information from the render method to the highlighting card in a way that does not map to one of the above events.
Emit custom events with the emit() method:
const customEvent = {
foo: "bar",
};
params.highlightingApi.emit("custom-event-name", customEvent);
Listen for custom events with the on() method:
params.highlightingApi.on("custom-event-name", e => {
const fooValue = e.foo;
});
Before using custom events, see if data can be tied directly to highlight sets or highlight objects and use one of the above events. See Passing custom data from render functions to highlighting cards for more information.
Accessing created highlight sets and highlights
Event listeners can be registered to listen for:
- Feature visibility change
- Highlight set add
- Highlight set remove
- Highlight set visibility change
- Highlight add
- Highlight remove
- Highlight move
- Term search start
- Term search complete
- Active highlight change
- Highlight click
Sometimes it can be helpful to retrieve the list of highlight sets and highlights that a feature has created via an instance of the Content Highlighting API.
The getSets() and getHighlights() methods can be used to retrieve highlight sets and highlights respectively.
Content highlighting cards
It is common for content highlighting features to include a custom UI in the left viewer dock for displaying the highlights created by the feature with some additional metadata for users. There are some unique challenges with building these types of UIs that the Content Highlighting Framework attempts to streamline.
The main benefit of using the Content Highlighting Framework to manage cards related to content highlights is that the card is given access to the Content Highlighting Card API (IContentHighlightingCardApi), which can be used to sync the card's content with the active document content and any rendered content highlights. See Accessing the Content Highlighting Card API for information on how to access the Content Highlighting Card API and see Using the Content Highlighting Card API for information on how it can be used to keep card content in sync.
The "late-open" problem
By default, Review Interface cards are not loaded into memory or added to the DOM until they are opened by the user for the first time. This poses a challenge when it comes to syncing the highlight data between the document content and the card, which is often running inside an iframe. The Content Highlighting API emits a series of events when things like highlight set creation, highlight creation, and highlight updates occur, but if these events are emitted before the card is loaded, then the card will "miss" these events. This is referred to as the "late-open" problem and the sections below will explain how to overcome it and keep the feature's content highlighting card in sync with the highlights on document content.
Defining the card
Content highlighting feature cards are defined the same way as standard cards. Extensions add the card configuration object (ICardConfig) to the cards array on the returned extension configuration object. This shows the Card Framework how to render the card. Next, the content highlighting feature needs to be updated with the ID of the card. The Content Highlighting Framework will take care of creating an instance of the card in the appropriate location at the appropriate time.
(function(params) {
const CARD_ID = "acme-highlighting-card";
const FEATURE_NAME = "ACME Highlighting Feature";
const contentHighlightingFeatureConfig = {
id: "acme-highlighting-feature",
name: FEATURE_NAME,
renderFn: async (params) => {
// Render logic here...
},
cardId: CARD_ID,
};
const cardConfig = {
id: CARD_ID,
title: FEATURE_NAME,
icon: { svgIcon: "your-rwc-icon-name" }, // required — card appears in a dock, which displays the icon on the tab
// Additional card config here...
};
return {
id: "acme.extension",
name: "ACME Extension",
contentHighlighting: [ contentHighlightingFeatureConfig ],
cards: [ cardConfig ],
};
}(parameters));
There are some special considerations to keep in mind when creating the card configuration:
- The
locationproperty will be ignored. The Content Highlighting Framework always places content highlighting cards in the left dock — this is not configurable. - The
iconproperty is required. Because content highlighting cards are always placed in a dock, the icon is what appears on the dock tab. Set exactly one ofsvgIcon(recommended — an RWC icon name fromrelativity-web-components),url,class, orfileName. See Card icons for details. - The
singletonproperty should be omitted or set tofalse. While today there is only one instance of the Viewer Collection component, in the future there may be more, which would result in multiple instances of the card, which would fail if this property is set totrue.
Accessing the Content Highlighting Card API
When the Content Highlighting Framework creates an instance of a content highlighting card, it injects the card's unique Content Highlighting Card API as a parameter to the card.
With a reference to the ICard object, extensions and their cards can access the Content Highlighting Card API via the parameters as follows:
const contentHighlightingCardApi = card.parameters[0];
Review extensions can access their content highlighting card via the Content Highlighting Manager associated with the Viewer Collection:
const FEATURE_ID = "acme-highlighting-feature";
const card = api.viewer.mainCollection.contentHighlights.getCard(FEATURE_ID);
const contentHighlightingCardApi = card.parameters[0];
Often, review extensions use the iframe loader to render card content. The Content Highlighting Framework injects the card object onto the iframe itself. The following code can be used to access the card within that card's iframe:
const card = window.frameElement.reviewCard;
const contentHighlightingCardApi = card.parameters[0];
Using the Content Highlighting Card API
The Content Highlighting Card API can be used to manage card state and is particularly useful for the following:
- Keeping card content in sync with the current document
- Keeping card content in sync with the current highlights rendered on a given document in a given viewer type
Keeping card content in sync with the current document
Content highlighting cards should keep their content in sync with the rest of the Review Interface by listening for events on the IContentHighlightingCardApi instance and on multiple IContentHighlightingApi instances.
At initial startup, the card should:
- Use the
IContentHighlightingCardApi'sgetContentDescription()andgetRenderStatus()methods to retrieve information about the current content being displayed in the application and the status of any in-flight content highlighting renders. This information can be stored in the card's application state. - Use the
IContentHighlightingCardApi'ssupportedViewerTypesproperty to determine the set of viewer types that it is supporting. This information can be stored in the card's application state. - Use the
IContentHighlightingCardApi'sgetHighlightingApi()method to retrieve the IContentHighlightingApi instance for each supported viewer type (if one has been created for each given viewer type). References to these instances can be stored in the card's application state. - Register event handlers for the various events emitted by each IContentHighlightingApi instance. These event handlers should update application state and/or update the UI as appropriate. See Highlighting events for more information.
- Register an event handler for the
contentevent onIContentHighlightingCardApi. These events indicate when new content is displayed in the viewer. They can be handled by the application to tell it when to update application state and/or when to update it's UI. In particular, thecontentevents will often provide new IContentHighlightingApi instances for specific viewer types as new content is loaded. When this happens, new event handlers, like those registered in step 4, should be registered on the new IContentHighlightingApi instance.- Use the
oncontent()method to register an event handler that fires each time new content is loaded in the viewer. The event includescontent(the new content description),previousContent,comparison, andhighlightingApi(theIContentHighlightingApifor the new content's viewer type, if available). Often times these event handlers will need to do things such as:- Clear application state related to all viewer types or a specific viewer type
- Switch which content is displayed to match that of the specific viewer type being actively used
- Register new event handlers on the new IContentHighlightingApi instance provided in the event
- Use the
onrender()method to register an event handler that fires each time a render function is invoked. The event includesviewerType,content, andhighlightingApi. - Use the
onrendercomplete()method to register an event handler that fires each time a render function completes. The event includesviewerType,content, andhighlightingApi. This is the recommended place to read final highlight counts. To show data only for the currently visible viewer, comparee.viewerTypeagainstapi.viewerCollection.activeViewer?.type. - Use the
onrendererror()method to register an event handler that fires each time a render function fails. The event adds anerrorproperty. This can be useful for showing an error message in the UI. - Use the
onrenderabort()method to register an event handler that fires each time a render function is aborted. (Usually this indicates that the user left the document before a render completed.) - Each
on*method has a correspondingoff*method (offcontent,offrender,offrendercomplete,offrendererror,offrenderabort) for deregistering event handlers during cleanup.
- Use the
Some highlighting card applications may use application frameworks such as React. In this case, these event handlers may be used to update React state, which will trigger React to update the UI. Other applications may not use any application frameworks at all. In this case, these event handlers may be used to perform DOM manipulation to update the UI.
The following TypeScript/React example illustrates a minimal card that displays the current viewer type's highlight count, staying in sync as the user switches between viewer types:
import React, { useEffect, useState } from 'react';
import type { IContentHighlightingCardApi } from 'reviewapi';
// In the card config's custom loader, access the card API via card.parameters[0]:
// loadCard: async (card, target) => {
// const cardApi = card.parameters[0] as IContentHighlightingCardApi;
// root = createRoot(target);
// root.render(<HitCountCard cardApi={cardApi} />);
// }
function HitCountCard({ cardApi }: { cardApi: IContentHighlightingCardApi }) {
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
const updateCount = () => {
const viewerType = cardApi.getContentDescription().viewerType;
if (!viewerType) { setCount(null); return; }
setCount(cardApi.getHighlightingApi(viewerType)?.getHighlights()?.length ?? null);
};
updateCount(); // populate immediately on mount
cardApi.oncontent(updateCount); // fires when viewer type or document changes
cardApi.onrendercomplete(updateCount); // fires when renderFn completes for any viewer type
return () => {
cardApi.offcontent(updateCount);
cardApi.offrendercomplete(updateCount);
};
}, [cardApi]);
if (count === null) return <p>Loading...</p>;
return <p><strong>{count}</strong> {count === 1 ? 'hit' : 'hits'} found</p>;
}
Key points:
getContentDescription().viewerTypereturns the currently active viewer type.getHighlightingApi(viewerType)?.getHighlights()returns the highlights for that viewer type from its most recent completed render — cached until the document changes, so switching back to a previously-viewed viewer type returns the correct count immediately without waiting for another render.onrendercompleteis the right place to read final highlight counts;onhighlightaddfires per-highlight as they are added during the search, which can be used to show a running count if desired.
Because highlight rendering will continue on in the background when a user switches between viewer types on a single document, highlighting cards will often maintain multiple instances of their UI in the DOM, one for each supported viewer type, and simply toggle the visibility of those instances so that only one is ever visible based on the active viewer type. Updates to the active viewer type can be listened for via the content event discussed above. This can help to ensure a performant user experience, when users frequently switch between different viewer types.
Navigating between highlights
Cards can drive highlight navigation — scrolling the viewer to the previous or next occurrence of a highlight set — using goToPrev(setId) and goToNext(setId) on IContentHighlightingApi.
To navigate, resolve the active viewer type and its highlighting API at the time the action occurs:
const viewerType = cardApi.getContentDescription().viewerType;
if (!viewerType) return; // no document loaded yet
const api = cardApi.getHighlightingApi(viewerType);
if (!api) return; // renders not yet complete for this viewer type
api.goToNext(setId); // scroll to next occurrence
api.goToPrev(setId); // scroll to previous occurrence
goToNext and goToPrev wrap around at the end/beginning of the document.
Obtaining setId:
setId is the identifier returned when a highlight set is created in the render function (e.g., the return value of highlightTerm()). To correlate sets with feature data at card time, attach identifying metadata when creating the set:
// In the render function:
const setId = params.highlightingApi.highlightTerm(
term,
highlightConfig,
searchConfig,
undefined,
{ term } // setMetadata — arbitrary key/value, accessible via set.metadata later
);
In the card, retrieve sets and match via metadata:
const api = cardApi.getHighlightingApi(viewerType);
const set = api?.getSets().find(s => (s.metadata as Record<string, unknown>)?.term === term);
if (set) api.goToNext(set.id);
s.highlights.length gives the total number of found occurrences and is safe to read after onrendercomplete has fired.
Controlling card visibility
By default, content highlighting feature cards will be visible whenever the active viewer type is one of the viewer types supported by the feature. (See Enabling features in specific viewer types for more information.)
Features can take on full control of when the feature card is visible or hidden by configuring a "card visibility function" in the cardVisibleFn property of their feature configuration. This function takes an instance of the Review Interface API, a reference to the Viewer Collection, and a content description object as parameters and returns a boolean indicating whether the card should be visible or not.
This is useful when features need to hide the card when specific types of unsupported content are loaded. For example, the following example card visibility function will only show the card when the the content type is a document, a viewer-level error, or an unsupported item placeholder AND the queue item is a document:
function (params) {
return ["queueitem", "viewererror", "unsupporteditem"].includes(params.content.type) &&
["document"].includes(params.content.item.type)
}
In the above example, short message documents or RDO documents would cause the card to be hidden. Similarly, an empty queue (something that happens in the Document Preview pane), would cause the card to be hidden.
NOTE: When features specify a card visibility function, they assume full control of card visibility. If the feature only supports a subset of viewer types, the feature's card visibility function is now responsible for setting card visibility based on viewer type.
Passing custom data from render functions to highlighting cards
It is common for highlighting features to need to pass custom data from the render function to the card. Often times, this data has a one-to-one relationship with individual highlights sets or highlights created by the render function. Other times, the data may have no relationship at all with highlight sets or highlights.
Highlight set-related data
When the data is related to a highlight set, it can be attached to the highlight set when it is created.
For location-based highlight sets, the data can be provided in the optional setMetadata parameter of the createSet() method:
const customData = {};
const setId = params.highlightingApi.createSet({ /* ...highlight config here */}, { foo: customData });
For search-based highlight sets, the data can be provided in the optional setMetadata parameter of the highlightTerm() or highlightTermWithContext() methods:
const customData = {};
const setOneId = params.highlightingApi.highlightTerm("needle", { /* ...highlight config here */}, { /* ...search config here */}, undefined, { foo: customData });
const setTwoId = params.highlightingApi.highlightTermWithContext("needle", "a needle in a haystack", { /* ...highlight config here */}, { /* ...search config here */}, undefined, { foo: customData });
Later, this custom data can be retrieved off of a highlight set object via the metadata property:
const highlightSet = params.highlightingApi.serialize(setId);
const retrievedCustomData = highlightSet.metadata.foo;
Highlight-related data
When the data is related to a highlight, it can be attached to the highlight when it is created:
For location-based highlights, the data can be provided in the optional highlightConfig parameter of the addHighlight() method:
const customData = {};
const highlight = await params.highlightingApi.addHighlight(highlightSetId, { /* ...location here */}, { userData: { foo: customData} });
const highlightId = highlight.id;
Later, the custom data can be retrieved off of the highlight object via the userData property:
const highlightSet = params.highlightingApi.serialize(setId);
const highlight = highlightSet.highlights.find(h => h.id === highlightId);
const retrievedCustomData = highlight.userData.foo;
For search-based highlights, the data can be provided in the optional termMetadata parameter of the highlightTerm(), highlightTermWithContext(), addTerm(), and addTermWithContext() methods:
const customData = {};
const setOneId = params.highlightingApi.highlightTerm("needle", { /* ...highlight config here */}, { /* ...search config here */}, { foo: customData });
const setTwoId = params.highlightingApi.highlightTermWithContext("needle", "a needle in a haystack", { /* ...highlight config here */}, { /* ...search config here */}, { foo: customData });
await params.highlightingApi.addTerm(setOneId, "haystack", { /* ...search config here */}, { foo: customData });
await params.highlightingApi.addTermWithContext(setTwoId, "haystack", "a needle in a haystack", { /* ...search config here */}, { foo: customData });
Later, the custom data can be retrieved off of the highlight objects via the termMetadata property:
const highlightSet = params.highlightingApi.serialize(setOneId);
const highlightsForTerm = highlightSet.highlights.filter(h => h.term === "needle");
const retrievedCustomData = highlightsForTerm[0].termMetadata.foo;
Other data
Sometimes, the custom data will not have a direct relationship with highlight set or highlight objects. In these cases, it is possible to use the Content Highlighting API to emit custom events with this data. The highlighting card can then register event listeners for these events to receive the custom data. See Custom events for more information.
Syncing text highlights across viewer types
This section discusses how to approach keeping the highlights created in one viewer type synced with the highlights created in another viewer type, where possible.
Challenges with cross-viewer text highlighting
Content highlighting features often strive to render the same highlights across all viewer types. This presents a challenge, since there are often differences between the document content rendered in the different viewer types.
For example, consider a Relativity document where the native document has both text and embedded images that include text. Now suppose Relativity OCR is used to generate long text data for the native. The resulting text in the text viewer will include the original text from the native document (possibly with some slight differences due to OCR quality) as well as the text from the embedded images. There are many ways for the text to differ between the different Relativity viewer types.
Consider another example: A Relativity document where the native is a PDF, the image was generated using Relativity Imaging, and the text was extracted by Relativity Processing during import. Imagine that a content highlighting feature used a custom image analysis process on the document image to identify the x, y coordinates of some content to be highlighted. Those coordinates would produce correctly-placed rectangular highlights in the Image Viewer. Scaling logic might even enable correctly placing similar highlights on the document in the Native Viewer. But now imaging that the content being highlighted is actually text. Placing rectangular highlights around text in the Text Viewer is not viable, because the flow of the text can change the location of the text on the document canvas when users resize the window, change the spacing, or enable word-wrap.
Best practices for syncing cross-viewer text highlights
The following are best practices for creating text highlights across multiple viewer types.
1. Use search-based highlighting to create highlights
Using search-based highlighting (highlightTerm() and addTerm()), where search terms are provided to the content highlighting API, which searches for any occurrences of the term and highlights them, can streamline the highlighting code. There are pros and cons to this approach.
Pros:
- Highlights can be rendered in any viewer type that supports text as long as the text appears exactly the same in those viewer types
- A single dataset can be used to generate highlights across all viewers, since the terms are the same across viewer types. Cons:
- If a term appears in the document text multiple times, but the content highlighting feature should only highlight a single occurrence of that term, further work is required. In these cases, sometimes the
highlightTermWithContext()andaddTermWithContext()APIs can work around this issue. For assistance with more advanced use cases, please reach out to developer support.
2. Use set groups to sync highlight visibility across viewer types
Most content highlighting features provide users with the ability to hide and show specific groups of highlights via UI controls. This can make it easier to focus on specific highlights on document content. Typically this is achieved by using the IContentHighlightingApi to hide and show the highlight sets those highlights belong to.
Because highlights and highlight sets are unique to a specific viewer type, a given term that was highlighting via term highlighting in multiple viewer types will have unique highlights and highlight sets for those viewer types. Hiding the highlight set that a highlight of term needle belongs to in the Native Viewer will not also hide the highlight set that a highlight with the same term belongs to in the Text Viewer.
To sync the visibility of these different highlight sets across multiple viewer types, the highlight sets can be added to the same "set group." Set groups can be used to group highlight sets across viewers, documents, and even Review Interface application lifespans. The visibility of any highlight sets that belong to the same set group will be kept in sync. This visibility data is stored in browser session storage.
Below is an example of creating two highlight sets and placing them in the same set group:
async function highlight(highlightingApi, highlightConfig, textSearchConfig) {
const highlightSetOneId = highlightingApi.createSet(highlightConfig);
// Add highlights to set one here...
const highlightSetTwoId = highlightingApi.highlightTerm("needle", highlightConfig, textSearchConfig);
const setGroupId = "my-set-group";
highlightingApi.createSetGroup(setGroupId, {
visibilityPersistenceMode: "browsersessionstorage",
});
highlightingApi.addSetToGroup(setGroupId, highlightSetOneId);
highlightingApi.addSetToGroup(setGroupId, highlightSetTwoId);
await highlightingApi.waitForTermHighlightingToComplete();
}
Currently, the visibility of set groups can either be persisted to browser session storage or not persisted at all. Browser session storage persistence is the default.
NOTE: Content highlighting features are currently limited to 100 set groups in any given workspace. Any set groups created beyond this point will cause the least recently used set group to be deleted. Any highlight sets in that group will remain unaffected if they are still rendered in the UI.
3. Cache HTTP requests in render functions
As mentioned in the first best practice above, using term highlighting across viewer types comes with the advantage of only needing to request a single set of data to render highlights across all viewer types as opposed to making unique requests for each viewer type. This can improve the time to first render for highlights, by eliminating some data retrieval.
For example, imagine that Document 1 is loaded in the Native Viewer and the render function of a content highlighting feature is invoked and retrieves data from the API to start rendering highlights in the Native Viewer. Then the user navigates to the Text Viewer for Document 1 and the render function is invoked a second time. This time, however, no additional data needs to be retrieved. The content highlighting feature can cache the results retrieved in the first render function invocation to use in the second.
The Content Highlighting Framework provides an item-level cache, ICache, to render functions that can be used to cache anything, but is particularly well-suited to cache data retrieved via HTTP calls. This cache is shared by all invocations of the feature's render function across all viewer types for a given document. When the user navigates to a new document in the review queue, a new cache object will be created and the old one will be cleaned up.
NOTE: The Content Highlighting Framework does not clear the item-level cache when the render function is being forcefully refreshed. It is up to the author of the render function to implement this, if it makes sense for a particular content highlighting feature.
Here is an example of using the item-level cache to cache the data retrieved by an HTTP call in a render function:
async function retrieveData(params) {
const workspaceId = params.api.configuration.workspaceId;
const documentId = params.content?.item?.artifactId;
const cache = params.cache;
const CACHE_KEY = "data";
if (cache.has(CACHE_KEY)) {
return cache.get(CACHE_KEY);
}
const url = `/acme/api/${workspaceId}/${documentId}/highlights`;
const response = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: {
["Content-Type"]: "application/json",
["X-CSRF-Header"]: "-",
},
signal: params.abortSignal,
});
const data = await response.json();
cache.set(CACHE_KEY, data);
return data;
}
In the above example, the same endpoint will always be used. In scenarios, where other variable parameters must be passed to the endpoint, the cache key may need to be more unique:
async function retrieveData(params, optionValue) {
const workspaceId = params.api.configuration.workspaceId;
const documentId = params.content?.item?.artifactId;
const cache = params.cache;
const CACHE_KEY = optionValue;
if (cache.has(CACHE_KEY)) {
return cache.get(CACHE_KEY);
}
const url = `/acme/api/${workspaceId}/${documentId}/highlights?option=${optionValue}`;
const response = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: {
["Content-Type"]: "application/json",
["X-CSRF-Header"]: "-",
},
signal: params.abortSignal,
});
const data = await response.json();
cache.set(CACHE_KEY, data);
return data;
}
Finally, as discussed in Determining what data should be retrieved, sometimes multiple HTTP calls may need to be made by a single render function. The provided cache can be used to support these use cases, as well, as long as the series of HTTP calls made for a given document will always be the same:
async function retrieveData(params, offset, batchSize) {
const workspaceId = params.api.configuration.workspaceId;
const documentId = params.content?.item?.artifactId;
const cache = params.cache;
const CACHE_KEY = `${offset}-${batchSize}`;
if (cache.has(CACHE_KEY)) {
return cache.get(CACHE_KEY);
}
const url = `/acme/api/${workspaceId}/${documentId}/highlights?offset=${offset}&batchSize=${batchSize}`;
const response = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: {
["Content-Type"]: "application/json",
["X-CSRF-Header"]: "-",
},
signal: params.abortSignal,
});
const data = await response.json();
cache.set(CACHE_KEY, data);
return data;
}
const batchSize = 100;
let offset = 0;
let totalTermCount;
let termsRetrieved = 0;
do {
// Retrieve new page of results
const response = await retrieveData(renderParams, offset, batchSize);
termsRetrieved += response.data.length;
totalTermCount = response.totalCount;
// Start searching and highlighting terms in document content
startHighlightingTerms(renderParams, response.data);
offset += batchSize;
} while(termsRetrieved < totalTermCount);
// Wait for all term highlighting to complete
await renderParams.highlightingApi.waitForTermHighlightingToComplete();