Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rich-text): support image as links #1158

Merged
merged 5 commits into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,18 @@
top: 35px;
left: 5px;
background: $white;
padding: $spacing-1;
padding: $spacing-2;
border-radius: 5px;
box-shadow: 0px 0px 20px rgba(193, 199, 205, 0.7);

.item {
@include flex-horizontal;
margin-bottom: $spacing-1;
width: 200px;
width: 300px;

label {
@extend %caption;
width: 40px;
width: 50px;
}

.control {
Expand All @@ -136,6 +136,10 @@
padding: 0.25rem $spacing-1;
border: solid 1px $neutral;
border-radius: 0.25rem;

&::placeholder {
color: $neutral;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import { useRef, useState, useContext } from 'react'

import { EditorContext } from '../RichTextEditor'
import styles from '../RichTextEditor.module.scss'

export const ImageBlock = ({
block,
Expand All @@ -24,7 +23,7 @@ export const ImageBlock = ({
const imageRef = useRef<HTMLImageElement>(null)
const [showPopover, setShowPopover] = useState(false)
const entity = contentState.getEntity(block.getEntityAt(0))
const { src, width, height } = entity.getData()
const { src, width, height, link } = entity.getData()

function hidePopover() {
// Do not setState if link is already removed
Expand Down Expand Up @@ -100,8 +99,18 @@ export const ImageBlock = ({
}
}

function renderPreviewImage() {
return link ? (
<a href={link} target="_blank" rel="noopener noreferrer">
<img ref={imageRef} src={src} width={width} height={height} alt="" />
</a>
) : (
<img ref={imageRef} src={src} width={width} height={height} alt="" />
)
}

return readOnly ? (
<img ref={imageRef} src={src} width={width} height={height} alt="" />
renderPreviewImage()
) : (
<span>
<img
Expand All @@ -113,14 +122,22 @@ export const ImageBlock = ({
alt=""
/>
{showPopover && (
<div contentEditable={false} className={styles.popover}>
<div contentEditable={false} className="popover">
<button onClick={getUpdateWidth(50)}>50%</button>
<span className={styles.divider}></span>
<span className="divider"></span>
<button onClick={getUpdateWidth(75)}>75%</button>
<span className={styles.divider}></span>
<span className="divider"></span>
<button onClick={getUpdateWidth(100)}>100%</button>
<span className={styles.divider}></span>
<span className="divider"></span>
<button onClick={handleRemove}>Remove</button>
{link && (
<>
<span className="divider"></span>
<a href={link} target="_blank" rel="noopener noreferrer">
Link <i className="bx bx-link"></i>
</a>
</>
)}
</div>
)}
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import cx from 'classnames'
import { AtomicBlockUtils } from 'draft-js'
import { useContext, useState } from 'react'

import type { FormEvent, MouseEvent as ReactMouseEvent } from 'react'

import { EditorContext } from '../RichTextEditor'

import styles from '../RichTextEditor.module.scss'

const VARIABLE_REGEX = new RegExp(/^{{\s*?\w+\s*?}}$/)

interface ImageControlProps {
currentState: any
expanded: boolean
Expand All @@ -22,14 +24,15 @@ const ImageForm = ({
onChange: (key: string, ...vals: string[]) => void
}) => {
const [imgSrc, setImgSrc] = useState('')
const [link, setLink] = useState('')

function stopPropagation(e: ReactMouseEvent<HTMLElement>) {
e.stopPropagation()
}

function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
onChange(imgSrc, 'auto', '100%', '')
onChange(imgSrc, 'auto', '100%', link)
}

return (
Expand All @@ -39,16 +42,29 @@ const ImageForm = ({
className={styles.form}
>
<div className={styles.item}>
<label>Source</label>
<div className={styles.control}>
<input
value={imgSrc}
type="text"
placeholder="Enter file.go.gov.sg link"
placeholder="file.go.gov.sg link"
onChange={(e) => setImgSrc(e.target.value)}
/>
</div>
</div>

<div className={styles.item}>
<label>Link to</label>
<div className={styles.control}>
<input
value={link}
type="text"
placeholder="Image click links to this URL"
onChange={(e) => setLink(e.target.value)}
/>
</div>
</div>

<div className={styles.submit}>
<button type="submit" disabled={!imgSrc}>
Add
Expand All @@ -59,8 +75,8 @@ const ImageForm = ({
}

export const ImageControl = (props: ImageControlProps) => {
const { expanded, onChange, onExpandEvent } = props
const { editorState } = useContext(EditorContext)
const { expanded, onExpandEvent, doCollapse } = props
const { editorState, setEditorState } = useContext(EditorContext)

function isDisabled(): boolean {
const selection = editorState.getSelection()
Expand All @@ -79,6 +95,41 @@ export const ImageControl = (props: ImageControlProps) => {
if (!isDisabled()) onExpandEvent()
}

function formatLink(link: string): string {
if (VARIABLE_REGEX.test(link)) return link
if (
link &&
!link.startsWith('http://') &&
!link.startsWith('https://') &&
!link.startsWith('mailto:')
) {
return `http://${link}`
}

return link
}

function addImage(
src: string,
height: string | number,
width: string | number,
link: string
): void {
const entityData = { src, height, width, link: formatLink(link) }
const entityKey = editorState
.getCurrentContent()
.createEntity('IMAGE', 'MUTABLE', entityData)
.getLastCreatedEntityKey()
const updated = AtomicBlockUtils.insertAtomicBlock(
editorState,
entityKey,
' '
)

setEditorState(updated)
doCollapse()
}

return (
<div className={styles.imageControl}>
<div
Expand All @@ -89,7 +140,7 @@ export const ImageControl = (props: ImageControlProps) => {
>
<i className="bx bx-image"></i>
</div>
{expanded && <ImageForm onChange={onChange} />}
{expanded && <ImageForm onChange={addImage} />}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ const getAttrStr = (attrs: Record<string, string>): string => {
case 'targetOption':
key = 'target'
break
case 'link':
key = 'data-link'
break
}

return `${attrStr} ${key}="${value}"`
Expand All @@ -237,7 +240,9 @@ const renderTag = (htmlTag: HTMLTag): string => {

switch (tag) {
case 'img':
return `<${tag}${attrStr} />`
return attr.link
? `<a href="${attr.link}"><${tag}${attrStr} /></a>`
: `<${tag}${attrStr} />`
default:
return type === 'open' ? `<${tag}${attrStr}>` : `</${tag}>`
}
Expand Down Expand Up @@ -687,11 +692,13 @@ class ContentBlocksBuilder {
private addImage(node: ChildNode, style: DraftInlineStyle) {
const image = node as HTMLImageElement
const { src, height, width } = image
const link = image.getAttribute('data-link')

this.contentState = this.contentState.createEntity('IMAGE', 'MUTABLE', {
src,
height: height > 0 ? height : 'auto',
width: `${width}%`,
link,
})

this.currentEntity = this.contentState.getLastCreatedEntityKey()
Expand Down
10 changes: 9 additions & 1 deletion modules/postman-templating/src/xss-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,15 @@ export const XSS_EMAIL_OPTION = {
h5: DEFAULT_EMAIL_ATTRS,
h6: DEFAULT_EMAIL_ATTRS,
a: ['href', 'title', 'target', ...DEFAULT_EMAIL_ATTRS],
img: ['src', 'alt', 'title', 'width', 'height', ...DEFAULT_EMAIL_ATTRS],
img: [
'src',
'alt',
'title',
'width',
'height',
'data-link',
...DEFAULT_EMAIL_ATTRS,
],
div: [],
tbody: [],
table: DEFAULT_EMAIL_ATTRS,
Expand Down