mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Rich editor text color (#17888)
* Start configuration * Add basic JS implementation * add style attr * refactor * render colors on server * docs * allow arbitrary text colors & color preview * default color docs * Update 10-rich-editor.md * custom colors * Update 10-rich-editor.md
This commit is contained in:
+19
-19
File diff suppressed because one or more lines are too long
@@ -83,6 +83,7 @@ Additional tools available in the toolbar include:
|
||||
- `lead` - Applies a `lead` class around the text, which is typically used for the first paragraph of an article.
|
||||
- `small` - Applies the `<small>` tag to the text, which is typically used for small print or disclaimers.
|
||||
- `code` - Format the selected text as inline code.
|
||||
- `textColor` - Changes the [text color](#customizing-text-colors) of the selected text.
|
||||
- `table` - Creates a table in the editor with a default layout of 3 columns and 2 rows, with the first row configured as a header row.
|
||||
- `tableAddColumnBefore` - Adds a new column before the current column.
|
||||
- `tableAddColumnAfter` - Adds a new column after the current column.
|
||||
@@ -126,6 +127,68 @@ RichEditor::make('content')
|
||||
])
|
||||
```
|
||||
|
||||
## Customizing text colors
|
||||
|
||||
The rich editor includes a text color tool for styling inline text. By default, it uses the [Tailwind CSS color palette](https://tailwindcss.com/docs/colors). In light mode, the 600 shades are applied to text, and in dark mode, the 400 shades are used.
|
||||
|
||||
You can customize which colors are available in the picker using the `textColors()` method:
|
||||
|
||||
```php
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
|
||||
RichEditor::make('content')
|
||||
->textColors([
|
||||
'#ef4444' => 'Red',
|
||||
'#10b981' => 'Green',
|
||||
'#0ea5e9' => 'Sky',
|
||||
])
|
||||
```
|
||||
|
||||
If you would like to define different colors for light and dark mode, you can use the a `TextColor` object to define the color:
|
||||
|
||||
```php
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\RichEditor\TextColor;
|
||||
|
||||
RichEditor::make('content')
|
||||
->textColors([
|
||||
'brand' => TextColor::make('Brand', '#0ea5e9'),
|
||||
'warning' => TextColor::make('Warning', '#f59e0b', darkColor: '#fbbf24'),
|
||||
])
|
||||
```
|
||||
|
||||
If you would like to add new colors onto the existing Tailwind palette, you can merge your colors into the `TextColor::getDefaults()` array:
|
||||
|
||||
```php
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\RichEditor\TextColor;
|
||||
|
||||
RichEditor::make('content')
|
||||
->textColors([
|
||||
'brand' => TextColor::make('Brand', '#0ea5e9'),
|
||||
'warning' => TextColor::make('Warning', '#f59e0b', darkColor: '#fbbf24'),
|
||||
...TextColor::getDefaults(),
|
||||
])
|
||||
```
|
||||
|
||||
When you use a `TextColor` object, the key of the array becomes the stored `data-color` attribute on the `<span>` tag, allowing you to reference the color in your CSS if needed. When you use the color as the array values, the actual color value (e.g., a HEX string) is stored as the `data-color` attribute.
|
||||
|
||||
You can also pass `textColors()` to the [content renderer](#rendering-rich-content) and [rich content attribute](#registering-rich-content-attributes) so that server-side rendering matches your editor configuration.
|
||||
|
||||
You can also allow users to pick custom colors that aren't in the predefined list by using the `customTextColors()` method:
|
||||
|
||||
```php
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
|
||||
RichEditor::make('content')
|
||||
->textColors([
|
||||
// ...
|
||||
])
|
||||
->customTextColors()
|
||||
```
|
||||
|
||||
You do not need to use `customTextColors()` on the [content renderer](#rendering-rich-content), as it will automatically render any custom colors that are used in the content.
|
||||
|
||||
## Rendering rich content
|
||||
|
||||
If you're [storing content as JSON](#storing-content-as-json) instead of HTML, or your content requires processing to inject [private image URLs](#using-private-images-in-the-editor) or similar, you'll need to use the `RichContentRenderer` tool in Filament to output HTML:
|
||||
@@ -181,6 +244,39 @@ RichContentRenderer::make($record->content)
|
||||
->toHtml()
|
||||
```
|
||||
|
||||
If you are using [custom text colors](#customizing-text-colors), you can pass an array of colors to the renderer to ensure that the colors are rendered correctly:
|
||||
|
||||
```php
|
||||
use Filament\Forms\Components\RichEditor\RichContentRenderer;
|
||||
use Filament\Forms\Components\RichEditor\TextColor;
|
||||
|
||||
RichContentRenderer::make($record->content)
|
||||
->textColors([
|
||||
'brand' => TextColor::make('Brand', '#0ea5e9', darkColor: '#38bdf8'),
|
||||
])
|
||||
->toHtml();
|
||||
```
|
||||
|
||||
### Styling the rendered content
|
||||
|
||||
The rich editor HTML uses a combination of HTML elements, CSS classes, and inline styles to style the content, depending on the features used in the editor. If you render the content in a Filament table column or infolist entry with `prose()`, Filament will automatically apply the necessary styles for you. If you are outputting the content in your own Blade view, you may need to add some additional styles to ensure that the content is styled correctly.
|
||||
|
||||
One way of styling the content is to use [Tailwind CSS Typography](https://tailwindcss.com/docs/typography-plugin). This plugin provides a set of pre-defined styles for common HTML elements, such as headings, paragraphs, lists, and tables. You can apply these styles to a container element using the `prose` class:
|
||||
|
||||
```blade
|
||||
<div class="prose dark:prose-invert">
|
||||
{!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
|
||||
</div>
|
||||
```
|
||||
|
||||
However, some features, such as the grid layout and text colors, require additional styles that are not included in the Tailwind CSS Typography plugin. Filament also includes its own `fi-prose` CSS class that adds these additional styles. Any app that loads Filament's `vendor/filament/support/resources/css/index.css` CSS will have access to this class. The styling is different to the `prose` class, but fits with Filament's design system better:
|
||||
|
||||
```blade
|
||||
<div class="fi-prose">
|
||||
{!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
By default, the editor outputs raw HTML, and sends it to the backend. Attackers are able to intercept the value of the component and send a different raw HTML string to the backend. As such, it is important that when outputting the HTML from a rich editor, it is sanitized; otherwise your site may be exposed to Cross-Site Scripting (XSS) vulnerabilities.
|
||||
@@ -555,6 +651,10 @@ class Post extends Model implements HasRichContent
|
||||
'name' => 'Full name',
|
||||
'today' => 'Today\'s date',
|
||||
])
|
||||
->textColors(
|
||||
'brand' => TextColor::make('Brand', '#0ea5e9', darkColor: '#38bdf8'),
|
||||
)
|
||||
->customTextColors()
|
||||
->plugins([
|
||||
HighlightRichContentPlugin::make(),
|
||||
]);
|
||||
|
||||
@@ -254,3 +254,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .fi-fo-rich-editor-text-color-select-option {
|
||||
@apply flex items-center gap-2;
|
||||
|
||||
& .fi-fo-rich-editor-text-color-select-option-preview {
|
||||
@apply h-5 w-5 shrink-0 rounded-full bg-(--color) dark:bg-(--dark-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function richEditorFormComponent({
|
||||
placeholder,
|
||||
state,
|
||||
statePath,
|
||||
textColors,
|
||||
uploadingFileMessage,
|
||||
floatingToolbars,
|
||||
}) {
|
||||
@@ -77,6 +78,7 @@ export default function richEditorFormComponent({
|
||||
noMergeTagSearchResultsMessage,
|
||||
placeholder,
|
||||
statePath,
|
||||
textColors,
|
||||
uploadingFileMessage,
|
||||
$wire: this.$wire,
|
||||
floatingToolbars,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Mark } from '@tiptap/core'
|
||||
|
||||
export default Mark.create({
|
||||
name: 'textColor',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
textColors: {},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'span',
|
||||
getAttrs: (element) => element.classList?.contains('color'),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
const attrs = { ...HTMLAttributes }
|
||||
const existingClass = HTMLAttributes.class
|
||||
attrs.class = ['color', existingClass].filter(Boolean).join(' ')
|
||||
|
||||
const colorName = HTMLAttributes['data-color']
|
||||
const colors = this.options.textColors || {}
|
||||
const config = colors[colorName]
|
||||
|
||||
const hasColorName =
|
||||
typeof colorName === 'string' && colorName.length > 0
|
||||
const cssVars = config
|
||||
? `--color: ${config.color}; --dark-color: ${config.darkColor}`
|
||||
: hasColorName
|
||||
? `--color: ${colorName}; --dark-color: ${colorName}`
|
||||
: null
|
||||
|
||||
if (cssVars) {
|
||||
const existingStyle =
|
||||
typeof HTMLAttributes.style === 'string'
|
||||
? HTMLAttributes.style
|
||||
: ''
|
||||
attrs.style = existingStyle
|
||||
? `${cssVars}; ${existingStyle}`
|
||||
: cssVars
|
||||
}
|
||||
|
||||
return ['span', attrs, 0]
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
'data-color': {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute('data-color'),
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes['data-color']) return {}
|
||||
return { 'data-color': attributes['data-color'] }
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setTextColor:
|
||||
({ color }) =>
|
||||
({ commands }) => {
|
||||
return commands.setMark(this.name, { 'data-color': color })
|
||||
},
|
||||
unsetTextColor:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.unsetMark(this.name)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -26,6 +26,7 @@ import MergeTag from './extension-merge-tag.js'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import Small from './extension-small.js'
|
||||
import TextColor from './extension-text-color.js'
|
||||
import Strike from '@tiptap/extension-strike'
|
||||
import Subscript from '@tiptap/extension-subscript'
|
||||
import Superscript from '@tiptap/extension-superscript'
|
||||
@@ -51,6 +52,7 @@ export default async ({
|
||||
noMergeTagSearchResultsMessage,
|
||||
placeholder,
|
||||
statePath,
|
||||
textColors,
|
||||
uploadingFileMessage,
|
||||
$wire,
|
||||
}) => [
|
||||
@@ -114,6 +116,9 @@ export default async ({
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
}),
|
||||
TextColor.configure({
|
||||
textColors,
|
||||
}),
|
||||
Small,
|
||||
Strike,
|
||||
Subscript,
|
||||
|
||||
@@ -514,7 +514,7 @@ return [
|
||||
|
||||
'link' => [
|
||||
|
||||
'label' => 'Edit',
|
||||
'label' => 'Link',
|
||||
|
||||
'modal' => [
|
||||
|
||||
@@ -536,6 +536,30 @@ return [
|
||||
|
||||
],
|
||||
|
||||
'text_color' => [
|
||||
|
||||
'label' => 'Text color',
|
||||
|
||||
'modal' => [
|
||||
|
||||
'heading' => 'Text color',
|
||||
|
||||
'form' => [
|
||||
|
||||
'color' => [
|
||||
'label' => 'Color',
|
||||
],
|
||||
|
||||
'custom_color' => [
|
||||
'label' => 'Custom color',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
'file_attachments_accepted_file_types_message' => 'Uploaded files must be of type: :values.',
|
||||
@@ -586,6 +610,7 @@ return [
|
||||
'table_merge_cells' => 'Merge cells',
|
||||
'table_split_cell' => 'Split cell',
|
||||
'table_toggle_header_row' => 'Toggle header row',
|
||||
'text_color' => 'Text color',
|
||||
'underline' => 'Underline',
|
||||
'undo' => 'Undo',
|
||||
],
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
placeholder: @js($getPlaceholder()),
|
||||
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')", isOptimisticallyLive: false) }},
|
||||
statePath: @js($statePath),
|
||||
textColors: @js($getTextColorsForJs()),
|
||||
uploadingFileMessage: @js($getUploadingFileMessage()),
|
||||
floatingToolbars: @js($floatingToolbars),
|
||||
})"
|
||||
|
||||
@@ -8,6 +8,7 @@ use Filament\Forms\Components\RichEditor\Actions\AttachFilesAction;
|
||||
use Filament\Forms\Components\RichEditor\Actions\CustomBlockAction;
|
||||
use Filament\Forms\Components\RichEditor\Actions\GridAction;
|
||||
use Filament\Forms\Components\RichEditor\Actions\LinkAction;
|
||||
use Filament\Forms\Components\RichEditor\Actions\TextColorAction;
|
||||
use Filament\Forms\Components\RichEditor\EditorCommand;
|
||||
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\Contracts\FileAttachmentProvider;
|
||||
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
||||
@@ -17,7 +18,9 @@ use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
|
||||
use Filament\Forms\Components\RichEditor\RichContentRenderer;
|
||||
use Filament\Forms\Components\RichEditor\RichEditorTool;
|
||||
use Filament\Forms\Components\RichEditor\StateCasts\RichEditorStateCast;
|
||||
use Filament\Forms\Components\RichEditor\TextColor;
|
||||
use Filament\Schemas\Components\StateCasts\Contracts\StateCast;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Concerns\HasExtraAlpineAttributes;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
@@ -83,6 +86,13 @@ class RichEditor extends Field implements Contracts\CanBeLengthConstrained
|
||||
*/
|
||||
protected array | Closure | null $floatingToolbars = null;
|
||||
|
||||
/**
|
||||
* @var array<string, string | TextColor> | Closure | null
|
||||
*/
|
||||
protected array | Closure | null $textColors = null;
|
||||
|
||||
protected bool | Closure | null $hasCustomTextColors = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -123,6 +133,11 @@ class RichEditor extends Field implements Contracts\CanBeLengthConstrained
|
||||
->action(arguments: '{ url: $getEditor().getAttributes(\'link\')?.href, shouldOpenInNewTab: $getEditor().getAttributes(\'link\')?.target === \'_blank\' }')
|
||||
->icon(Heroicon::Link)
|
||||
->iconAlias('forms:components.rich-editor.toolbar.link'),
|
||||
RichEditorTool::make('textColor')
|
||||
->label(__('filament-forms::components.rich_editor.tools.text_color'))
|
||||
->action(arguments: '{ color: $getEditor().getAttributes(\'textColor\')[\'data-color\'] ?? null }')
|
||||
->icon(Heroicon::Swatch)
|
||||
->iconAlias('forms:components.rich-editor.toolbar.text-color'),
|
||||
RichEditorTool::make('h1')
|
||||
->label(__('filament-forms::components.rich_editor.tools.h1'))
|
||||
->jsHandler('$getEditor()?.chain().focus().toggleHeading({ level: 1 }).run()')
|
||||
@@ -732,6 +747,7 @@ class RichEditor extends Field implements Contracts\CanBeLengthConstrained
|
||||
CustomBlockAction::make(),
|
||||
GridAction::make(),
|
||||
LinkAction::make(),
|
||||
TextColorAction::make(),
|
||||
...array_reduce(
|
||||
$this->getPlugins(),
|
||||
fn (array $carry, RichContentPlugin $plugin): array => [
|
||||
@@ -949,4 +965,53 @@ class RichEditor extends Field implements Contracts\CanBeLengthConstrained
|
||||
|
||||
return parent::callAfterStateUpdated($shouldBubbleToParents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string | TextColor> | Closure | null $colors
|
||||
*/
|
||||
public function textColors(array | Closure | null $colors): static
|
||||
{
|
||||
$this->textColors = $colors;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string | TextColor>
|
||||
*/
|
||||
public function getTextColors(): array
|
||||
{
|
||||
$textColors = $this->evaluate($this->textColors) ?? $this->getContentAttribute()?->getTextColors() ?? TextColor::getDefaults();
|
||||
|
||||
return Arr::mapWithKeys(
|
||||
$textColors,
|
||||
fn (string | TextColor $color, string $name): array => [$name => ($color instanceof TextColor) ? $color : TextColor::make($color, $name)],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{color: string, darkColor: string}>
|
||||
*/
|
||||
public function getTextColorsForJs(): array
|
||||
{
|
||||
return array_map(
|
||||
fn (TextColor $color): array => [
|
||||
'color' => $color->getColor(),
|
||||
'darkColor' => $color->getDarkColor(),
|
||||
],
|
||||
$this->getTextColors(),
|
||||
);
|
||||
}
|
||||
|
||||
public function customTextColors(bool | Closure | null $condition = true): static
|
||||
{
|
||||
$this->hasCustomTextColors = $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasCustomTextColors(): bool
|
||||
{
|
||||
return (bool) ($this->evaluate($this->hasCustomTextColors) ?? $this->getContentAttribute()?->hasCustomTextColors() ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Forms\Components\RichEditor\Actions;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\RichEditor\EditorCommand;
|
||||
use Filament\Forms\Components\RichEditor\TextColor;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class TextColorAction
|
||||
{
|
||||
public static function make(): Action
|
||||
{
|
||||
return Action::make('textColor')
|
||||
->label(__('filament-forms::components.rich_editor.actions.text_color.label'))
|
||||
->modalHeading(__('filament-forms::components.rich_editor.actions.text_color.modal.heading'))
|
||||
->modalWidth(Width::Large)
|
||||
->fillForm(fn (array $arguments, RichEditor $component): ?array => filled($arguments['color'] ?? null) ? [
|
||||
'color' => array_key_exists($arguments['color'], $options = $component->getTextColors()) ? $arguments['color'] : null,
|
||||
'customColor' => $component->hasCustomTextColors() && (! array_key_exists($arguments['color'], $options)) ? $arguments['color'] : null,
|
||||
] : null)
|
||||
->schema(function (RichEditor $component) {
|
||||
$options = Arr::mapWithKeys(
|
||||
$component->getTextColors(),
|
||||
fn (TextColor $color, string $name): array => [$name => <<<HTML
|
||||
<div class="fi-fo-rich-editor-text-color-select-option">
|
||||
<div class="fi-fo-rich-editor-text-color-select-option-preview" style="--color: {$color->getColor()}; --dark-color: {$color->getDarkColor()}"></div>
|
||||
|
||||
<div>{$color->getSafeLabelHtml()}</div>
|
||||
</div>
|
||||
HTML],
|
||||
);
|
||||
|
||||
return [
|
||||
Select::make('color')
|
||||
->label(__('filament-forms::components.rich_editor.actions.text_color.modal.form.color.label'))
|
||||
->options($options)
|
||||
->allowHtml()
|
||||
->native(false)
|
||||
->visible(filled($options)),
|
||||
ColorPicker::make('customColor')
|
||||
->label(__('filament-forms::components.rich_editor.actions.text_color.modal.form.custom_color.label'))
|
||||
->visibleJs(<<<'JS'
|
||||
! $get('color')
|
||||
JS)
|
||||
->visible($component->hasCustomTextColors()),
|
||||
];
|
||||
})
|
||||
->action(function (array $arguments, array $data, RichEditor $component): void {
|
||||
$isSingleCharacterSelection = ($arguments['editorSelection']['head'] ?? null) === ($arguments['editorSelection']['anchor'] ?? null);
|
||||
|
||||
$color = $data['color'] ?? $data['customColor'] ?? null;
|
||||
|
||||
if (blank($color)) {
|
||||
$component->runCommands(
|
||||
[
|
||||
...($isSingleCharacterSelection ? [EditorCommand::make(
|
||||
'extendMarkRange',
|
||||
arguments: ['textColor'],
|
||||
)] : []),
|
||||
EditorCommand::make('unsetTextColor'),
|
||||
],
|
||||
editorSelection: $arguments['editorSelection'],
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$component->runCommands(
|
||||
[
|
||||
...($isSingleCharacterSelection ? [EditorCommand::make(
|
||||
'extendMarkRange',
|
||||
arguments: ['link'],
|
||||
)] : []),
|
||||
EditorCommand::make(
|
||||
'setTextColor',
|
||||
arguments: [[
|
||||
'color' => $color,
|
||||
]],
|
||||
),
|
||||
],
|
||||
editorSelection: $arguments['editorSelection'],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Filament\Forms\Components\RichEditor\FileAttachmentProviders\Contracts\FileA
|
||||
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class RichContentAttribute implements Htmlable
|
||||
{
|
||||
@@ -38,6 +39,13 @@ class RichContentAttribute implements Htmlable
|
||||
|
||||
protected bool $isJson = false;
|
||||
|
||||
/**
|
||||
* @var ?array<string, string | TextColor>
|
||||
*/
|
||||
protected ?array $textColors = null;
|
||||
|
||||
protected bool $hasCustomTextColors = false;
|
||||
|
||||
public function __construct(protected Model $model, protected string $name) {}
|
||||
|
||||
public static function make(Model $model, string $name): static
|
||||
@@ -127,6 +135,7 @@ class RichContentAttribute implements Htmlable
|
||||
->fileAttachmentsDisk($this->getFileAttachmentsDiskName())
|
||||
->fileAttachmentsVisibility($this->getFileAttachmentsVisibility())
|
||||
->fileAttachmentProvider($this->getFileAttachmentProvider())
|
||||
->textColors($this->getTextColors())
|
||||
->toHtml();
|
||||
}
|
||||
|
||||
@@ -204,4 +213,39 @@ class RichContentAttribute implements Htmlable
|
||||
{
|
||||
return $this->isJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?array<string, string | TextColor> $colors
|
||||
*/
|
||||
public function textColors(?array $colors): static
|
||||
{
|
||||
$this->textColors = $colors;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string | TextColor>
|
||||
*/
|
||||
public function getTextColors(): array
|
||||
{
|
||||
$textColors = $this->textColors ?? TextColor::getDefaults();
|
||||
|
||||
return Arr::mapWithKeys(
|
||||
$textColors,
|
||||
fn (string | TextColor $color, string $name): array => [$name => ($color instanceof TextColor) ? $color : TextColor::make($color, $name)],
|
||||
);
|
||||
}
|
||||
|
||||
public function customTextColors(bool $condition = true): static
|
||||
{
|
||||
$this->hasCustomTextColors = $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasCustomTextColors(): bool
|
||||
{
|
||||
return $this->hasCustomTextColors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ use Filament\Forms\Components\RichEditor\TipTapExtensions\MergeTagExtension;
|
||||
use Filament\Forms\Components\RichEditor\TipTapExtensions\RawHtmlMergeTagExtension;
|
||||
use Filament\Forms\Components\RichEditor\TipTapExtensions\RenderedCustomBlockExtension;
|
||||
use Filament\Forms\Components\RichEditor\TipTapExtensions\SmallExtension;
|
||||
use Filament\Forms\Components\RichEditor\TipTapExtensions\TextColorExtension;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
@@ -91,6 +93,11 @@ class RichContentRenderer implements Htmlable
|
||||
*/
|
||||
protected array $nodeProcessors = [];
|
||||
|
||||
/**
|
||||
* @var ?array<string, string | TextColor>
|
||||
*/
|
||||
protected ?array $textColors = null;
|
||||
|
||||
/**
|
||||
* @param string | array<string, mixed> | null $content
|
||||
*/
|
||||
@@ -317,6 +324,11 @@ class RichContentRenderer implements Htmlable
|
||||
app(RawHtmlMergeTagExtension::class),
|
||||
app(RenderedCustomBlockExtension::class),
|
||||
app(SmallExtension::class),
|
||||
app(TextColorExtension::class, [
|
||||
'options' => [
|
||||
'textColors' => $this->getTextColors(),
|
||||
],
|
||||
]),
|
||||
app(Strike::class),
|
||||
app(Subscript::class),
|
||||
app(Superscript::class),
|
||||
@@ -459,4 +471,27 @@ class RichContentRenderer implements Htmlable
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?array<string, string | TextColor> $colors
|
||||
*/
|
||||
public function textColors(?array $colors): static
|
||||
{
|
||||
$this->textColors = $colors;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string | TextColor>
|
||||
*/
|
||||
public function getTextColors(): array
|
||||
{
|
||||
$textColors = $this->textColors ?? TextColor::getDefaults();
|
||||
|
||||
return Arr::mapWithKeys(
|
||||
$textColors,
|
||||
fn (string | TextColor $color, string $name): array => [$name => ($color instanceof TextColor) ? $color : TextColor::make($color, $name)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Forms\Components\RichEditor;
|
||||
|
||||
use Filament\Support\Colors\Color;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TextColor
|
||||
{
|
||||
public function __construct(
|
||||
protected ?string $label = null,
|
||||
protected ?string $color = null,
|
||||
protected ?string $darkColor = null,
|
||||
) {}
|
||||
|
||||
public static function make(?string $label = null, ?string $color = null, ?string $darkColor = null): static
|
||||
{
|
||||
return app(static::class, [
|
||||
'label' => $label,
|
||||
'color' => $color,
|
||||
'darkColor' => $darkColor,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getSafeLabelHtml(): string
|
||||
{
|
||||
return e($this->getLabel());
|
||||
}
|
||||
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function getDarkColor(): ?string
|
||||
{
|
||||
return $this->darkColor ?? $this->getColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, TextColor>
|
||||
*/
|
||||
public static function getDefaults(): array
|
||||
{
|
||||
return Arr::mapWithKeys(
|
||||
Color::all(),
|
||||
fn (array $color, string $name): array => [$name => TextColor::make(Str::ucwords($name), $color['600'], $color['400'] ?? null)],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Forms\Components\RichEditor\TipTapExtensions;
|
||||
|
||||
use Filament\Forms\Components\RichEditor\TextColor;
|
||||
use Tiptap\Core\Mark;
|
||||
|
||||
class TextColorExtension extends Mark
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $name = 'textColor';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function addOptions(): array
|
||||
{
|
||||
return [
|
||||
'textColors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function parseHTML(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'tag' => 'span',
|
||||
'getAttrs' => fn ($DOMNode): bool => in_array('color', explode(' ', (string) $DOMNode->getAttribute('class'))),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<mixed>>
|
||||
*/
|
||||
public function addAttributes(): array
|
||||
{
|
||||
return [
|
||||
'data-color' => [
|
||||
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-color') ?: null,
|
||||
'renderHTML' => function ($attributes) {
|
||||
$value = null;
|
||||
|
||||
if (is_array($attributes)) {
|
||||
$value = $attributes['data-color'] ?? null;
|
||||
} elseif (is_object($attributes)) {
|
||||
$value = $attributes->{'data-color'} ?? ($attributes->dataColor ?? null);
|
||||
}
|
||||
|
||||
return [
|
||||
'data-color' => $value,
|
||||
];
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $mark
|
||||
* @param array<string, mixed> $HTMLAttributes
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function renderHTML($mark, $HTMLAttributes = []): array
|
||||
{
|
||||
$existingClass = isset($HTMLAttributes['class']) ? (string) $HTMLAttributes['class'] : '';
|
||||
$HTMLAttributes['class'] = trim(implode(' ', array_filter(['color', $existingClass])));
|
||||
|
||||
$colorName = $HTMLAttributes['data-color'] ?? null;
|
||||
$colors = $this->options['textColors'] ?? [];
|
||||
$config = is_string($colorName) ? ($colors[$colorName] ?? null) : null;
|
||||
|
||||
if ($config instanceof TextColor) {
|
||||
$cssVars = "--color: {$config->getColor()}; --dark-color: {$config->getDarkColor()}";
|
||||
} elseif (filled($colorName)) {
|
||||
$cssVars = "--color: {$colorName}; --dark-color: {$colorName}";
|
||||
} else {
|
||||
$cssVars = null;
|
||||
}
|
||||
|
||||
if (filled($cssVars)) {
|
||||
$existingStyle = isset($HTMLAttributes['style']) ? (string) $HTMLAttributes['style'] : '';
|
||||
$HTMLAttributes['style'] = $existingStyle !== '' ? ($cssVars . '; ' . $existingStyle) : $cssVars;
|
||||
}
|
||||
|
||||
return [
|
||||
'span',
|
||||
$HTMLAttributes,
|
||||
0,
|
||||
];
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -766,11 +766,15 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& .color:where(:not(.fi-not-prose, .fi-not-prose *)) {
|
||||
@apply text-(--color) dark:text-(--dark-color);
|
||||
}
|
||||
|
||||
& .lead:where(:not(.fi-not-prose, .fi-not-prose *)) {
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
& .grid-layout {
|
||||
& .grid-layout:where(:not(.fi-not-prose, .fi-not-prose *)) {
|
||||
@apply grid grid-cols-1 gap-4;
|
||||
|
||||
&[data-from-breakpoint='default'] {
|
||||
|
||||
@@ -106,6 +106,7 @@ class SupportServiceProvider extends PackageServiceProvider
|
||||
->allowRelativeLinks()
|
||||
->allowRelativeMedias()
|
||||
->allowAttribute('class', allowedElements: '*')
|
||||
->allowAttribute('data-color', allowedElements: '*')
|
||||
->allowAttribute('data-from-breakpoint', allowedElements: '*')
|
||||
->allowAttribute('style', allowedElements: '*')
|
||||
->withMaxInputLength(500000),
|
||||
|
||||
Reference in New Issue
Block a user