tiptap icon indicating copy to clipboard operation
tiptap copied to clipboard

Text color seems to be automatically converted to RGB

Open sereneinserenade opened this issue 4 years ago • 26 comments

Description It seems like the colors that should be in hex, are automatically transformed to rgb and therefore the check for the color is failing.

  • The video

https://user-images.githubusercontent.com/45892659/131475139-2cbc0921-af20-40ca-9acc-75f48340f178.mov

  • Rendered HTML

image

  • Editor Content String from Vue component of the demo

image

Steps to reproduce the bug Steps to reproduce the behavior:

  1. Go to https://www.tiptap.dev/api/extensions/color
  2. Set color for some text to any color you like
  3. See the color input in the menubar and you will notice that the color from the text that you changed will get recognized by input, but the color which was already there, i.e. purple(#958DF1) gets converted to RGB

Expected behavior The color shouldn't be converted to rgb so that we have consistent behaviour

Environment?

  • operating system: MacOs 11
  • browser: Chrome
  • mobile/desktop: Desktop
  • tiptap version: Whichever it is used in the demo website(https://www.tiptap.dev/api/extensions/color) on 31.08.2021

sereneinserenade avatar Aug 31 '21 09:08 sereneinserenade

I think that’s a browser thing, but I’ll check it.

hanspagel avatar Aug 31 '21 10:08 hanspagel

Yep, it's a browser thing!

The computed value is the corresponding rgb() or rgba() value:

https://developer.mozilla.org/en-US/docs/Web/CSS/color#formal_definition

const div = document.createElement('div');
div.style.color = '#958DF1';
console.log(div.style.color);
// rgb(149, 141, 241)

razh avatar Aug 31 '21 20:08 razh

Three things we could do:

  1. Recommend to use rgb
  2. Use rgb in the example
  3. Make isActive smart enough to convert it and check for both

I think we go with 1 and 2, not sure about 3.

Anyway, thanks for reporting!

hanspagel avatar Aug 31 '21 21:08 hanspagel

I fiddled around a bit. A funny side node: the native HTML color picker only works with hex colors 🙃

There are some options now:

  1. we only support HEX or RGB and convert between them to a specific format (defaults to HEX I think). It’s easy to convert between these two formats to this could happen within the color extension package.
  2. we support any color input (like HSL, LAB, ...) and convert it to a specific format. This requires an external library.
  3. we prevent any color conversion (so that hex colors are not converted to RGB for example). So everything is up to the user. This may require the user to convert colors somehow before/after.

What do you think?

philippkuehn avatar Sep 16 '21 10:09 philippkuehn

+1 for 1

we only support HEX or RGB and convert between them to a specific format (defaults to HEX I think). It’s easy to convert between these two formats to this could happen within the color extension package.

hanspagel avatar Sep 16 '21 14:09 hanspagel

+1 for 1. same as @hanspagel , also would love to contribute

sereneinserenade avatar Sep 16 '21 14:09 sereneinserenade

What about cases where alpha channel is set (rgba() and #rrggbbaa)? Chrome seems to convert #rrggbbaa to rgba():

const div = document.createElement('div');
div.style.color = '#958DF100';
console.log(div.style.color);
// rgba(149, 141, 241, 0)

I also agree with the 1 option – it would cover most use cases out of the box and if the user wants to use a different color space, they would simply need to convert color to RGB or HEX.

domnantas avatar Sep 16 '21 16:09 domnantas

yeah with HEX/RGB I meant HEX/RGB/RGBA :)

lets’s do option 1 then!

philippkuehn avatar Sep 16 '21 16:09 philippkuehn

Seems like that not every value will be converted by the browser :(

const div = document.createElement('div');
div.style.color = '#958DF100';
console.log(div.style.color);
// rgba(149, 141, 241, 0)
const div = document.createElement('div');
div.style.color = 'red';
console.log(div.style.color);
// red

So option 1 is not enough I think.

philippkuehn avatar Sep 19 '21 18:09 philippkuehn

Ok some more thoughts.

color2k seems like a great small library and good fit for our needs.

But converting always to Hex will cause other issues.

  1. isActive will fail for other color formats than Hex Should we add an extension option convertToHex to opt-out?
editor.commands.setColor('rgb(0,0,0)')
editor.isActive('textStyle', { color: 'rgb(0,0,0)' }) // false -> would expect this returns true
editor.isActive('textStyle', { color: '#000000' }) // true
  1. what about the highlight extension? There is a multicolor option which allows to use any color. Should we convert them to Hex too?

philippkuehn avatar Sep 19 '21 19:09 philippkuehn

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jul 06 '22 23:07 stale[bot]

Issue still relevant

domnantas avatar Jul 07 '22 06:07 domnantas

Please reopen this, this issue is still relevant

domnantas avatar Jul 17 '22 19:07 domnantas

@philippkuehn Sorry for tagging :/ Please reopen this issue, I'm currently struggling with this

edit: My workaround for now

function convertRgbColorsToHex(string) {
  return string.replace(/rgb\(\d+,\s*\d+,\s*\d+\)/g,
    (rgbString) => "#" + rgbString
      .match(/\b(\d+)\b/g)
      .map((digit) =>
        parseInt(digit).toString(16).padStart(2, "0").toUpperCase()
       )
      .join("")
    )
}

thieleju avatar Aug 03 '22 07:08 thieleju

Currently having the same issue, @thieleju kind of fixes it in converting it but still seeing the error happening.

mattvb91 avatar Nov 05 '22 16:11 mattvb91

Same issue here.

drezr avatar Dec 11 '22 12:12 drezr

Same issue here.

Jhoydev avatar Feb 24 '23 11:02 Jhoydev

I put it back into our 2.0.0 Release Milestone.

bdbch avatar Feb 24 '23 14:02 bdbch

I just ran into this same issue. This issue also occurs when you are setting content on update for the editor. all of your styles will get parsed into rgb for hex styles. Hope the next update fixes this,

https://github.com/ueberdosis/tiptap/issues/4167

JahnoelRondon avatar Jun 27 '23 19:06 JahnoelRondon

Same issue here.

tsl1127 avatar Oct 18 '23 08:10 tsl1127

Made an extension to it, also added support for backgroundColor, hope this will help:

import Color from '@tiptap/extension-color'

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    backgroundColor: {
      setBgColor: (backgroundColor: string) => ReturnType,
      unsetBgColor: () => ReturnType,
    }
  }
}

// rgbStyleToHex
// from https://stackoverflow.com/a/3627747/5433572
// and https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
function rgbStyleToHex(color: string | null): string | null {
  if (!color || color.indexOf("rgb") < 0) {
    return color
  }

  if (color.charAt(0) == "#") {
    return color
  }

  const nums = /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/i.exec(color),
    r = parseInt(nums![2], 10).toString(16),
    g = parseInt(nums![3], 10).toString(16),
    b = parseInt(nums![4], 10).toString(16)

  return ("#"+ ((r.length == 1 ? "0"+ r : r) + (g.length == 1 ? "0"+ g : g) + (b.length == 1 ? "0"+ b : b))).toUpperCase()
}

export const MyColor = Color.extend({
  addGlobalAttributes() {
    return [
      {
        types: this.options.types,
        attributes: {
          color: {
            default: null,
            parseHTML: element => element.style.color?.replace(/['"]+/g, ''),
            renderHTML: attributes => {
              if (!attributes.color) {
                return {}
              }

              return {
                style: `color: ${rgbStyleToHex(attributes.color)}`,
              }
            },
          },
          backgroundColor: {
            default: null,
            parseHTML: element => element.style.backgroundColor?.replace(/['"]+/g, ''),
            renderHTML: attributes => {
              if (!attributes.backgroundColor) {
                return {}
              }

              return {
                style: `background-color: ${rgbStyleToHex(attributes.backgroundColor)}`,
              }
            },
          },
        },
      },
    ]
  },

  addCommands() {
    return {
      ...this.parent?.(),
      setBgColor: backgroundColor => ({ chain }) => {
        return chain().setMark('textStyle', { backgroundColor }).run()
      },
      unsetBgColor: () => ({ chain }) => {
        return chain().setMark('textStyle', { backgroundColor: null }).removeEmptyTextStyle().run()
      },
    }
  },

})

eastecho avatar Oct 20 '23 12:10 eastecho

Made an extension to it, also added support for backgroundColor, hope this will help:

import Color from '@tiptap/extension-color'

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    backgroundColor: {
      setBgColor: (backgroundColor: string) => ReturnType,
      unsetBgColor: () => ReturnType,
    }
  }
}

// rgbStyleToHex
// from https://stackoverflow.com/a/3627747/5433572
// and https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
function rgbStyleToHex(color: string | null): string | null {
  if (!color || color.indexOf("rgb") < 0) {
    return color
  }

  if (color.charAt(0) == "#") {
    return color
  }

  const nums = /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/i.exec(color),
    r = parseInt(nums![2], 10).toString(16),
    g = parseInt(nums![3], 10).toString(16),
    b = parseInt(nums![4], 10).toString(16)

  return ("#"+ ((r.length == 1 ? "0"+ r : r) + (g.length == 1 ? "0"+ g : g) + (b.length == 1 ? "0"+ b : b))).toUpperCase()
}

export const MyColor = Color.extend({
  addGlobalAttributes() {
    return [
      {
        types: this.options.types,
        attributes: {
          color: {
            default: null,
            parseHTML: element => element.style.color?.replace(/['"]+/g, ''),
            renderHTML: attributes => {
              if (!attributes.color) {
                return {}
              }

              return {
                style: `color: ${rgbStyleToHex(attributes.color)}`,
              }
            },
          },
          backgroundColor: {
            default: null,
            parseHTML: element => element.style.backgroundColor?.replace(/['"]+/g, ''),
            renderHTML: attributes => {
              if (!attributes.backgroundColor) {
                return {}
              }

              return {
                style: `background-color: ${rgbStyleToHex(attributes.backgroundColor)}`,
              }
            },
          },
        },
      },
    ]
  },

  addCommands() {
    return {
      ...this.parent?.(),
      setBgColor: backgroundColor => ({ chain }) => {
        return chain().setMark('textStyle', { backgroundColor }).run()
      },
      unsetBgColor: () => ({ chain }) => {
        return chain().setMark('textStyle', { backgroundColor: null }).removeEmptyTextStyle().run()
      },
    }
  },

})

In parseHTML you must do this to make it work:

// for color:
rgbStyleToHex(element.style.color?.replace(/['"]+/g, ''))

// for backgroundColor
rgbStyleToHex(element.style.backgroundColor?.replace(/['"]+/g, ''))

Ehsan200 avatar Nov 29 '23 06:11 Ehsan200

That works, thanks!

EskelCz avatar Jul 03 '24 18:07 EskelCz

So we need to parse it to Hex, s***t!

SalahAdDin avatar Aug 19 '24 07:08 SalahAdDin

I'd be happy to take a PR for this to be included as part of the color extension. Should probably warn on any other color values set

nperez0111 avatar Aug 19 '24 07:08 nperez0111

I just checked it, first it gets the color in rgba, and later it gets the color in hex, why?

SalahAdDin avatar Aug 20 '24 04:08 SalahAdDin

I just upgraded to v3 and this issue is now inside of TextStyleKit - I had to remove the previous color extension per the upgrade guide. The code however just throws warnings, and my color selection (and the entire editor) otherwise works fine, so for now I am just ignoring it. I am using the default browser input (i.e. <input type="color" ...> and have only tested in Brave Browser)

OliverGrimsley avatar Jul 14 '25 21:07 OliverGrimsley

I just upgraded to v3 and this issue is now inside of TextStyleKit - I had to remove the previous color extension per the upgrade guide. The code however just throws warnings, and my color selection (and the entire editor) otherwise works fine, so for now I am just ignoring it. I am using the default browser input (i.e. <input type="color" ...> and have only tested in Brave Browser)

But the issue where fixed, right?

I would be good to open issues with the warnings.

SalahAdDin avatar Jul 15 '25 00:07 SalahAdDin

@OliverGrimsley what warnings are thrown?

bdbch avatar Jul 15 '25 10:07 bdbch

@OliverGrimsley what warnings are thrown?

The console warning:

The specified value "" does not conform to the required format.  The format is "#rrggbb" where rr, gg, bb are two-digit hexadecimal numbers.

This is a Vue App, using Vite (fully current version), and my package json has:

    "@tiptap/core": "^3.0.1",
    "@tiptap/extension-bullet-list": "^3.0.1",
    "@tiptap/extension-color": "^3.0.1",
    "@tiptap/extension-highlight": "^3.0.1",
    "@tiptap/extension-link": "^3.0.1",
    "@tiptap/extension-list-item": "^3.0.1",
    "@tiptap/extension-text-align": "^3.0.1",
    "@tiptap/extension-text-style": "^3.0.1",
    "@tiptap/pm": "^3.0.1",
    "@tiptap/starter-kit": "^3.0.1",
    "@tiptap/vue-3": "^3.0.1",

The actual code where the button is located:

<input
        class="rounded-md border border-slate-400 bg-slate-300 h-6 w-6"
        type="color"
        @input="editor.chain().focus().setColor($event.target.value).run()"
        :value="editor.getAttributes('textStyle').color"
/>
<button class="rounded-sm border border-slate-400 bg-green-300 dark:bg-slate-500 h-6 px-1" @click="editor.chain().focus().unsetColor().run()">
        Unset color
 </button>

The editor works just fine, despite the warnings

OliverGrimsley avatar Jul 15 '25 12:07 OliverGrimsley