diff --git a/package.json b/package.json index 263d6db8..5b556c2c 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "classnames": "^2.5.1", "dayjs": "^1.11.10", "focus-visible": "^5.2.0", + "sanitize-html": "^2.11.0", "styled-components": "^6.1.6", "stylis": "^4.3.1" }, @@ -81,6 +82,7 @@ "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@types/react-router-dom": "^5.3.3", + "@types/sanitize-html": "^2.9.5", "@types/styled-components": "^5.1.34", "@types/testing-library__jest-dom": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.17.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdfcb324..24e4e2e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ dependencies: focus-visible: specifier: ^5.2.0 version: 5.2.0 + sanitize-html: + specifier: ^2.11.0 + version: 2.11.0 styled-components: specifier: ^6.1.6 version: 6.1.6(react-dom@18.2.0)(react@18.2.0) @@ -108,6 +111,9 @@ devDependencies: '@types/react-router-dom': specifier: ^5.3.3 version: 5.3.3 + '@types/sanitize-html': + specifier: ^2.9.5 + version: 2.9.5 '@types/styled-components': specifier: ^5.1.34 version: 5.1.34 @@ -3295,6 +3301,12 @@ packages: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true + /@types/sanitize-html@2.9.5: + resolution: {integrity: sha512-2Sr1vd8Dw+ypsg/oDDfZ57OMSG2Befs+l2CMyCC5bVSK3CpE7lTB2aNlbbWzazgVA+Qqfuholwom6x/mWd1qmw==} + dependencies: + htmlparser2: 8.0.2 + dev: true + /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} dev: true @@ -5215,7 +5227,6 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: true /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} @@ -5324,11 +5335,9 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 - dev: true /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true /domhandler@4.3.1: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} @@ -5342,7 +5351,6 @@ packages: engines: {node: '>= 4'} dependencies: domelementtype: 2.3.0 - dev: true /domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -5358,7 +5366,6 @@ packages: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - dev: true /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -5424,7 +5431,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: true /env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} @@ -5675,7 +5681,6 @@ packages: /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - dev: true /escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} @@ -6752,6 +6757,14 @@ packages: engines: {node: '>=8'} dev: true + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + /http-proxy-agent@7.0.0: resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} engines: {node: '>= 14'} @@ -7107,7 +7120,6 @@ packages: /is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - dev: true /is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -8269,6 +8281,10 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + dev: false + /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -9445,7 +9461,6 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -10086,6 +10101,17 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true + /sanitize-html@2.11.0: + resolution: {integrity: sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==} + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.32 + dev: false + /sass@1.69.7: resolution: {integrity: sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==} engines: {node: '>=14.0.0'} diff --git a/src/components/GlobalContext.tsx b/src/components/GlobalContext.tsx index 00f825ed..de7ea92b 100644 --- a/src/components/GlobalContext.tsx +++ b/src/components/GlobalContext.tsx @@ -80,6 +80,7 @@ const GlobalContextProvider: FunctionComponent> = ( lineWidth: 3, mediaHeight: 200, nestedCardHeight: 150, + parseDetailsTextHTML: false, scrollable: { scrollbar: false, }, diff --git a/src/components/timeline-elements/timeline-card-content/text-or-content.tsx b/src/components/timeline-elements/timeline-card-content/text-or-content.tsx index 8e314643..f08fde87 100644 --- a/src/components/timeline-elements/timeline-card-content/text-or-content.tsx +++ b/src/components/timeline-elements/timeline-card-content/text-or-content.tsx @@ -1,9 +1,10 @@ import { TimelineContentModel } from '@models/TimelineContentModel'; import { ForwardRefExoticComponent, forwardRef, useContext } from 'react'; +import sanitizeHtml from 'sanitize-html'; import { GlobalContext } from '../../GlobalContext'; import { - TimelineSubContent, TimelineContentDetails, + TimelineSubContent, } from './timeline-card-content.styles'; export type TextOrContentModel = Pick< @@ -26,34 +27,69 @@ const getTextOrContent: ( // const { timelineContent, theme, detailedText, showMore } = prop; const isTextArray = Array.isArray(detailedText); - const { fontSizes, classNames } = useContext(GlobalContext); + const { fontSizes, classNames, parseDetailsTextHTML } = + useContext(GlobalContext); if (timelineContent) { return
{timelineContent}
; } else { let textContent = null; if (isTextArray) { - textContent = (detailedText as string[]).map((text, index) => ( - - {text} - - )); + textContent = (detailedText as string[]).map((text, index) => { + const props = parseDetailsTextHTML + ? { + dangerouslySetInnerHTML: { + __html: sanitizeHtml(text, { + parseStyleAttributes: true, + }), + }, + } + : null; + console.log(props); + return ( + + {parseDetailsTextHTML ? null : text} + + ); + }); } else { - textContent = detailedText; + textContent = parseDetailsTextHTML + ? sanitizeHtml(detailedText, { + parseStyleAttributes: true, + }) + : detailedText; } + const textContentProps = + parseDetailsTextHTML && !isTextArray + ? { + dangerouslySetInnerHTML: { + __html: sanitizeHtml(textContent, { + parseStyleAttributes: true, + }), + }, + } + : {}; + return textContent ? ( - {textContent} + {parseDetailsTextHTML && !isTextArray ? null : textContent} ) : null; } diff --git a/src/demo/data.tsx b/src/demo/data.tsx index 1d2bebae..b2ce569a 100644 --- a/src/demo/data.tsx +++ b/src/demo/data.tsx @@ -18,8 +18,15 @@ const items: TimelineItemModel[] = [ 'Men of the British Expeditionary Force (BEF) wade out to a destroyer during the evacuation from Dunkirk.', // cardDetailedText: [`On 10 May 1940, Hitler began his long-awaited offensive in the west by invading neutral Holland and Belgium and attacking northern France.`, `Holland capitulated after only five days of fighting, and the Belgians surrendered on 28 May. With the success of the German ‘Blitzkrieg’, the British Expeditionary Force and French troops were in danger of being cut off and destroyed.`], cardDetailedText: [ - `On 10 May 1940, Hitler began his long-awaited offensive in the west by invading neutral Holland and Belgium and attacking northern France.`, + `On 10 May 1940, Hitler began his long-awaited offensive in the west by invading neutral Holland and Belgium and attacking northern France. +
`, + ` + `, ], + // cardDetailedText: `Holland capitulated the after only five days of fighting, and the Belgians surrendered on 28 May.With the success of the German ‘Blitzkrieg’, the British Expeditionary Force and French troops were in danger of being cut off and destroyed.`, }, { title: '25 July 1941', @@ -32,6 +39,7 @@ const items: TimelineItemModel[] = [ }, type: 'IMAGE', }, + // items: [ // { // cardTitle: 'The Battle of Britian Yaar', diff --git a/src/demo/vertical-samples.tsx b/src/demo/vertical-samples.tsx index c3eca0d9..4f63040a 100644 --- a/src/demo/vertical-samples.tsx +++ b/src/demo/vertical-samples.tsx @@ -128,6 +128,7 @@ export const VerticalBasic: FunctionComponent<{ noUniqueId disableTimelinePoint uniqueID="vertical_basic_test" + parseDetailsTextHTML // textOverlay // borderLessCards // theme={{ diff --git a/src/models/TimelineMediaModel.ts b/src/models/TimelineMediaModel.ts index a7cb558b..85ea98e4 100644 --- a/src/models/TimelineMediaModel.ts +++ b/src/models/TimelineMediaModel.ts @@ -60,6 +60,7 @@ export interface CardMediaModel { content?: string | ReactNode; // Text details associated with the media. + // detailsText?: ForwardRefExoticComponent; detailsText?: ForwardRefExoticComponent; // Indicates if media should be hidden. diff --git a/src/models/TimelineModel.ts b/src/models/TimelineModel.ts index e84ea714..7d0697ff 100644 --- a/src/models/TimelineModel.ts +++ b/src/models/TimelineModel.ts @@ -172,6 +172,8 @@ export type TimelineProps = { onThemeChange?: () => void; + parseDetailsTextHTML?: boolean; + // option to enable scrollbar scrollable?: boolean | { scrollbar: boolean }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 688b538d..1de8dd4a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ import { SlideShowType, TimelineMode } from '@models/TimelineModel'; import { darkTheme, defaultTheme } from '../components/common/themes'; +import santizeHtml from 'sanitize-html'; export const uniqueID = () => { const chars = @@ -65,3 +66,14 @@ export const getSlideShowType: (mode: TimelineMode) => SlideShowType = ( return 'reveal'; }; + +export const isTextArray = (text: string | string[]): text is string[] => { + return Array.isArray(text); +} + +export const sanitizeHtmlText = (text: string | string[]) => { + if (isTextArray(text)) { + return text.map((t) => santizeHtml(t)); + } + return santizeHtml(text); +}