basecamp / trix

A rich text editor for everyday writing
https://trix-editor.org/
MIT License
18.94k stars 1.11k forks source link

A color picker 100% functional ! #985

Open silva96 opened 2 years ago

silva96 commented 2 years ago

EDIT: if you are using Trix 2.0+ read the tutorial but instead of using this stimulus_controller, please use the stimulus_controller here: https://github.com/basecamp/trix/issues/985#issuecomment-1307422240

After a good time of reading issues and thinking I finally could create a fully functional color picker for Trix

This also works when reloading the editor page because I call reset after the config.

I also added text-align center icon to center blocks

trix2

For this to work, just wrap your rich_text_area in a div with data-controller="trix" and add a data: { trix_target: 'editor' } to the rich_text_area

<div class="control" data-controller="trix">
        <%= f.rich_text_area :description, placeholder: t('products.edit.description_placeholder'),
                              data: { trix_target: 'editor' } %>
</div>

Also add a stimulus controller called trix-controller.js and then update the stimulus manifest

update the Stimulus manifest with ./bin/rails stimulus:manifest:update

import { Controller } from '@hotwired/stimulus'
import Trix from 'trix'

// Connects to data-controller="trix"
export default class extends Controller {
  static targets = ['editor', 'foregroundColorPicker', 'backgroundColorPicker']

  connect () {
    this.initTrix()
    this.reloadOriginalContent()
  }

  initTrix () {
    if (this.hasForegroundColorPickerTarget) { return }

    Trix.config.blockAttributes.heading1.tagName = 'h3'
    this.addForegroundButtonInToolbar()
    this.addBackgroundButtonInToolbar()
    this.addTextAlignCenterButtonInToolbar()
  }

  reloadOriginalContent () {
    this.editorTarget.reset()
  }

  openForegroundColorPicker () {
    this.foregroundColorPickerTarget.click()
  }

  openBackgroundColorPicker () {
    this.backgroundColorPickerTarget.click()
  }

  foregroundColorChanged () {
    this.editorTarget.editor.activateAttribute('foregroundColor', this.foregroundColorPickerTarget.value)
  }

  backgroundColorChanged () {
    this.editorTarget.editor.activateAttribute('backgroundColor', this.backgroundColorPickerTarget.value)
  }

  addForegroundButtonInToolbar () {
    Trix.config.textAttributes.foregroundColor = {
      styleProperty: 'color',
      inheritable: true
    }

    this.element
      .querySelector('.trix-button-group.trix-button-group--text-tools')
      .insertAdjacentHTML('beforeend', this.foregroundColorButtons)
  }

  addBackgroundButtonInToolbar () {
    Trix.config.textAttributes.backgroundColor = {
      styleProperty: 'backgroundColor',
      inheritable: true
    }

    this.element
      .querySelector('.trix-button-group.trix-button-group--text-tools')
      .insertAdjacentHTML('beforeend', this.backgroundColorButtons)
  }

  addTextAlignCenterButtonInToolbar () {
    Trix.config.blockAttributes.textAlignCenter = {
      tagName: 'centered-div'
    }

    this.element
      .querySelector('.trix-button-group.trix-button-group--block-tools')
      .insertAdjacentHTML('beforeend', this.textAlignButtons)
  }

  get foregroundColorButtons () {
    return `<input type="color" style="width:0;height:0;padding:0;margin-top:20px;visibility:hidden"
                   data-trix-target="foregroundColorPicker" data-action="trix#foregroundColorChanged">
            <button type="button" class="trix-button" data-action="click->trix#openForegroundColorPicker" title="Text color">
              <span class="icon"><i class="fas fa-palette fa-lg"></i></span>
            </button>`
  }

  get backgroundColorButtons () {
    return `<input type="color" style="width:0;height:0;padding:0;margin-top:20px;visibility:hidden"
                   data-trix-target="backgroundColorPicker" data-action="trix#backgroundColorChanged">
            <button type="button" class="trix-button" data-action="click->trix#openBackgroundColorPicker" title="Text background color">
              <span class="icon"><i class="fas fa-fill-drip fa-lg"></i></span>
            </button>`
  }

  get textAlignButtons () {
    return `<button type="button" class="trix-button" data-trix-attribute="textAlignCenter">
              <span class="icon"><i class="fas fa-align-center fa-lg"></i></span>
            </button>`
  }
}

You may want to replace the icon <span class="icon"><i class="fas fa-palette"></i></span> (I use fontawesome, feel free to use whatever you like, maybe a 🎨 icon would work)

this solves https://github.com/basecamp/trix/issues/498 https://github.com/basecamp/trix/issues/606 https://github.com/basecamp/trix/issues/655 and many others.

For the centered text you will need to:

Add this css

centered-div {
  display: block;
  text-align: center;
}

Also add centered-div as allowed tags (I have this in my application.rb)

config.after_initialize do
      ActionText::ContentHelper.allowed_attributes.add 'style'
      ActionText::ContentHelper.allowed_attributes.add 'controls'
      ActionText::ContentHelper.allowed_attributes.add 'poster'

      ActionText::ContentHelper.allowed_tags.add 'video'
      ActionText::ContentHelper.allowed_tags.add 'source'
      ActionText::ContentHelper.allowed_tags.add 'centered-div'
    end
arsandov commented 1 year ago

Hey @silva96, thanks for sharing, amazing instructions! I implemented it and it works great! Maybe you might add - as reminder for newbies like me- to update the Stimulus manifest with ./bin/rails stimulus:manifest:update after adding the Trix Controller.

silva96 commented 1 year ago

Hey @silva96, thanks for sharing, amazing instructions! I implement it and it works great! Maybe you might add - as reminder for newbies like me- to update the Stimulus manifest with ./bin/rails stimulus:manifest:update after adding the Trix Controller.

Sure, I'll add it!

silva96 commented 1 year ago

UPDATE: for trix 2.0+ the controller is slightly different (and better, less hacky)

import { Controller } from '@hotwired/stimulus'
import Trix from 'trix'
const { lang } = Trix.config

// Connects to data-controller="trix"
export default class extends Controller {
  static targets = ['editor', 'foregroundColorPicker', 'backgroundColorPicker']

  connect () {
    document.addEventListener('trix-before-initialize', () => {
      this.initTrix()
    })
  }

  initTrix () {
    Trix.config.blockAttributes.heading1.tagName = 'h3'
    Trix.config.toolbar.getDefaultHTML = this.getDefaultHTML.bind(this)
    this.addForegroundButtonConfig()
    this.addBackgroundButtonConfig()
    this.addTextAlignCenterButtonConfig()
  }

  openForegroundColorPicker () {
    this.foregroundColorPickerTarget.click()
  }

  openBackgroundColorPicker () {
    this.backgroundColorPickerTarget.click()
  }

  foregroundColorChanged () {
    this.editorTarget.editor.activateAttribute('foregroundColor', this.foregroundColorPickerTarget.value)
  }

  backgroundColorChanged () {
    this.editorTarget.editor.activateAttribute('backgroundColor', this.backgroundColorPickerTarget.value)
  }

  addForegroundButtonConfig () {
    Trix.config.textAttributes.foregroundColor = {
      styleProperty: 'color',
      inheritable: true
    }
  }

  addBackgroundButtonConfig () {
    Trix.config.textAttributes.backgroundColor = {
      styleProperty: 'backgroundColor',
      inheritable: true
    }
  }

  addTextAlignCenterButtonConfig () {
    Trix.config.blockAttributes.textAlignCenter = {
      tagName: 'centered-div'
    }
  }

  getDefaultHTML () {
    return `<div class="trix-button-row">
      <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${lang.bold}" tabindex="-1">${lang.bold}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${lang.italic}" tabindex="-1">${lang.italic}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${lang.strike}" tabindex="-1">${lang.strike}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${lang.link}" tabindex="-1">${lang.link}</button>
        ${this.foregroundColorButtons}
        ${this.backgroundColorButtons}
      </span>
      <span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-heading-1" data-trix-attribute="heading1" title="${lang.heading1}" tabindex="-1">${lang.heading1}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-quote" data-trix-attribute="quote" title="${lang.quote}" tabindex="-1">${lang.quote}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-code" data-trix-attribute="code" title="${lang.code}" tabindex="-1">${lang.code}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${lang.bullets}" tabindex="-1">${lang.bullets}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${lang.numbers}" tabindex="-1">${lang.numbers}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-decrease-nesting-level" data-trix-action="decreaseNestingLevel" title="${lang.outdent}" tabindex="-1">${lang.outdent}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-increase-nesting-level" data-trix-action="increaseNestingLevel" title="${lang.indent}" tabindex="-1">${lang.indent}</button>
        ${this.textAlignButtons}
      </span>
      <span class="trix-button-group trix-button-group--file-tools" data-trix-button-group="file-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-attach" data-trix-action="attachFiles" title="${lang.attachFiles}" tabindex="-1">${lang.attachFiles}</button>
      </span>
      <span class="trix-button-group-spacer"></span>
      <span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${lang.undo}" tabindex="-1">${lang.undo}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${lang.redo}" tabindex="-1">${lang.redo}</button>
      </span>
    </div>
    <div class="trix-dialogs" data-trix-dialogs>
      <div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
        <div class="trix-dialog__link-fields">
          <input type="url" name="href" class="trix-input trix-input--dialog" placeholder="${lang.urlPlaceholder}" aria-label="${lang.url}" required data-trix-input>
          <div class="trix-button-group">
            <input type="button" class="trix-button trix-button--dialog" value="${lang.link}" data-trix-method="setAttribute">
            <input type="button" class="trix-button trix-button--dialog" value="${lang.unlink}" data-trix-method="removeAttribute">
          </div>
        </div>
      </div>
    </div>`
  }

  get foregroundColorButtons () {
    return `<input type="color" style="width:0;height:0;padding:0;margin-top:20px;visibility:hidden"
                   data-trix-target="foregroundColorPicker" data-action="trix#foregroundColorChanged">
            <button type="button" class="trix-button" data-action="click->trix#openForegroundColorPicker" title="Text color">
              <span class="icon"><i class="fas fa-palette fa-lg"></i></span>
            </button>`
  }

  get backgroundColorButtons () {
    return `<input type="color" style="width:0;height:0;padding:0;margin-top:20px;visibility:hidden"
                   data-trix-target="backgroundColorPicker" data-action="trix#backgroundColorChanged">
            <button type="button" class="trix-button" data-action="click->trix#openBackgroundColorPicker" title="Text background color">
              <span class="icon"><i class="fas fa-fill-drip fa-lg"></i></span>
            </button>`
  }

  get textAlignButtons () {
    return `<button type="button" class="trix-button" data-trix-attribute="textAlignCenter">
              <span class="icon"><i class="fas fa-align-center fa-lg"></i></span>
            </button>`
  }
}
neuged commented 1 year ago

Thank you especially for mentioning the call to reset(). That might have me some hours of searching.

silva96 commented 1 year ago

Thank you especially for mentioning the call to reset(). That might have me some hours of searching.

In trix 2.0 it's not needed! (since we init trix on the event trix-before-initialize, so it always initialize properly)

neuged commented 1 year ago

In trix 2.0 it's not needed! (since we init trix on the event trix-before-initialize, so it always initialize properly)

Thanks, you are right. With the upgrade to 2.0.1 we do not need the call anymore. :+1:

chinacheng commented 1 year ago

it's greate, thank you very much!

In trix 1.x,

I add text align left and right button by this idea, and find that tags will nested if you click left\center\right button in same words. finally i find a method: put these attributes(nestable: false\exclusive: true) toTrix.config.blockAttributes.textAlignLeft will reslove this issue.

Hope this helps someone who has the same problem。

  addTextAlignLeftButtonInToolbar () {
    console.log(Trix.config.blockAttributes)
    console.log(this.element)
    Trix.config.blockAttributes.textAlignLeft = { 
      tagName: 'align-left',
      nestable: false,
      exclusive: true
    }   

    this.element
      .querySelector('.trix-button-group.trix-button-group--block-tools')
      .insertAdjacentHTML('beforeend', this.textAlignLeftButtons)
  }
mahoammedkhaled commented 11 months ago

Is there a video explaining how to install the color editor on it?

silva96 commented 11 months ago

od: put these attributes(nestable: false\exclusive: true) toTrix.config.blockAttributes.textAlignLeft will reslove this issue.

There's no video, there's a step by step tutorial that is very well explained. Do you have any doubt? feel free to ask!

afcapel commented 11 months ago

FWIW, we have color pickers both in Basecamp and Hey. You can view the code with inspect source.

For example:

https://app.hey.com/assets/initializers/rich_text-61ba21bdd43c434b34620c74dc222edd0a26ab07.js https://app.hey.com/assets/controllers/color_picker_controller-3a32b84428f8a3b78c1b0f06f2e2f6b4ade1790a.js

silva96 commented 10 months ago

FWIW, we have color pickers both in Basecamp and Hey. You can view the code with inspect source.

For example:

https://app.hey.com/assets/initializers/rich_text-61ba21bdd43c434b34620c74dc222edd0a26ab07.js https://app.hey.com/assets/controllers/color_picker_controller-3a32b84428f8a3b78c1b0f06f2e2f6b4ade1790a.js

That's nice! out of curiosity, how do they look? can you show a picture or gif?

emilsundberg commented 10 months ago

@silva96 Here is an example from composing a new email in Hey with a color picker:

https://github.com/basecamp/trix/assets/215958/ed2d9c11-1cbb-4d7b-a2c8-be4141d7bb0c

JimCook57 commented 10 months ago

This all looks great, but it looks like it is tied in with Rails. Is there an example how to add a color picker with just straight javascript. For example, adding it to this page?

<!DOCTYPE html>
<html>
    <head>
        <title>Trix Test</title>
        <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix/dist/trix.css" />
        <script type="text/javascript" src="https://unpkg.com/trix/dist/trix.umd.js"></script>
    </head>
    <body>
        <trix-editor class="trix-content"></trix-editor>
    </body>
</html>
silva96 commented 10 months ago

@JimCook57 it's tied to Stimulus more than rails actually. But you can extract the relevant code and do vanilla JS instead of stimulus.

chiaki8 commented 8 months ago

Thank you, I like this instruction very much and hope this color picker feature is included as default (OOTB). However, I encounter an issue.

In my tests, the first stimulus controller code worked perfectly on Rails 7.0.8 with Trix 1.3.1, but the second stimulus code did not initialize the Trix editor on Rails 7.1.2 with Trix 2.0.8. It does not initially (at the first page loading) show the color tool buttons and color span tags (missing) on the rich_text_area. If I refresh the page, they are shown and work properly.

For the test on Rails 7.1.2, I just created a simple Rails scaffold and modified the application.rb since the sanitizer was changed on Rails 7.1, otherwise it causes a system error.

Rails.application.config.after_initialize do
  # ActionText::ContentHelper.allowed_attributes.add 'style'
  # ActionText::ContentHelper.allowed_attributes.add 'controls'
  # ActionText::ContentHelper.allowed_attributes.add 'poster'
  #
  # ActionText::ContentHelper.allowed_tags.add 'video'
  # ActionText::ContentHelper.allowed_tags.add 'source'
  # ActionText::ContentHelper.allowed_tags.add 'centered-div'

  ActionText::ContentHelper.allowed_attributes = Loofah::HTML5::SafeList::ACCEPTABLE_ATTRIBUTES.add('controls')
  ActionText::ContentHelper.allowed_tags = Loofah::HTML5::SafeList::ACCEPTABLE_ELEMENTS.merge(['source', 'centered-div'])
end

Initially (at the first page loading), the trix-before-initialize event does not occur, so the color tool buttons are not shown. If I refresh the editor page, the event occurs and the color tool buttons are shown and work properly.

silva96 commented 8 months ago

@chiaki8 can you create a reproduction repo to test locally?

chiaki8 commented 8 months ago

@silva96 thanks, here is the reproduction repo.

chiaki8 commented 6 months ago

I found out that it works fine (shows the color tool buttons at the first page loading) by disabling turbo for the links to Trix Editor, sorry.

devrizwanali commented 6 months ago

Thanks you @silva96 - it's working perfectly fine for me 👍