diff --git a/packages/widget/src/assets/copy-to-clipboard.ts b/packages/widget/src/assets/copy-to-clipboard.ts
new file mode 100644
index 00000000..83fce8e3
--- /dev/null
+++ b/packages/widget/src/assets/copy-to-clipboard.ts
@@ -0,0 +1,30 @@
+import { svg } from 'lit';
+
+export const copyToClipboardButton = (
+ textToCopy: string,
+ onCopy: (newText: string, x: number, y: number) => void,
+) => svg`
+
+`;
+
+// Be aware: the copy-to-clipboard functionality is not tested. Sadly, it is basically impossible to test clipboard
+// functionality in Playwright.
+const copyToClipboard = (
+ event: PointerEvent,
+ textToCopy: string,
+ onCopy: (newText: string, x: number, y: number) => void,
+) => {
+ navigator.clipboard
+ .writeText(textToCopy)
+ .then(() => {
+ const x: number = event.pageX;
+ const y: number = event.pageY;
+ onCopy('Copied!', x, y);
+ })
+ .catch((err) => {
+ console.log('Failed to copy to clipboard\n\n', err);
+ });
+};
diff --git a/packages/widget/src/assets/index.ts b/packages/widget/src/assets/index.ts
index 947fd7af..9c1dba2f 100644
--- a/packages/widget/src/assets/index.ts
+++ b/packages/widget/src/assets/index.ts
@@ -1,4 +1,5 @@
// All files which will be accessible when the widget is installed via npm are declared here
+export * from './copy-to-clipboard';
export * from './close-details-button';
export * from './logo-small';
export * from './logo-large';
diff --git a/packages/widget/src/detail-navigation-header.ts b/packages/widget/src/detail-navigation-header.ts
index c3d6b4a8..4b05c149 100644
--- a/packages/widget/src/detail-navigation-header.ts
+++ b/packages/widget/src/detail-navigation-header.ts
@@ -17,7 +17,7 @@ const backButton = (
`;
};
diff --git a/packages/widget/src/detail-view.ts b/packages/widget/src/detail-view.ts
index 197b227f..4b40317d 100644
--- a/packages/widget/src/detail-view.ts
+++ b/packages/widget/src/detail-view.ts
@@ -6,7 +6,7 @@ import {
TYPE_DISPLAY_OPTIONS,
} from './display-object.ts';
import { renderDetailNavigationHeader } from './detail-navigation-header';
-import { closeDetailsButton } from './assets';
+import { closeDetailsButton, copyToClipboardButton } from './assets';
type MetadataKey = string;
type MetadataValue = string | string[];
@@ -17,6 +17,7 @@ export function renderDetailsView(
allNodes: DisplayObject[],
updateSelectedNode: (node: DisplayObject) => void,
closeDetailsView: () => void,
+ updateDetailTooltip: (newText: string, x: number, y: number) => void,
): HTMLTemplateResult {
const opts = TYPE_DISPLAY_OPTIONS[selectedNode.type];
const backgroundColor = opts.detailViewBackgroundColor || opts.backgroundColor;
@@ -24,7 +25,9 @@ export function renderDetailsView(
const fieldsToDisplay: MetadataTuple[] = getMetadataFieldsToDisplay(selectedNode);
const detailBody: HTMLTemplateResult =
- fieldsToDisplay.length > 0 ? createMetadataGrid(fieldsToDisplay) : emptyMetadataMessage();
+ fieldsToDisplay.length > 0
+ ? renderMetadataGrid(fieldsToDisplay, updateDetailTooltip)
+ : emptyMetadataMessage();
return html`
@@ -42,16 +45,20 @@ export function renderDetailsView(
`;
}
-const createMetadataGrid = (
+// Renders the metadata grid, which is a 2-column grid of key-value pairs.
+// The key is displayed in the left column, and the value is displayed in the right column. If the value is an array, it
+// is displayed as multiple rows, with the key cell spanning all those rows.
+const renderMetadataGrid = (
metadataEntries: [MetadataKey, MetadataValue][],
+ updateDetailTooltip: (newText: string, x: number, y: number) => void,
): HTMLTemplateResult => {
const gridItems: HTMLTemplateResult[] = metadataEntries.map(([key, value], index) =>
- createGridItem(key, value, index),
+ createGridItem(key, value, index, updateDetailTooltip),
);
return html`
+ `;
}
const createGridItem = (
key: MetadataKey,
value: MetadataValue,
index: number,
+ updateDetailTooltip: (newText: string, x: number, y: number) => void,
): HTMLTemplateResult => {
- return html` ${displayMetadataKey(key, value, index)} ${displayMetadataValue(key, value)} `;
+ return html`
+ ${renderMetadataKey(key, value, index)} ${displayMetadataValue(key, value, updateDetailTooltip)}
+ `;
};
const emptyMetadataMessage = (): HTMLTemplateResult => {
@@ -124,10 +136,15 @@ const getMetadataFieldsToDisplay = (node: DisplayObject): MetadataTuple[] => {
// then keep only the fields that should be displayed:
return Object.entries(normalizedNode)
.filter(([key, value]) => isDisplayObjectMetadataField(key) && value)
- .filter(isMetadataTuple);
+ .filter(valueIsAStringOrStringArray);
};
-const isMetadataTuple = (tuple: [string, any]): tuple is MetadataTuple => {
+// This type guard asserts that the tuple's value is a string or string array. We need to narrow this down so that we
+// can handle "content" differently from other fields, making its key cell span multiple rows.
+//
+// It's worth noting that if we ever add a non-string or non-string-array field to DisplayObjectMetadata, this method
+// will need to be updated, because right now it will filter those fields out and keep them from being displayed.
+const valueIsAStringOrStringArray = (tuple: [string, any]): tuple is MetadataTuple => {
const [_, value] = tuple;
const isString = typeof value === 'string';
diff --git a/packages/widget/src/display-object.ts b/packages/widget/src/display-object.ts
index b739f24a..ee48f7c7 100644
--- a/packages/widget/src/display-object.ts
+++ b/packages/widget/src/display-object.ts
@@ -15,16 +15,16 @@ export interface DisplayObjectMetadata {
actors?: string;
}
-// DisplayObjects are the widget's internal representation of a node from the graph view.
-// They roughly correspond to a ThingT in the Docmap spec, but with only the fields that we want to display.
+// A DisplayObject is the widget's internal representation of a node in the graph view.
+// This type roughly corresponds to ThingT in the @docmaps/sdk, but with only the fields that we want to display.
export interface DisplayObject extends DisplayObjectMetadata {
nodeId: string; // Used internally to construct graph relationships, never rendered
type: string;
}
-// The following 3 statements allow us to use FieldsToDisplay both as a type and as something we can
-// check against at runtime. We could also use io-ts for this, but that felt like overkill since this
-// is the only place in the widget where we do something like this.
+// The following 3 statements allow us to use FieldsToDisplay both as a type and as something we can check against at
+// runtime. We could use io-ts for this, but that felt like overkill since this is the only place in the widget where we
+// do something like this.
export type DisplayObjectMetadataField = keyof DisplayObjectMetadata;
const DisplayObjectMetadataPrototype: { [K in DisplayObjectMetadataField]: null } = {
doi: null,
@@ -39,8 +39,8 @@ export function isDisplayObjectMetadataField(key: string): key is DisplayObjectM
return key in DisplayObjectMetadataPrototype;
}
-// Returns a new DisplayObject which has no fields set to the value undefined,
-// meaning the new Display Object can be merged with another DisplayObject via destructuring.
+// Returns a new DisplayObject which has no fields set to the value undefined, meaning the new Display Object can be
+// merged with another DisplayObject via destructuring.
//
// Also puts the fields in the order in which they should be displayed.
export function normalizeDisplayObject(displayObject: DisplayObject): DisplayObject {
@@ -57,6 +57,8 @@ export function normalizeDisplayObject(displayObject: DisplayObject): DisplayObj
};
}
+// Lets you combine two display objects into one, with the fields of the second object taking precedence.
+// only the first object can be undefined.
export function mergeDisplayObjects(a: DisplayObject | undefined, b: DisplayObject): DisplayObject {
return {
...(a && normalizeDisplayObject(a)),
@@ -64,7 +66,7 @@ export function mergeDisplayObjects(a: DisplayObject | undefined, b: DisplayObje
};
}
-// DisplayObjectEdges are the widget's internal representation of an edge connecting two DisplayObjects.
+// DisplayObjectEdges are the widget's internal representation of a connection between two DisplayObjects.
export type DisplayObjectEdge = {
sourceId: string;
targetId: string;
@@ -75,16 +77,23 @@ export type DisplayObjectGraph = {
edges: DisplayObjectEdge[];
};
-// The appearance of a DisplayObject in the graph view and in the detail view is determined by its 'type' field.
-// The following constants define the possible values of the 'type' field and the appearance that corresponds with each value.
+// The appearance of a DisplayObject in the graph view and in the detail view is determined by its 'type' field. The
+// following constants define the possible values of the 'type' field and the styling that corresponds with each value.
export type TypeDisplayOption = {
+ // Abbreviation representing the type, e.g. 'R' for 'Review'
shortLabel: string;
+ // Full, human-readable name of the type, e.g. 'Review'
longLabel: string;
+ // Background color of the node in the graph view
backgroundColor: string;
+ // Text color of the node in the graph view
textColor: string;
- dottedBorder?: boolean; // whether the node should be rendered with a dotted border
- detailViewBackgroundColor?: string; // if this is not set, backgroundColor will be used
- detailViewTextColor?: string; // if this is not set, textColor will be used
+ // whether the node should be rendered with a dotted border in the graph view
+ dottedBorder?: boolean;
+ // Background color of the detail view header. if this is not set, backgroundColor will be used
+ detailViewBackgroundColor?: string;
+ // Text color of the detail view header. if this is not set, textColor will be used
+ detailViewTextColor?: string;
};
export const TYPE_DISPLAY_OPTIONS: {
diff --git a/packages/widget/src/docmaps-widget.ts b/packages/widget/src/docmaps-widget.ts
index 7c6e1ec8..8c63c6ed 100644
--- a/packages/widget/src/docmaps-widget.ts
+++ b/packages/widget/src/docmaps-widget.ts
@@ -24,6 +24,13 @@ export class DocmapsWidget extends LitElement {
@state()
graph?: DisplayObjectGraph;
+ @state()
+ detailTooltip: { text: string; x: number; y: number } = {
+ text: '',
+ x: 0,
+ y: 0,
+ };
+
// We keep track of this because we have to render once before drawing the graph
// so that D3 has a canvas to draw into.
#hasRenderedOnce: boolean = false;
@@ -50,9 +57,21 @@ export class DocmapsWidget extends LitElement {
};
// Method to clear the selected node and go back to the graph view
- private closeDetailView() {
+ private closeDetailView = () => {
this.selectedNode = undefined;
- }
+ };
+
+ private updateDetailTooltip = (newText: string, x: number, y: number) => {
+ // Show the tooltip with the provided text and position
+ this.detailTooltip = {
+ text: newText,
+ x: x,
+ y: y,
+ };
+
+ // Set a timeout to hide the tooltip after 3 seconds
+ setTimeout(() => (this.detailTooltip = { ...this.detailTooltip, text: '' }), 2000);
+ };
render(): HTMLTemplateResult {
const d3Canvas: HTMLTemplateResult = html` `;
@@ -65,6 +84,23 @@ export class DocmapsWidget extends LitElement {
DOCMAP