-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use custom guard to check if domNode is Element in RichTextElement re…
…placer - it is a (hopefully temporary) workaround for an issue where the recommended instanceof check doesn't work (see remarkablemark/html-react-parser#633)
- Loading branch information
Showing
1 changed file
with
94 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,145 +1,142 @@ | ||
import React from "react"; | ||
import parseHTML, { domToReact, DOMNode, HTMLReactParserOptions, Element } from "html-react-parser"; | ||
import React from 'react'; | ||
import parseHTML, { domToReact, DOMNode, HTMLReactParserOptions, Element } from 'html-react-parser'; | ||
import { Elements, IContentItem, ILink, IRichTextImage } from '@kontent-ai/delivery-sdk'; | ||
|
||
const IMAGE_ID_ATTRIBUTE_IDENTIFIER = "data-image-id"; | ||
const LINKED_ITEM_ID_ATTRIBUTE_IDENTIFIER = "data-item-id"; | ||
const IMAGE_ID_ATTRIBUTE_IDENTIFIER = 'data-image-id'; | ||
const LINKED_ITEM_ID_ATTRIBUTE_IDENTIFIER = 'data-item-id'; | ||
|
||
const isLinkedItem = (domNode: DOMNode): boolean => { | ||
if (domNode instanceof Element) { | ||
return domNode.tagName === "object" && domNode.attributes.find(attr => attr.name === "type")?.value === "application/kenticocloud"; | ||
} | ||
return false; | ||
} | ||
// Use custom guard instead of the `instanceof` check because it seems to be broken now (see https://github.com/remarkablemark/html-react-parser/issues/633) | ||
const isElement = (domNode: DOMNode): domNode is Element => | ||
domNode.type === 'tag'; | ||
|
||
const isImage = (domNode: DOMNode): boolean => { | ||
if (domNode instanceof Element) { | ||
return domNode.tagName === "figure" && domNode.attributes.find(attr => attr.name === IMAGE_ID_ATTRIBUTE_IDENTIFIER)?.value !== "undefined"; | ||
} | ||
return false; | ||
} | ||
const isLinkedItem = (domNode: DOMNode): domNode is Element => | ||
isElement(domNode) && | ||
domNode.tagName === 'object' && | ||
domNode.attributes.find(attr => attr.name === 'type')?.value === 'application/kenticocloud'; | ||
|
||
const isLinkedItemLink = (domNode: DOMNode) => { | ||
if (domNode instanceof Element) { | ||
return domNode.tagName === "a" && domNode.attributes.find(attr => attr.name === LINKED_ITEM_ID_ATTRIBUTE_IDENTIFIER)?.value !== "undefined"; | ||
} | ||
return false; | ||
} | ||
const isImage = (domNode: DOMNode): domNode is Element => | ||
isElement(domNode) && | ||
domNode.tagName === 'figure' && | ||
domNode.attributes.find(attr => attr.name === IMAGE_ID_ATTRIBUTE_IDENTIFIER)?.value !== 'undefined'; | ||
|
||
const isLinkedItemLink = (domNode: DOMNode): domNode is Element => | ||
isElement(domNode) && | ||
domNode.tagName === 'a' && | ||
domNode.attributes.find(attr => attr.name === LINKED_ITEM_ID_ATTRIBUTE_IDENTIFIER)?.value !== 'undefined'; | ||
|
||
export type DOMToReactFunction = ( | ||
nodes: DOMNode[], | ||
options?: HTMLReactParserOptions | undefined | ||
nodes: DOMNode[], | ||
options?: HTMLReactParserOptions | undefined, | ||
) => string | JSX.Element | JSX.Element[] | ||
|
||
|
||
export type DomElementOptionsType = { | ||
domElement: Element, | ||
domToReact: DOMToReactFunction | ||
domElement: Element, | ||
domToReact: DOMToReactFunction | ||
}; | ||
|
||
export type ResolveLinkedItemType = ( | ||
linkedItem: IContentItem | undefined, | ||
domOptions: DomElementOptionsType | ||
linkedItem: IContentItem | undefined, | ||
domOptions: DomElementOptionsType, | ||
) => JSX.Element | JSX.Element[] | undefined | null | false | ||
|
||
export type ResolveImageType = ( | ||
image: IRichTextImage, | ||
domOptions: DomElementOptionsType | ||
image: IRichTextImage, | ||
domOptions: DomElementOptionsType, | ||
) => JSX.Element | JSX.Element[] | undefined | null | false | ||
|
||
export type ResolveLinkType = ( | ||
link: ILink, | ||
domOptions: DomElementOptionsType | ||
link: ILink, | ||
domOptions: DomElementOptionsType, | ||
) => JSX.Element | JSX.Element[] | undefined | null | false | ||
|
||
export type ResolveDomNodeType = ( | ||
domOptions: { | ||
domNode: DOMNode, | ||
domToReact: DOMToReactFunction | ||
} | ||
domOptions: { | ||
domNode: DOMNode, | ||
domToReact: DOMToReactFunction | ||
}, | ||
) => JSX.Element | JSX.Element[] | undefined | null | false | ||
|
||
|
||
const replaceNode = ( | ||
domNode: DOMNode, | ||
richTextElement: Elements.RichTextElement, | ||
resolveLinkedItem?: ResolveLinkedItemType, | ||
resolveImage?: ResolveImageType, | ||
resolveLink?: ResolveLinkType, | ||
resolveDomNode?: ResolveDomNodeType, | ||
domNode: DOMNode, | ||
richTextElement: Elements.RichTextElement, | ||
resolveLinkedItem?: ResolveLinkedItemType, | ||
resolveImage?: ResolveImageType, | ||
resolveLink?: ResolveLinkType, | ||
resolveDomNode?: ResolveDomNodeType, | ||
): JSX.Element | object | void | undefined | null | false => { | ||
|
||
const { images, links } = richTextElement; | ||
if (resolveLinkedItem && richTextElement.linkedItems) { | ||
if (isLinkedItem(domNode)) { | ||
const node = domNode as Element; | ||
const codeName = node?.attributes.find(attr => attr.name === "data-codename")?.value; | ||
const linkedItem = codeName ? richTextElement.linkedItems.find(item => item.system.codename === codeName) : undefined; | ||
return resolveLinkedItem(linkedItem, { domElement: node, domToReact }); | ||
} | ||
const { images, links } = richTextElement; | ||
if (resolveLinkedItem && richTextElement.linkedItems) { | ||
if (isLinkedItem(domNode)) { | ||
const node = domNode; | ||
const codeName = node?.attributes.find(attr => attr.name === 'data-codename')?.value; | ||
const linkedItem = codeName ? richTextElement.linkedItems.find(item => item.system.codename === codeName) | ||
: undefined; | ||
return resolveLinkedItem(linkedItem, { domElement: node, domToReact }); | ||
} | ||
|
||
if (resolveImage && images) { | ||
if (isImage(domNode)) { | ||
const node = domNode as Element; | ||
const imageId = node?.attributes.find(attr => attr.name === IMAGE_ID_ATTRIBUTE_IDENTIFIER)?.value; | ||
const image = images.find((image: { imageId: string }) => image.imageId === imageId); | ||
if (image) { | ||
return resolveImage(image, { domElement: node, domToReact }); | ||
} | ||
} | ||
} | ||
|
||
if (resolveImage && images) { | ||
if (isImage(domNode)) { | ||
const node = domNode; | ||
const imageId = node?.attributes.find(attr => attr.name === IMAGE_ID_ATTRIBUTE_IDENTIFIER)?.value; | ||
const image = images.find((image: { imageId: string }) => image.imageId === imageId); | ||
if (image) { | ||
return resolveImage(image, { domElement: node, domToReact }); | ||
} | ||
} | ||
} | ||
|
||
if (resolveLink && links) { | ||
if (isLinkedItemLink(domNode)) { | ||
const node = domNode as Element; | ||
if (resolveLink && links) { | ||
if (isLinkedItemLink(domNode)) { | ||
const node = domNode; | ||
|
||
const linkId = node?.attributes.find(attr => attr.name === LINKED_ITEM_ID_ATTRIBUTE_IDENTIFIER)?.value; | ||
const link = links.find((link: { linkId: string }) => link.linkId === linkId); | ||
if (link) { | ||
return resolveLink(link, { domElement: node, domToReact }); | ||
} | ||
} | ||
const linkId = node?.attributes.find(attr => attr.name === LINKED_ITEM_ID_ATTRIBUTE_IDENTIFIER)?.value; | ||
const link = links.find((link: { linkId: string }) => link.linkId === linkId); | ||
if (link) { | ||
return resolveLink(link, { domElement: node, domToReact }); | ||
} | ||
} | ||
} | ||
|
||
if (resolveDomNode) { | ||
return resolveDomNode({ domNode, domToReact }); | ||
} | ||
} | ||
if (resolveDomNode) { | ||
return resolveDomNode({ domNode, domToReact }); | ||
} | ||
}; | ||
|
||
export type ResolversType = { | ||
resolveLinkedItem?: ResolveLinkedItemType; | ||
resolveImage?: ResolveImageType; | ||
resolveLink?: ResolveLinkType; | ||
resolveDomNode?: ResolveDomNodeType; | ||
resolveLinkedItem?: ResolveLinkedItemType; | ||
resolveImage?: ResolveImageType; | ||
resolveLink?: ResolveLinkType; | ||
resolveDomNode?: ResolveDomNodeType; | ||
} | ||
|
||
export type IReactRichTextElementProps = { | ||
richTextElement: Elements.RichTextElement, | ||
resolvers?: ResolversType | ||
richTextElement: Elements.RichTextElement, | ||
resolvers?: ResolversType | ||
|
||
}; | ||
|
||
const RichTextElement: React.FC<IReactRichTextElementProps> = ({ | ||
richTextElement, | ||
resolvers: resolvers | ||
export const RichTextElement: React.FC<IReactRichTextElementProps> = ({ | ||
richTextElement, | ||
resolvers: resolvers, | ||
}) => { | ||
|
||
// To avoid type error in case resolvers is undefined | ||
const { | ||
resolveLinkedItem, | ||
resolveImage, | ||
resolveLink, | ||
resolveDomNode, | ||
} = { ...resolvers }; | ||
|
||
// To avoid type error in case resolvers is undefined | ||
const { | ||
resolveLinkedItem, | ||
resolveImage, | ||
resolveLink, | ||
resolveDomNode, | ||
} = { ...resolvers }; | ||
|
||
const cleanedValue = richTextElement.value.replace(/(\n|\r)+/, ""); | ||
const result = parseHTML(cleanedValue, { | ||
replace: (domNode) => replaceNode(domNode, richTextElement, resolveLinkedItem, resolveImage, resolveLink, resolveDomNode), | ||
}); | ||
|
||
return <>{result}</>; | ||
} | ||
const cleanedValue = richTextElement.value.replace(/(\n|\r)+/, ''); | ||
const result = parseHTML(cleanedValue, { | ||
replace: (domNode) => replaceNode(domNode, richTextElement, resolveLinkedItem, resolveImage, resolveLink, resolveDomNode), | ||
}); | ||
|
||
export { RichTextElement }; | ||
return <>{result}</>; | ||
}; |