diff --git a/src/plugins/cursor-plugin.js b/src/plugins/cursor-plugin.js index 1bbbdc6..3de0beb 100644 --- a/src/plugins/cursor-plugin.js +++ b/src/plugins/cursor-plugin.js @@ -10,6 +10,7 @@ import { import { yCursorPluginKey, ySyncPluginKey } from './keys.js' import * as math from 'lib0/math' +import { setAlphaChannel } from '../utils/colors.js'; /** * Default awareness state filter @@ -49,8 +50,9 @@ export const defaultCursorBuilder = (user) => { * @return {import('prosemirror-view').DecorationAttrs} */ export const defaultSelectionBuilder = (user) => { + const color = setAlphaChannel(user.color, 0.7); return { - style: `background-color: ${user.color}70`, + style: `background-color: ${color}`, class: 'ProseMirror-yjs-selection' } } @@ -91,8 +93,8 @@ export const createDecorations = ( const user = aw.user || {} if (user.color == null) { user.color = '#ffa500' - } else if (!rxValidColor.test(user.color)) { - // We only support 6-digit RGB colors in y-prosemirror + } else if (!CSS.supports('color', user.color)) { + // We only support CSS colors in y-prosemirror console.warn('A user uses an unsupported color format', user) } if (user.name == null) { diff --git a/src/utils/colors.js b/src/utils/colors.js new file mode 100644 index 0000000..b2a18c1 --- /dev/null +++ b/src/utils/colors.js @@ -0,0 +1,85 @@ +/** + * Detects the type of color format specified in the given string + * @param {string} color A Hex/RGB/RGBA/HSL/HSLA string + * @returns {'HEX'|'RGB'|'HSL'} + */ +export const detectColorFormat = (color) => { + if (color.startsWith('#')) { + return 'HEX' + } else if (color.startsWith('rgb')) { + return 'RGB' + } else if (color.startsWith('hsl')) { + return 'HSL' + } +} + +/** + * Sets the alpha channel of the given color to the specified value + * @param {string} color rgb string + * @param {number} alphaValue Number ranging from 0 to 1 + * @return {string} A new rgba string with the alpha channel set to the `alphaValue` parameter + */ +export const setRgbAlphaChannel = (color, alphaValue) => { + const [r, g, b] = color.match(/\d+/g) + return `rgba(${r}, ${g}, ${b}, ${alphaValue})` +} + +/** + * Sets the alpha channel of the given color to the specified value + * @param {string} color hex string + * @param {number} alphaValue Number ranging from 0 to 1 + * @return {string} A new Hex string with the alpha channel set to the `alphaValue` parameter + */ +export const setHexAlphaChannel = (color, alphaValue) => { + // Remove any leading '#' from the hex color string + color = color.replace(/^#/, '') + + // Check if the hex color has alpha channel + if (color.length === 6) { + // If alpha channel doesn't exist, add it + color += Math.floor(alphaValue * 255).toString(16).padStart(2, '0') + } else if (color.length === 8) { + // If alpha channel exists, replace it with the new alpha value + color = color.slice(0, 6) + Math.floor(alphaValue * 255).toString(16).padStart(2, '0') + } + + return '#' + color +} + +/** + * Sets the alpha channel of the given color to the specified value + * @param {string} color hsl string + * @param {number} alphaValue Number ranging from 0 to 1 + * @return {string} A new hsla string with the alpha channel set to the `alphaValue` parameter + */ +export const setHslAlphaChannel = (color, alphaValue) => { + // Extract components from the HSLA color string + const match = color.match(/hsl[a]?\(\s*(\d+)\s*,\s*(\d+%)\s*,\s*(\d+%)\s*(?:,\s*([\d.]+)\s*)?\)/i) + if (!match) { + return null // Return null if the input string doesn't match HSLA format + } + + const h = parseInt(match[1]) // Hue + const s = parseInt(match[2]) // Saturation + const l = parseInt(match[3]) // Lightness + let a = match[4] ? parseFloat(match[4]) : 1 // Alpha, default to 1 if not provided + + // Update alpha channel with the new value + a = alphaValue + + // Return the updated HSLA color string + return `hsla(${h}, ${s}%, ${l}%, ${a})` +} + +/** + * Sets the Alpha value of Any type of given color string to the specified value + * @param {string} color RGB/Hex/HSL color string + * @param {*} alphaValue Color value in the same format with the alpha set to `alphaValue` + */ +export const setAlphaChannel = (color, alphaValue) => { + const colorFormat = detectColorFormat(color) + + if (colorFormat === 'RGB') return setRgbAlphaChannel(color, alphaValue) + if (colorFormat === 'HSL') return setHslAlphaChannel(color, alphaValue) + if (colorFormat === 'HEX') return setHexAlphaChannel(color, alphaValue) +} diff --git a/tests/y-prosemirror.test.js b/tests/y-prosemirror.test.js index 2eef1ac..04c1faa 100644 --- a/tests/y-prosemirror.test.js +++ b/tests/y-prosemirror.test.js @@ -16,6 +16,7 @@ import { yUndoPlugin, yXmlFragmentToProsemirrorJSON } from '../src/y-prosemirror.js' +import { setRgbAlphaChannel, setHexAlphaChannel, setHslAlphaChannel, setAlphaChannel } from '../src/utils/colors.js' import { EditorState, Plugin, TextSelection } from 'prosemirror-state' import { EditorView } from 'prosemirror-view' import * as basicSchema from 'prosemirror-schema-basic' @@ -508,3 +509,35 @@ export const testRepeatGenerateProsemirrorChanges300 = tc => { checkResult(applyRandomTests(tc, pmChanges, 300, createNewProsemirrorView)) } */ + +/** + * @param {t.TestCase} _tc + */ +export const testColorUtils = _tc => { + const rgb = 'rgb(42,42,42)' + const rgbWithAlpha = setRgbAlphaChannel(rgb, 0.7) + t.compare(rgbWithAlpha, 'rgba(42, 42, 42, 0.7)') + + // even if a rgb string has alpha channel, we should replace with a new value + const rgbWithReplacedAlpha = setRgbAlphaChannel(rgbWithAlpha, 0.5) + t.compare(rgbWithReplacedAlpha, 'rgba(42, 42, 42, 0.5)') + + const hex = '#007bff' + const hexWithAlpha = setHexAlphaChannel(hex, 0.7) + t.compare(hexWithAlpha, '#007bffb2') + + const hexWithReplacedAlpha = setHexAlphaChannel(hexWithAlpha, 0.5) + t.compare(hexWithReplacedAlpha, '#007bff7f') + + const hsl = 'hsl(240, 100%, 50%)' + const hslWithAlpha = setHslAlphaChannel(hsl, 0.7) + t.compare(hslWithAlpha, 'hsla(240, 100%, 50%, 0.7)') + + const hslWithReplacedAlpha = setHslAlphaChannel(hslWithAlpha, 0.5) + t.compare(hslWithReplacedAlpha, 'hsla(240, 100%, 50%, 0.5)') + + // tests for generic function for all three types + t.compare(setAlphaChannel(rgb, 0.7), rgbWithAlpha) + t.compare(setAlphaChannel(hex, 0.7), hexWithAlpha) + t.compare(setAlphaChannel(hsl, 0.7), hslWithAlpha) +}