diff --git a/src/common/graph/node.ts b/src/common/graph/node.ts index fd6bb7d77e9a2ddc5d7d690c2ace7433c961d675..2a1126ab43d5a6ff52fac534b57433ac2a10e93b 100644 --- a/src/common/graph/node.ts +++ b/src/common/graph/node.ts @@ -2,13 +2,19 @@ import { GraphElement } from "./graphelement"; import { NodeType } from "./nodetype"; import { Link } from "./link"; +export interface DescriptiveReference { + url: string; + description: string; +} + export interface NodeProperties { name: string; description?: string; icon?: string; banner?: string; video?: string; - references?: string[]; + references?: DescriptiveReference[]; + visibleAfter?: Date; } export interface NodeData extends NodeProperties { @@ -62,15 +68,15 @@ export interface GraphNode extends NodeProperties { export class Node extends GraphElement<NodeData, SimNodeData> - implements GraphNode -{ + implements GraphNode { public name: string; public description: string; public type: NodeType; public icon: string; public banner: string; public video: string; - public references: string[]; + public references: DescriptiveReference[]; + public visibleAfter: Date; public neighbors: Node[]; public links: Link[]; @@ -102,6 +108,7 @@ export class Node banner: this.banner, video: this.video, references: this.references, + visibleAfter: this.visibleAfter, type: this.type ? this.type.id : undefined, }; } diff --git a/src/display/components/nodeinfo/mediaarea.tsx b/src/display/components/nodeinfo/mediaarea.tsx index e8ebfe4107f8a8ce75c112e63f44b2b563f3c5a4..ef7792770de50ad74aa81a047d4054e9d9540317 100644 --- a/src/display/components/nodeinfo/mediaarea.tsx +++ b/src/display/components/nodeinfo/mediaarea.tsx @@ -5,12 +5,13 @@ import splitAtDelimiters from "./splitAtDelimiters"; import "katex/dist/katex.css"; import "./mediaarea.css"; import References from "./references"; +import { DescriptiveReference } from "../../../common/graph/node"; interface MediaAreaProps { description: string; image?: string; video?: string; - references?: string[]; + references?: DescriptiveReference[]; } /** diff --git a/src/display/components/nodeinfo/references.tsx b/src/display/components/nodeinfo/references.tsx index f883ec6e24442550427728b52d79b0696f42cd33..1471c388e28554202e0254641df97ed741a95b1e 100644 --- a/src/display/components/nodeinfo/references.tsx +++ b/src/display/components/nodeinfo/references.tsx @@ -1,7 +1,8 @@ import React from "react"; +import { DescriptiveReference } from "../../../common/graph/node"; interface ReferencesProps { - references: string[]; + references: DescriptiveReference[]; } /** @@ -10,13 +11,21 @@ interface ReferencesProps { * @constructor */ function References({ references }: ReferencesProps) { + + return ( <ul> {references.map((ref) => ( - <li key={ref}> - <a href={ref} rel={"noopener noreferrer"} target={"_blank"}> - {ref} + <li key={ref.url}> + [ + <a + href={ref.url} + rel={"noopener noreferrer"} + target={"_blank"} + > + {ref.url} </a> + ] {ref.description} </li> ))} </ul> diff --git a/src/editor/components/nodedetails.tsx b/src/editor/components/nodedetails.tsx index 6e29bd45fab8228b6356e63a6f2afa522e42c148..8f7981c2ea99605c7f7f3b0cef8bd38520d3076f 100644 --- a/src/editor/components/nodedetails.tsx +++ b/src/editor/components/nodedetails.tsx @@ -3,6 +3,7 @@ import { Node } from "../../common/graph/node"; import { NodeType } from "../../common/graph/nodetype"; import "./nodedetails.css"; import { NodeDataChangeRequest } from "../editor"; +import ReferencesEditor from "./referenceseditor"; type NodeDetailsProps = { selectedNodes: Node[]; @@ -114,7 +115,7 @@ function NodeDetails({ <h3>{selectedNodes.length} nodes selected</h3> )} - {selectedNodes.length === 1 ? ( + {selectedNodes.length === 1 && ( <div> <label htmlFor="node-description">Description</label> <br /> @@ -129,8 +130,6 @@ function NodeDetails({ } ></textarea> </div> - ) : ( - "" )} <div> <label htmlFor="node-image">Icon Image</label> @@ -219,23 +218,13 @@ function NodeDetails({ } ></input> </div> - {selectedNodes.length === 1 ? ( - <div> - <label htmlFor="node-references">References</label>{" "} - <small>One URL per line</small> - <br /> - <textarea - id="node-references" - name="node-references" - className="bottom-space" - value={referenceData.references ?? ""} - onChange={(event) => - handleDataChange("references", event.target.value) - } - ></textarea> - </div> - ) : ( - "" + {selectedNodes.length === 1 && ( + <ReferencesEditor + references={referenceData.references ?? []} + onReferencesChange={(references) => + handleDataChange("references", references) + } + /> )} </div> ); diff --git a/src/editor/components/referenceentry.tsx b/src/editor/components/referenceentry.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b1fedca4259a18f9f418a3b3c928fe8470e41f60 --- /dev/null +++ b/src/editor/components/referenceentry.tsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import { DescriptiveReference } from "../../common/graph/node"; +import "./nodetypeentry.css"; + +type ReferenceEntryProps = { + reference: DescriptiveReference; + onReferenceDelete: (url: string) => void; + onReferenceChange: ( + originalUrl: string, + reference: DescriptiveReference + ) => void; +}; + +function ReferenceEntry({ + reference, + onReferenceDelete, + onReferenceChange, +}: ReferenceEntryProps) { + const [editedReference, setEditedReference] = + useState<DescriptiveReference>(reference); + const originalUrl = reference.url; + + const onPropChange = ( + prop: keyof DescriptiveReference, + newValue: string + ) => { + reference[prop] = newValue; + setEditedReference(reference); + }; + + return ( + <div + className="reference" + onBlur={() => onReferenceChange(originalUrl, editedReference)} + > + <label htmlFor="reference-url">Url</label> + <br /> + <input + className="reference-url" + type={"text"} + value={reference.url} + onChange={(event) => onPropChange("url", event.target.value)} + /> + <br /> + <label htmlFor="reference-description">Description</label> + <br /> + <textarea + className="reference-description" + value={reference.description} + onChange={(event) => + onPropChange("description", event.target.value) + } + /> + <br /> + <button onClick={() => onReferenceDelete(originalUrl)}> + Delete + </button> + </div> + ); +} + +export default ReferenceEntry; diff --git a/src/editor/components/referenceseditor.tsx b/src/editor/components/referenceseditor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6f615e858a621c824a6349a124fbf4a9769fc1f1 --- /dev/null +++ b/src/editor/components/referenceseditor.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { DescriptiveReference } from "../../common/graph/node"; +import "./nodetypeseditor.css"; +import ReferenceEntry from "./referenceentry"; + +type ReferencesEditorProps = { + references: DescriptiveReference[]; + onReferencesChange: (newReferences: DescriptiveReference[]) => void; +}; + +function ReferencesEditor({ + references, + onReferencesChange, +}: ReferencesEditorProps) { + const isUrlUnique = (url: string): boolean => + !references.some((ref: DescriptiveReference) => ref.url == url); + + const handleReferenceDelete = (url: string) => { + references = references.filter( + (ref: DescriptiveReference) => ref.url != url + ); + onReferencesChange(references); + }; + + const handleReferenceChange = ( + url: string, + reference: DescriptiveReference + ) => { + // Make sure url stays unique + if (isUrlUnique(reference.url)) { + console.log( + "URL of changed reference is not unique anymore for current node. Cannot save changes on reference." + ); + return; + } + + const index: number = references.findIndex( + (ref: DescriptiveReference) => ref.url == url + ); + references[index] = reference; + onReferencesChange(references); + }; + + const addReference = () => { + // Make sure no empty url exists already + // => Button only enabled if no empty url in references + + references.push({ url: "", description: "" }); + onReferencesChange(references); + }; + + return ( + <div id="references-editor"> + <label htmlFor="references-editor">References</label> + {references.map( + (reference: DescriptiveReference, index: number) => { + return ( + <ReferenceEntry + key={index} + reference={reference} + onReferenceChange={handleReferenceChange} + onReferenceDelete={handleReferenceDelete} + /> + ); + } + )} + <br /> + <button onClick={addReference} disabled={!isUrlUnique("")}> + Add reference + </button> + </div> + ); +} + +export default ReferencesEditor;