-
Notifications
You must be signed in to change notification settings - Fork 73
fix: correctly set color and highlight of pasted text #2033
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
base: main
Are you sure you want to change the base?
Changes from all commits
3bcafbe
459e0b0
dd3ee0f
36d4a5b
8611418
7a02659
3cf8860
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /** | ||
| * Converts a CSS color value to hex format (#RRGGBB). | ||
| * Handles rgb(), rgba(), hex, and returns null for empty input, transparent rgba, invalid rgb values. | ||
| * Named colors are returned as-is. | ||
| * | ||
| * @param {string|null|undefined} cssColor - A CSS color string | ||
| * @returns {string|null} Normalized color string or null | ||
| */ | ||
| export function cssColorToHex(cssColor) { | ||
| if (!cssColor) return null; | ||
| const trimmed = cssColor.trim(); | ||
| if (!trimmed) return null; | ||
|
|
||
| // Already hex — pass through | ||
| if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) { | ||
| return trimmed; | ||
| } | ||
|
|
||
| // Parse rgb(r, g, b) or rgba(r, g, b, a) | ||
| const rgbMatch = trimmed.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/); | ||
| if (rgbMatch) { | ||
| const [, r, g, b, a] = rgbMatch; | ||
|
|
||
| if (a !== undefined && parseFloat(a) === 0) return null; | ||
| if (Number(r) > 255 || Number(g) > 255 || Number(b) > 255) return null; | ||
|
|
||
| return '#' + [r, g, b].map((c) => Number(c).toString(16).padStart(2, '0')).join(''); | ||
| } | ||
|
|
||
| // Return as-is for other valid formats (named colors, etc.) | ||
| // Browsers normalize pasted colors to rgb(), so this is a rare fallback | ||
| return trimmed; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| // @ts-nocheck | ||
| import { Mark, Attribute } from '@core/index.js'; | ||
| import { cssColorToHex } from '@core/utilities/cssColorToHex.js'; | ||
|
|
||
| /** | ||
| * Configuration options for Highlight | ||
|
|
@@ -34,7 +35,7 @@ export const Highlight = Mark.create({ | |
| return { | ||
| color: { | ||
| default: null, | ||
| parseDOM: (element) => element.getAttribute('data-color') || element.style.backgroundColor, | ||
| parseDOM: (element) => cssColorToHex(element.getAttribute('data-color') || element.style.backgroundColor), | ||
| renderDOM: (attributes) => { | ||
| if (!attributes.color) { | ||
| return {}; | ||
|
|
@@ -49,7 +50,18 @@ export const Highlight = Mark.create({ | |
| }, | ||
|
|
||
| parseDOM() { | ||
| return [{ tag: 'mark' }]; | ||
| return [ | ||
| { tag: 'mark' }, | ||
| { | ||
| style: 'background-color', | ||
| getAttrs: (value) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the transparent/inherit check here and the Since Either way works -- just pick one place for that logic.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adressed on 7a02659. Chose to keep the logic explictly here so that |
||
| if (!value || value === 'transparent' || value === 'inherit' || value === 'initial' || value === 'unset') | ||
| return false; | ||
| const color = cssColorToHex(value); | ||
| return color ? { color } : false; | ||
| }, | ||
| }, | ||
| ]; | ||
| }, | ||
|
|
||
| renderDOM({ htmlAttributes }) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { test } from '../../fixtures/superdoc.js'; | ||
|
|
||
| test('@behavior pasting html with rgb() background-color applies highlight', async ({ superdoc }) => { | ||
| await superdoc.page.evaluate(() => { | ||
| (window as any).editor.commands.insertContent( | ||
| '<span style="background-color: rgb(255, 255, 0)">Yellow highlighted text</span>', | ||
| ); | ||
| }); | ||
| await superdoc.screenshot('paste-rgb-background-color-highlight'); | ||
| }); | ||
|
|
||
| test('@behavior pasting html with transparent background-color applies no highlight', async ({ superdoc }) => { | ||
| await superdoc.page.evaluate(() => { | ||
| (window as any).editor.commands.insertContent( | ||
| '<span style="background-color: transparent">No highlight text</span>', | ||
| ); | ||
| }); | ||
| await superdoc.screenshot('paste-transparent-background-no-highlight'); | ||
| }); | ||
|
|
||
| test('@behavior pasting html with hex background-color applies highlight', async ({ superdoc }) => { | ||
| await superdoc.page.evaluate(() => { | ||
| (window as any).editor.commands.insertContent( | ||
| '<span style="background-color: #ffff00">Yellow highlighted text</span>', | ||
| ); | ||
| }); | ||
| await superdoc.screenshot('paste-hex-background-color-highlight'); | ||
| }); | ||
|
|
||
| test('@behavior pasting html with rgba zero-alpha background applies no highlight', async ({ superdoc }) => { | ||
| await superdoc.page.evaluate(() => { | ||
| (window as any).editor.commands.insertContent( | ||
| '<span style="background-color: rgba(255, 0, 0, 0)">No highlight text</span>', | ||
| ); | ||
| }); | ||
| await superdoc.screenshot('paste-rgba-zero-alpha-no-highlight'); | ||
| }); | ||
|
|
||
| test('@behavior pasting html with rgb() text color is applied', async ({ superdoc }) => { | ||
| await superdoc.page.evaluate(() => { | ||
| (window as any).editor.commands.insertContent('<span style="color: rgb(255, 0, 0)">Red text</span>'); | ||
| }); | ||
| await superdoc.screenshot('paste-rgb-text-color'); | ||
| }); | ||
|
|
||
| test('@behavior pasting html with hex text color is applied', async ({ superdoc }) => { | ||
| await superdoc.page.evaluate(() => { | ||
| (window as any).editor.commands.insertContent('<span style="color: #ff0000">Red text</span>'); | ||
| }); | ||
| await superdoc.screenshot('paste-hex-text-color'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rgb/rgba parsing is case-sensitive and not anchored to the closing ")", so values like "RGB(255,0,0)" or strings with extra trailing characters won't be converted. Also, channels aren't clamped to 0–255; if a value like "rgb(300,0,0)" is encountered, the generated hex will be longer than 6 digits and invalid. Consider using a case-insensitive, fully-anchored regex and clamping channels to [0,255] before hex conversion.