Sanitize rich-text paste and allow empty business postal codes.
Strip background/font-family/colors on paste while keeping bold, lists, and font size; treat business address postal code as optional. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
eb9b8b9fa3
commit
ac287e02cc
@@ -201,7 +201,7 @@ export function BusinessProfilePage() {
|
||||
: item.province,
|
||||
city: item.city,
|
||||
address: item.address,
|
||||
postalCode: item.postalCode,
|
||||
postalCode: item.postalCode ?? '',
|
||||
landline: item.landline ?? '',
|
||||
}
|
||||
})
|
||||
@@ -332,7 +332,7 @@ export function BusinessProfilePage() {
|
||||
province: item.province.trim(),
|
||||
city: item.city.trim(),
|
||||
address: item.address.trim(),
|
||||
postalCode: item.postalCode.trim(),
|
||||
postalCode: item.postalCode.trim() || undefined,
|
||||
landline: item.landline?.trim() || null,
|
||||
}))
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface BusinessAddress {
|
||||
province: string
|
||||
city: string
|
||||
address: string
|
||||
postalCode: string
|
||||
postalCode: string | null
|
||||
landline: string | null
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface UpdateBusinessProfilePayload {
|
||||
province: string
|
||||
city: string
|
||||
address: string
|
||||
postalCode: string
|
||||
postalCode?: string
|
||||
landline?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -62,6 +62,21 @@
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.toolbar button.codeModeBtn {
|
||||
width: auto;
|
||||
min-width: 28px;
|
||||
padding: 0 8px;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.fontSizeSelect:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.fontSizeWrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -238,6 +253,35 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.codeEditor {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 140px;
|
||||
padding: var(--field-padding-y) var(--field-padding-x);
|
||||
border: 0;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
background: var(--elevated-surface);
|
||||
color: var(--text-primary);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
unicode-bidi: isolate;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.codeEditor::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
.editor > * {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Bold,
|
||||
Code2,
|
||||
IndentDecrease,
|
||||
IndentIncrease,
|
||||
Italic,
|
||||
@@ -60,6 +61,183 @@ function nearestFontSize(rawPx: number): FontSizeValue {
|
||||
const SELECTED_IMAGE_CLASS = 'richTextImageSelected'
|
||||
const HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/
|
||||
|
||||
/** Tags kept on paste (structure + bold). Everything else is unwrapped. */
|
||||
const PASTE_KEEP_TAGS = new Set([
|
||||
'B',
|
||||
'STRONG',
|
||||
'UL',
|
||||
'OL',
|
||||
'LI',
|
||||
'P',
|
||||
'DIV',
|
||||
'BR',
|
||||
'SPAN',
|
||||
])
|
||||
|
||||
const PASTE_DROP_TAGS = new Set([
|
||||
'SCRIPT',
|
||||
'STYLE',
|
||||
'META',
|
||||
'LINK',
|
||||
'TITLE',
|
||||
'XML',
|
||||
'HEAD',
|
||||
'NOSCRIPT',
|
||||
])
|
||||
|
||||
function parseCssFontSizePx(value: string): number | null {
|
||||
const trimmed = value.trim().toLowerCase()
|
||||
if (!trimmed) return null
|
||||
const px = trimmed.match(/^([\d.]+)\s*px$/)
|
||||
if (px) {
|
||||
const n = Number.parseFloat(px[1])
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
const pt = trimmed.match(/^([\d.]+)\s*pt$/)
|
||||
if (pt) {
|
||||
const n = Number.parseFloat(pt[1])
|
||||
// CSS: 1pt ≈ 1.333px at 96dpi
|
||||
return Number.isFinite(n) ? n * (96 / 72) : null
|
||||
}
|
||||
const em = trimmed.match(/^([\d.]+)\s*em$/)
|
||||
if (em) {
|
||||
const n = Number.parseFloat(em[1])
|
||||
return Number.isFinite(n) ? n * 14 : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isCssBold(weight: string): boolean {
|
||||
const w = weight.trim().toLowerCase()
|
||||
if (w === 'bold' || w === 'bolder') return true
|
||||
const n = Number.parseInt(w, 10)
|
||||
return Number.isFinite(n) && n >= 600
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep bold, lists, and font-size on paste; strip bg, font-family, colors, etc.
|
||||
*/
|
||||
function sanitizePastedHtml(html: string): string {
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = html
|
||||
const source = template.content.querySelector('body') ?? template.content
|
||||
const out = document.createDocumentFragment()
|
||||
|
||||
function appendChildren(from: ParentNode, to: ParentNode) {
|
||||
Array.from(from.childNodes).forEach((child) => {
|
||||
const cleaned = cleanNode(child)
|
||||
if (!cleaned) return
|
||||
if (cleaned instanceof DocumentFragment) {
|
||||
to.appendChild(cleaned)
|
||||
} else {
|
||||
to.appendChild(cleaned)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function wrapBold(node: Node): HTMLElement {
|
||||
const strong = document.createElement('strong')
|
||||
strong.appendChild(node)
|
||||
return strong
|
||||
}
|
||||
|
||||
function cleanNode(node: Node): Node | DocumentFragment | null {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return document.createTextNode(node.textContent ?? '')
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return null
|
||||
|
||||
const el = node as HTMLElement
|
||||
const tag = el.tagName.toUpperCase()
|
||||
|
||||
if (PASTE_DROP_TAGS.has(tag)) return null
|
||||
|
||||
// Comments / Word junk like <o:p>
|
||||
if (tag.includes(':')) {
|
||||
const frag = document.createDocumentFragment()
|
||||
appendChildren(el, frag)
|
||||
return frag.childNodes.length ? frag : null
|
||||
}
|
||||
|
||||
const styleBold = isCssBold(el.style.fontWeight)
|
||||
const fontSizePx = parseCssFontSizePx(el.style.fontSize)
|
||||
const snappedSize = fontSizePx != null ? nearestFontSize(fontSizePx) : null
|
||||
|
||||
// <font size="…"> → keep as sized span
|
||||
let fontTagSize: FontSizeValue | null = null
|
||||
if (tag === 'FONT') {
|
||||
const sizeAttr = el.getAttribute('size')
|
||||
// HTML font size 1–7 → approximate px
|
||||
const map: Record<string, FontSizeValue> = {
|
||||
'1': '12px',
|
||||
'2': '12px',
|
||||
'3': '14px',
|
||||
'4': '16px',
|
||||
'5': '20px',
|
||||
'6': '28px',
|
||||
'7': '28px',
|
||||
}
|
||||
if (sizeAttr && map[sizeAttr]) fontTagSize = map[sizeAttr]
|
||||
const faceSize = parseCssFontSizePx(el.style.fontSize)
|
||||
if (faceSize != null) fontTagSize = nearestFontSize(faceSize)
|
||||
}
|
||||
|
||||
const sizeToApply = snappedSize ?? fontTagSize
|
||||
let keepTag: string | null = PASTE_KEEP_TAGS.has(tag) ? tag : null
|
||||
if (tag === 'B') keepTag = 'STRONG'
|
||||
if (tag === 'FONT' && sizeToApply) keepTag = 'SPAN'
|
||||
|
||||
if (keepTag) {
|
||||
const clean = document.createElement(keepTag.toLowerCase())
|
||||
if (sizeToApply) clean.style.fontSize = sizeToApply
|
||||
appendChildren(el, clean)
|
||||
|
||||
// Drop empty style-less spans (e.g. leftover highlight wrappers).
|
||||
if (keepTag === 'SPAN' && !clean.style.cssText && !styleBold) {
|
||||
const frag = document.createDocumentFragment()
|
||||
while (clean.firstChild) frag.appendChild(clean.firstChild)
|
||||
return frag.childNodes.length ? frag : null
|
||||
}
|
||||
|
||||
if (styleBold && keepTag !== 'STRONG') {
|
||||
const strong = document.createElement('strong')
|
||||
while (clean.firstChild) strong.appendChild(clean.firstChild)
|
||||
clean.appendChild(strong)
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
// Unwrap unknown tags, but preserve bold / font-size via wrappers
|
||||
const frag = document.createDocumentFragment()
|
||||
appendChildren(el, frag)
|
||||
|
||||
let result: Node | DocumentFragment = frag
|
||||
if (sizeToApply) {
|
||||
const span = document.createElement('span')
|
||||
span.style.fontSize = sizeToApply
|
||||
span.appendChild(frag)
|
||||
result = span
|
||||
}
|
||||
if (styleBold) {
|
||||
result = wrapBold(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
appendChildren(source, out)
|
||||
|
||||
const holder = document.createElement('div')
|
||||
holder.appendChild(out)
|
||||
// Drop empty Word paragraphs that are only
|
||||
holder.querySelectorAll('p, div').forEach((block) => {
|
||||
const text = (block.textContent ?? '').replace(/\u00a0/g, ' ').trim()
|
||||
if (!text && !block.querySelector('ul, ol, li, br, img')) {
|
||||
block.remove()
|
||||
}
|
||||
})
|
||||
return holder.innerHTML
|
||||
}
|
||||
|
||||
function normalizeHexColor(value: string): string | null {
|
||||
const trimmed = value.trim()
|
||||
const withHash = trimmed.startsWith('#') ? trimmed : `#${trimmed}`
|
||||
@@ -119,6 +297,7 @@ export function RichTextEditor({
|
||||
const [hexInput, setHexInput] = useState(DEFAULT_TEXT_COLOR)
|
||||
const [currentAlign, setCurrentAlign] = useState<TextAlign | null>(null)
|
||||
const [currentFontSize, setCurrentFontSize] = useState<FontSizeValue>(DEFAULT_FONT_SIZE)
|
||||
const [codeMode, setCodeMode] = useState(false)
|
||||
|
||||
const syncChange = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
@@ -148,6 +327,21 @@ export function RichTextEditor({
|
||||
setHandlePos(null)
|
||||
}, [])
|
||||
|
||||
const toggleCodeMode = useCallback(() => {
|
||||
setCodeMode((open) => {
|
||||
const next = !open
|
||||
if (next) {
|
||||
// Flush latest visual edits before showing the HTML source.
|
||||
if (editorRef.current) {
|
||||
onChange(editorRef.current.innerHTML)
|
||||
}
|
||||
clearImageSelection()
|
||||
setColorOpen(false)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [onChange, clearImageSelection])
|
||||
|
||||
const selectImage = useCallback(
|
||||
(img: HTMLImageElement) => {
|
||||
editorRef.current?.querySelectorAll(`img.${SELECTED_IMAGE_CLASS}`).forEach((node) => {
|
||||
@@ -161,11 +355,12 @@ export function RichTextEditor({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (codeMode) return
|
||||
if (editorRef.current && editorRef.current.innerHTML !== value) {
|
||||
editorRef.current.innerHTML = value
|
||||
clearImageSelection()
|
||||
}
|
||||
}, [value, clearImageSelection])
|
||||
}, [value, codeMode, clearImageSelection])
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowImages) return
|
||||
@@ -460,6 +655,43 @@ export function RichTextEditor({
|
||||
changeIndent(e.shiftKey ? -1 : 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste cleaned HTML: keep bold, lists, and font-size;
|
||||
* strip background, font-family, colors, and other text properties.
|
||||
*/
|
||||
function onEditorPaste(e: React.ClipboardEvent<HTMLDivElement>) {
|
||||
e.preventDefault()
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
|
||||
const html = e.clipboardData.getData('text/html')
|
||||
const plain = e.clipboardData.getData('text/plain')
|
||||
const cleaned = html ? sanitizePastedHtml(html) : ''
|
||||
|
||||
editor.focus()
|
||||
|
||||
if (cleaned.trim()) {
|
||||
document.execCommand('insertHTML', false, cleaned)
|
||||
syncChange()
|
||||
return
|
||||
}
|
||||
|
||||
if (!plain) return
|
||||
const inserted = document.execCommand('insertText', false, plain)
|
||||
if (!inserted) {
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0)
|
||||
range.deleteContents()
|
||||
range.insertNode(document.createTextNode(plain))
|
||||
range.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
}
|
||||
syncChange()
|
||||
}
|
||||
|
||||
function styleInsertedImage(img: HTMLImageElement) {
|
||||
img.style.width = '100%'
|
||||
img.style.maxWidth = '100%'
|
||||
@@ -538,6 +770,7 @@ export function RichTextEditor({
|
||||
type="button"
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => exec('bold')}
|
||||
disabled={codeMode}
|
||||
title="Bold"
|
||||
aria-label="Bold"
|
||||
>
|
||||
@@ -547,6 +780,7 @@ export function RichTextEditor({
|
||||
type="button"
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => exec('italic')}
|
||||
disabled={codeMode}
|
||||
title="Italic"
|
||||
aria-label="Italic"
|
||||
>
|
||||
@@ -556,6 +790,7 @@ export function RichTextEditor({
|
||||
type="button"
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => exec('underline')}
|
||||
disabled={codeMode}
|
||||
title="Underline"
|
||||
aria-label="Underline"
|
||||
>
|
||||
@@ -569,6 +804,7 @@ export function RichTextEditor({
|
||||
value={currentFontSize}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onChange={(e) => applyFontSize(e.target.value as FontSizeValue)}
|
||||
disabled={codeMode}
|
||||
title="Font size"
|
||||
aria-label="Font size"
|
||||
>
|
||||
@@ -586,6 +822,7 @@ export function RichTextEditor({
|
||||
className={[styles.colorBtn, colorOpen ? styles.colorBtnActive : ''].filter(Boolean).join(' ')}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={toggleColorPicker}
|
||||
disabled={codeMode}
|
||||
title="Text color"
|
||||
aria-label="Text color"
|
||||
aria-expanded={colorOpen}
|
||||
@@ -594,7 +831,7 @@ export function RichTextEditor({
|
||||
<Palette size={16} />
|
||||
<span className={styles.colorSwatch} style={{ background: textColor }} aria-hidden="true" />
|
||||
</button>
|
||||
{colorOpen && (
|
||||
{colorOpen && !codeMode && (
|
||||
<div className={styles.colorPopover} role="dialog" aria-label="Text color">
|
||||
<input
|
||||
type="color"
|
||||
@@ -627,6 +864,7 @@ export function RichTextEditor({
|
||||
className={styles.dirBtn}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => setBlockDirection('rtl')}
|
||||
disabled={codeMode}
|
||||
title="Right to left"
|
||||
aria-label="Right to left"
|
||||
>
|
||||
@@ -637,6 +875,7 @@ export function RichTextEditor({
|
||||
className={styles.dirBtn}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => setBlockDirection('ltr')}
|
||||
disabled={codeMode}
|
||||
title="Left to right"
|
||||
aria-label="Left to right"
|
||||
>
|
||||
@@ -648,6 +887,7 @@ export function RichTextEditor({
|
||||
className={currentAlign === 'left' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => setBlockAlign('left')}
|
||||
disabled={codeMode}
|
||||
title="Align left"
|
||||
aria-label="Align left"
|
||||
aria-pressed={currentAlign === 'left'}
|
||||
@@ -659,6 +899,7 @@ export function RichTextEditor({
|
||||
className={currentAlign === 'center' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => setBlockAlign('center')}
|
||||
disabled={codeMode}
|
||||
title="Align center"
|
||||
aria-label="Align center"
|
||||
aria-pressed={currentAlign === 'center'}
|
||||
@@ -670,6 +911,7 @@ export function RichTextEditor({
|
||||
className={currentAlign === 'right' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => setBlockAlign('right')}
|
||||
disabled={codeMode}
|
||||
title="Align right"
|
||||
aria-label="Align right"
|
||||
aria-pressed={currentAlign === 'right'}
|
||||
@@ -681,6 +923,7 @@ export function RichTextEditor({
|
||||
className={currentAlign === 'justify' ? styles.toolbarBtnActive : undefined}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => setBlockAlign('justify')}
|
||||
disabled={codeMode}
|
||||
title="Justify"
|
||||
aria-label="Justify"
|
||||
aria-pressed={currentAlign === 'justify'}
|
||||
@@ -688,10 +931,24 @@ export function RichTextEditor({
|
||||
<AlignJustify size={16} />
|
||||
</button>
|
||||
<span className={styles.divider} />
|
||||
<button type="button" onMouseDown={onToolbarMouseDown} onClick={() => exec('insertUnorderedList')} title="Bullet list" aria-label="Bullet list">
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => exec('insertUnorderedList')}
|
||||
disabled={codeMode}
|
||||
title="Bullet list"
|
||||
aria-label="Bullet list"
|
||||
>
|
||||
<List size={16} />
|
||||
</button>
|
||||
<button type="button" onMouseDown={onToolbarMouseDown} onClick={() => exec('insertOrderedList')} title="Numbered list" aria-label="Numbered list">
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => exec('insertOrderedList')}
|
||||
disabled={codeMode}
|
||||
title="Numbered list"
|
||||
aria-label="Numbered list"
|
||||
>
|
||||
<ListOrdered size={16} />
|
||||
</button>
|
||||
<span className={styles.divider} />
|
||||
@@ -699,6 +956,7 @@ export function RichTextEditor({
|
||||
type="button"
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => changeIndent(-1)}
|
||||
disabled={codeMode}
|
||||
title="Decrease indent"
|
||||
aria-label="Decrease indent"
|
||||
>
|
||||
@@ -708,6 +966,7 @@ export function RichTextEditor({
|
||||
type="button"
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={() => changeIndent(1)}
|
||||
disabled={codeMode}
|
||||
title="Increase indent"
|
||||
aria-label="Increase indent"
|
||||
>
|
||||
@@ -719,7 +978,7 @@ export function RichTextEditor({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadingImage}
|
||||
disabled={uploadingImage || codeMode}
|
||||
title="Insert image"
|
||||
aria-label="Insert image"
|
||||
>
|
||||
@@ -734,6 +993,19 @@ export function RichTextEditor({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<span className={styles.divider} />
|
||||
<button
|
||||
type="button"
|
||||
className={[styles.codeModeBtn, codeMode ? styles.toolbarBtnActive : ''].filter(Boolean).join(' ')}
|
||||
onMouseDown={onToolbarMouseDown}
|
||||
onClick={toggleCodeMode}
|
||||
title={codeMode ? 'Visual mode' : 'Code mode'}
|
||||
aria-label={codeMode ? 'Visual mode' : 'Code mode'}
|
||||
aria-pressed={codeMode}
|
||||
>
|
||||
<Code2 size={16} />
|
||||
<span>HTML</span>
|
||||
</button>
|
||||
</div>
|
||||
{uploadError && (
|
||||
<p className={styles.uploadError} role="alert">
|
||||
@@ -741,23 +1013,43 @@ export function RichTextEditor({
|
||||
</p>
|
||||
)}
|
||||
<div ref={editorShellRef} className={styles.editorShell}>
|
||||
{codeMode && (
|
||||
<textarea
|
||||
className={styles.codeEditor}
|
||||
style={{ minHeight: editorMinHeight }}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
dir="ltr"
|
||||
aria-label="HTML source"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={editorRef}
|
||||
className={styles.editor}
|
||||
style={{ minHeight: editorMinHeight }}
|
||||
contentEditable
|
||||
style={{
|
||||
minHeight: editorMinHeight,
|
||||
display: codeMode ? 'none' : undefined,
|
||||
}}
|
||||
contentEditable={!codeMode}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-hidden={codeMode}
|
||||
dir={dir}
|
||||
lang={locale}
|
||||
data-placeholder={placeholder}
|
||||
onInput={syncChange}
|
||||
onKeyDown={onEditorKeyDown}
|
||||
onPaste={onEditorPaste}
|
||||
onKeyUp={refreshAlignFromSelection}
|
||||
onMouseUp={refreshAlignFromSelection}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
{allowImages && selectedImageEl && handlePos && (
|
||||
{!codeMode && allowImages && selectedImageEl && handlePos && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.resizeHandle}
|
||||
|
||||
Reference in New Issue
Block a user