Skip to content

Commit

Permalink
fix: Improve the algorithm of link detection and html insertion to re…
Browse files Browse the repository at this point in the history
…move bugs - refs 282898

* Drop dangerouslySetInnerHTML

* Improve FootnotesBlockView.jsx

* Update FootnotesBlockView.jsx

* Update FootnotesBlockView.jsx

* Create FootnotesBlockView.test.jsx

* Update FootnotesBlockView.test.jsx

* Update FootnotesBlockView.jsx to check if on the Client Side

* Update render.jsx to improve link detection

* Update render.jsx

* Update utils.js

* Update FootnotesBlockView.jsx

* Update FootnotesBlockView.test.jsx

* tests

* tests

* fix refresh of page when clicking on a ref

* cypress test

* cypress test

* unit test

* make links clickable

* make links clickable
  • Loading branch information
dobri1408 authored Jan 30, 2025
1 parent 68bd826 commit 4e03b60
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 92 deletions.
48 changes: 3 additions & 45 deletions src/Blocks/Footnote/FootnotesBlockView.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { escapeRegExp } from 'lodash';

import {
openAccordionOrTabIfContainsFootnoteReference,
getAllBlocksAndSlateFields,
Expand All @@ -8,10 +8,9 @@ import {
import './less/public.less';

import { UniversalLink } from '@plone/volto/components';
import { renderTextWithLinks } from '../../editor/utils';

const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const urlRegex =
/\b((http|https|ftp):\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?\b/g;

/**
* @summary The React component that displays the list of footnotes inserted
Expand Down Expand Up @@ -78,47 +77,6 @@ const FootnotesBlockView = (props) => {
startList = citationIndice;
}

const renderTextWithLinks = (text) => {
if (!text) return null;
const links = text.match(urlRegex);
if (!links) {
return (
<div
dangerouslySetInnerHTML={{
__html: text,
}}
/>
);
}
let result = [];
const parts = text.split(
new RegExp(`(${links.map((link) => escapeRegExp(link)).join('|')})`),
);
parts.forEach((part, index) => {
if (links.includes(part)) {
result.push(
<UniversalLink
key={`link-${index}`}
href={part}
openLinkInNewTab={false}
>
{part}
</UniversalLink>,
);
return;
}

result.push(
<span
dangerouslySetInnerHTML={{
__html: part,
}}
/>,
);
});

return <div>{result}</div>;
};
return (
<div className="footnotes-listing-block">
<h3 title={placeholder}>{title}</h3>
Expand All @@ -141,7 +99,7 @@ const FootnotesBlockView = (props) => {
key={`footnote-${zoteroId || uid}`}
id={`footnote-${zoteroId || uid}`}
>
<div>{renderTextWithLinks(footnoteText)}</div>
<div>{renderTextWithLinks(footnoteText, zoteroId)}</div>
{refsList ? (
<>
{/** some footnotes are never parent so we need the parent to reference */}
Expand Down
84 changes: 84 additions & 0 deletions src/Blocks/Footnote/FootnotesBlockView.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import FootnotesBlockView from './FootnotesBlockView';

jest.mock('@plone/volto/components', () => ({
UniversalLink: ({ children, href }) => <a href={href}>{children}</a>,
}));

jest.mock('@eeacms/volto-slate-footnote/editor/utils', () => ({
openAccordionOrTabIfContainsFootnoteReference: jest.fn(),
renderTextWithLinks: jest.fn(),
getAllBlocksAndSlateFields: jest.fn(() => [
{ id: 'block1', footnote: 'Footnote with no link' },
{ id: 'block2', footnote: 'Footnote with link http://example.com' },
{ id: 'block3', footnote: 'Footnote with <b>HTML</b>' },
]),
makeFootnoteListOfUniqueItems: jest.fn((blocks) => ({
note1: {
uid: '1',
footnote: 'First note with a reference',
zoteroId: 'zotero1',
refs: { ref1: 'ref1' },
},
note2: {
uid: '2',
footnote: 'Second note with multiple references',
zoteroId: null,
refs: { ref2: 'ref2', ref3: 'ref3' },
},
note3: {
uid: '3',
footnote: '<i>Note with HTML</i>',
zoteroId: 'zotero3',
refs: {},
},
})),
}));

describe('FootnotesBlockView', () => {
const propsVariations = [
{
description: 'renders with global metadata',
props: {
data: { title: 'Global Metadata', global: true, placeholder: 'Global' },
properties: { test: 'metadata' },
tabData: null,
content: null,
metadata: { test: 'metadata' },
},
},
{
description: 'renders with tabData',
props: {
data: { title: 'Tab Data', global: false, placeholder: 'Tab' },
properties: { test: 'tabProperties' },
tabData: { test: 'tabData' },
content: null,
},
},
{
description: 'renders with content',
props: {
data: { title: 'Content Data', global: false, placeholder: 'Content' },
properties: { test: 'contentProperties' },
tabData: null,
content: { test: 'contentData' },
},
},
{
description: 'renders with no metadata',
props: {
data: { title: 'No Metadata', global: false, placeholder: 'Default' },
properties: { test: 'defaultProperties' },
tabData: null,
content: null,
},
},
];

test.each(propsVariations)('$description', ({ props }) => {
render(<FootnotesBlockView {...props} />);
});
});
83 changes: 37 additions & 46 deletions src/editor/render.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import React from 'react';
import { Popup, List } from 'semantic-ui-react';
import { useEditorContext } from '@plone/volto-slate/hooks';

import { getAllBlocksAndSlateFields } from '@eeacms/volto-slate-footnote/editor/utils';
import {
makeFootnoteListOfUniqueItems,
openAccordionOrTabIfContainsFootnoteReference,
} from './utils';
import { isEmpty } from 'lodash';
import { useSelector } from 'react-redux';
import { UniversalLink } from '@plone/volto/components';

import { renderTextWithLinks } from './utils';
import { useHistory } from 'react-router-dom';

/**
* Removes '<?xml version="1.0"?>' from footnote
* @param {string} footnote
* @returns {string} formatted footnote
*/

const urlRegex = /https?:\/\/[^\s]+/g;

export const FootnoteElement = (props) => {
const { attributes, children, element, mode, extras } = props;
const { data = {} } = element;
const { uid, zoteroId } = data;
const editor = useEditorContext();
const ref = React.useRef();
const history = useHistory();

const initialFormData = useSelector((state) => state?.content?.data || {});
const blockProps = editor?.getBlockProps ? editor.getBlockProps() : null;
Expand Down Expand Up @@ -51,35 +53,6 @@ export const FootnoteElement = (props) => {
: // no extra citations (no multiples)
`[${Object.keys(notesObjResult).indexOf(zoteroId) + 1}]`;

const renderTextWithLinks = (text) => {
if (!text) return null;
const parts = text.split(urlRegex);
const links = text.match(urlRegex);
let result = [];

parts.forEach((part, index) => {
result.push(
<div
dangerouslySetInnerHTML={{
__html: part,
}}
/>,
);

if (links && links[index]) {
result.push(
<UniversalLink
key={`link-${index}`}
href={links[index]}
openLinkInNewTab={false}
>
{links[index]}
</UniversalLink>,
);
}
});
return result;
};
const citationIndice = zoteroId // ZOTERO
? indiceIfZoteroId
: // FOOTNOTES
Expand Down Expand Up @@ -142,16 +115,20 @@ export const FootnoteElement = (props) => {
<List divided relaxed selection>
<List.Item
href={`#footnote-${citationRefId}`}
onClick={() =>
onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${citationRefId}`,
)
}
);
if (e.target.tagName !== 'A') {
e.preventDefault();
history.push(`#footnote-${citationRefId}`);
}
}}
key={`#footnote-${citationRefId}`}
>
<List.Content>
<List.Description>
{renderTextWithLinks(footnoteText)}
{renderTextWithLinks(footnoteText, zoteroId)}
</List.Description>
</List.Content>
</List.Item>
Expand All @@ -164,16 +141,22 @@ export const FootnoteElement = (props) => {
return (
<List.Item
href={`#footnote-${item.zoteroId || item.uid}`}
onClick={() =>
onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${item.zoteroId || item.uid}`,
)
}
);
if (e.target.tagName !== 'A') {
e.preventDefault();
history.push(
`#footnote-${item.zoteroId || item.uid}`,
);
}
}}
key={`#footnote-${item.zoteroId || item.uid}`}
>
<List.Content>
<List.Description>
{renderTextWithLinks(footnoteText)}
{renderTextWithLinks(footnoteText, item.zoteroId)}
</List.Description>
</List.Content>
</List.Item>
Expand Down Expand Up @@ -202,11 +185,15 @@ export const FootnoteElement = (props) => {
<List divided relaxed selection>
<List.Item
href={`#footnote-${citationRefId}`}
onClick={() =>
onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${citationRefId}`,
)
}
);
if (e.target.tagName !== 'A') {
e.preventDefault();
history.push(`#footnote-${citationRefId}`);
}
}}
key={`#footnote-${citationRefId}`}
>
<List.Content>
Expand All @@ -219,11 +206,15 @@ export const FootnoteElement = (props) => {
data.extra.map((item) => (
<List.Item
href={`#footnote-${item.zoteroId || item.uid}`}
onClick={() =>
onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${item.zoteroId || item.uid}`,
)
}
);
if (e.target.tagName !== 'A') {
e.preventDefault();
history.push(`#footnote-${citationRefId}`);
}
}}
key={`#footnote-${item.zoteroId || item.uid}`}
>
<List.Content>
Expand Down
Loading

0 comments on commit 4e03b60

Please sign in to comment.