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: implement generic color capabilities #154

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
8 changes: 5 additions & 3 deletions src/plugins/cursor-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
}
}
Expand Down Expand Up @@ -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) {
Expand Down
85 changes: 85 additions & 0 deletions src/utils/colors.js
Original file line number Diff line number Diff line change
@@ -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)
}
33 changes: 33 additions & 0 deletions tests/y-prosemirror.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}