diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 7886a79a4..2829daa60 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -1,7 +1,7 @@ import React, { forwardRef } from 'react'; import codemiror, { EditorConfiguration } from 'codemirror'; import { UnControlled as CodeMirror } from 'react-codemirror2'; -import { INDENT_UNIT } from './editorUtils'; +import { INDENT_UNIT, highlightUrlOverlay } from './editorUtils'; import { clsx } from 'clsx'; import { Spin } from 'antd'; @@ -12,25 +12,13 @@ type TCodeEditor = { fullscreen: boolean; keyFoldCode(cm: any): void; handleChange(editor: any, data: any, value: any): void; - onLinksFound(): void; }; type TEditorConfiguration = EditorConfiguration & { foldCode: boolean; }; const CodeEditor = forwardRef( - ( - { - busy, - value, - editable, - fullscreen, - keyFoldCode, - handleChange, - onLinksFound, - }, - ref - ) => { + ({ busy, value, editable, fullscreen, keyFoldCode, handleChange }, ref) => { return ( ( )} onChange={handleChange} editorDidMount={editor => { + highlightUrlOverlay(editor); (ref as React.MutableRefObject).current = editor; }} - onUpdate={onLinksFound} /> ); diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index d78da0b66..ebfa1d05c 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -62,7 +62,7 @@ } .code-mirror-editor { - .fusion-resource-link { + .cm-fusion-resource-link:not(.cm-property) { color: #0974ca !important; cursor: pointer !important; background-color: rgba(#0974ca, 0.12); diff --git a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx index f33982c32..4a17a1a27 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx +++ b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx @@ -24,13 +24,11 @@ document.createRange = () => { describe('ResourceEditor', () => { it('check if code editor will be rendered in the screen', async () => { const editor = React.createRef(); - const onLinksFound = jest.fn(); const { queryByText, container } = render( {}} handleChange={() => {}} diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 4a4d519bb..28448bd43 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -1,6 +1,7 @@ import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { has } from 'lodash'; import isValidUrl, { + isAllowedProtocol, isExternalLink, isStorageLink, isUrlCurieFormat, @@ -46,6 +47,8 @@ type TReturnedResolvedData = Omit< export const LINE_HEIGHT = 15; export const INDENT_UNIT = 4; +export const CODEMIRROR_HOVER_CLASS = 'CodeMirror-hover-tooltip'; +export const CODEMIRROR_LINK_CLASS = 'fusion-resource-link'; const NEAR_BY = [0, 0, 0, 5, 0, -5, 5, 0, -5, 0]; const isDownloadableLink = (resource: Resource) => { return Boolean( @@ -74,6 +77,15 @@ export const getDataExplorerResourceItemArray = ( data._rev, ]) as TDEResource; }; +export const isClickableLine = (url: string) => { + return ( + isValidUrl(url) && + isAllowedProtocol(url) && + !isUrlCurieFormat(url) && + !isStorageLink(url) + ); +}; + export function getTokenAndPosAt(e: MouseEvent, current: CodeMirror.Editor) { const node = e.target || e.srcElement; const text = @@ -86,9 +98,12 @@ export function getTokenAndPosAt(e: MouseEvent, current: CodeMirror.Editor) { }; const pos = current.coordsChar(coords); const token = current.getTokenAt(pos); - if (token && token.string === text) { + const url = token + ? token.string.replace(/\\/g, '').replace(/\"/g, '') + : null; + if (token && url === text) { return { - token, + url, coords: { left: editorRect.left, top: coords.top + LINE_HEIGHT, @@ -101,6 +116,29 @@ export function getTokenAndPosAt(e: MouseEvent, current: CodeMirror.Editor) { coords: { left: editorRect.left, top: e.pageY }, }; } +export const highlightUrlOverlay = (editor: CodeMirror.Editor) => { + editor.addOverlay({ + token: (stream: any, tall: any, call: any) => { + const rxWord = '" '; // Define what separates a word + let ch = stream.peek(); + let word = ''; + // \uE001: end of line + // \uE000: start of line + if (rxWord.includes(ch) || ch === '\uE000' || ch === '\uE001') { + stream.next(); + return null; + } + + while ((ch = stream.peek()) && !rxWord.includes(ch)) { + word += ch; + stream.next(); + } + + if (isClickableLine(word)) return CODEMIRROR_LINK_CLASS; + return; + }, + }); +}; export async function editorLinkResolutionHandler({ nexus, apiEndpoint, diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index e9f0facec..34ed4fe12 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -15,23 +15,12 @@ import 'codemirror/addon/fold/foldcode'; import 'codemirror/addon/fold/foldgutter'; import 'codemirror/addon/fold/brace-fold'; -import isValidUrl, { - isAllowedProtocal, - isStorageLink, - isUrlCurieFormat, -} from '../../../utils/validUrl'; import CodeEditor from './CodeEditor'; import { RootState } from '../../store/reducers'; -import { - useEditorPopover, - useEditorTooltip, - CODEMIRROR_LINK_CLASS, -} from './useEditorTooltip'; +import { useEditorPopover, useEditorTooltip } from './useEditorTooltip'; import { DATA_EXPLORER_GRAPH_FLOW_PATH } from '../../store/reducers/data-explorer'; import ResourceResolutionCache from './ResourcesLRUCache'; import './ResourceEditor.less'; - -const AnchorLinkIcon = require('../../images/AnchorLink.svg'); export interface ResourceEditorProps { rawData: { [key: string]: any }; onSubmit: (rawData: { [key: string]: any }) => void; @@ -53,15 +42,6 @@ export interface ResourceEditorProps { const switchMarginRight = { marginRight: 5 }; -const isClickableLine = (url: string) => { - return ( - isValidUrl(url) && - isAllowedProtocal(url) && - !isUrlCurieFormat(url) && - !isStorageLink(url) - ); -}; - const ResourceEditor: React.FunctionComponent = props => { const { rawData, @@ -96,7 +76,6 @@ const ResourceEditor: React.FunctionComponent = props => { oidc: state.oidc, config: state.config, })); - const userAuthenticated = oidc.user && oidc.user.access_token; const keyFoldCode = (cm: any) => { cm.foldCode(cm.getCursor()); }; @@ -131,16 +110,6 @@ const ResourceEditor: React.FunctionComponent = props => { } onMetadataChange?.(checked); }; - const onLinksFound = () => { - const elements = document.getElementsByClassName('cm-string'); - Array.from(elements).forEach((item, index) => { - const itemSpan = item as HTMLSpanElement; - const url = itemSpan.innerText.replace(/^"|"$/g, ''); - if (isClickableLine(url)) { - itemSpan.classList.add(CODEMIRROR_LINK_CLASS); - } - }); - }; React.useEffect(() => { setEditing(false); @@ -204,11 +173,6 @@ const ResourceEditor: React.FunctionComponent = props => {
{showControlPanel && (
@@ -298,7 +262,6 @@ const ResourceEditor: React.FunctionComponent = props => { editable={editable} handleChange={handleChange} keyFoldCode={keyFoldCode} - onLinksFound={onLinksFound} fullscreen={fullscreen} />
diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 9e06ade0d..693c019cd 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx'; import { useNexusContext } from '@bbp/react-nexus'; import { useSelector } from 'react-redux'; import { + CODEMIRROR_HOVER_CLASS, TEditorPopoverResolvedData, editorLinkResolutionHandler, getTokenAndPosAt, @@ -15,8 +16,6 @@ import useResolutionActions from './useResolutionActions'; const downloadImg = require('../../images/DownloadingLoop.svg'); -export const CODEMIRROR_HOVER_CLASS = 'CodeMirror-hover-tooltip'; -export const CODEMIRROR_LINK_CLASS = 'fusion-resource-link'; type TTooltipCreator = Pick< TEditorPopoverResolvedData, 'error' | 'resolvedAs' | 'results' @@ -226,10 +225,8 @@ function useEditorTooltip({ async function onMouseOver(ev: MouseEvent) { const node = ev.target as HTMLElement; - if (node) { - const { token } = getTokenAndPosAt(ev, currentEditor); - const content = token?.string || ''; - const url = content.replace(/\\/g, '').replace(/\"/g, ''); + if (node && !node.classList.contains('cm-property')) { + const { url } = getTokenAndPosAt(ev, currentEditor); if (url && mayBeResolvableLink(url)) { node.classList.add('wait-for-tooltip'); removeTooltipsFromDOM(); @@ -340,10 +337,8 @@ function useEditorPopover({ async function onMouseDown(_: CodeMirror.Editor, ev: MouseEvent) { removeTooltipsFromDOM(); const node = ev.target as HTMLElement; - if (node) { - const { token } = getTokenAndPosAt(ev, currentEditor); - const content = token?.string || ''; - const url = content.replace(/\\/g, '').replace(/\"/g, ''); + if (node && !node.classList.contains('cm-property')) { + const { url } = getTokenAndPosAt(ev, currentEditor); if (url && mayBeResolvableLink(url)) { editorLinkResolutionHandler({ nexus, diff --git a/src/utils/validUrl.ts b/src/utils/validUrl.ts index cc641c87e..ec59b6913 100644 --- a/src/utils/validUrl.ts +++ b/src/utils/validUrl.ts @@ -34,7 +34,7 @@ function isExternalLink(url: string): boolean { function isStorageLink(url: string): boolean { return url.startsWith('file:///gpfs'); } -function isAllowedProtocal(url: string): boolean { +function isAllowedProtocol(url: string): boolean { return url.startsWith('https://') || url.startsWith('http://'); } @@ -43,6 +43,6 @@ export { isUrlCurieFormat, isExternalLink, isStorageLink, - isAllowedProtocal, + isAllowedProtocol, }; export default isValidUrl;