Skip to content
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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,50 @@ CommentsAction::make()

**Important**: Make sure to whitelist the classes in your Tailwind config if you override them.

### Editor toolbar

The comment editor shows a formatting toolbar above the input. The available buttons are:

`bold`, `italic`, `underline`, `strike`, `h1`, `h2`, `h3`, `blockquote`, `bulletList`, `orderedList`, `code`, `link`.

You can configure which buttons appear globally via the `toolbar` option in your `config/commentions.php` file. Buttons may be a flat list, or grouped into arrays to render visual separators between groups:

```php
'toolbar' => [
'enabled' => env('COMMENTIONS_TOOLBAR_ENABLED', true),

'buttons' => [
['bold', 'italic', 'underline'],
['bulletList', 'orderedList'],
['link'],
],
],
```

To hide the toolbar entirely, set `enabled` to `false` (or set `COMMENTIONS_TOOLBAR_ENABLED=false` in your `.env`).

You can also override the buttons on a per-component basis using the `toolbarButtons()` method:

```php
use Kirschbaum\Commentions\Filament\Infolists\Components\CommentsEntry;

CommentsEntry::make('comments')
->mentionables(fn (Model $record) => User::all())
->toolbarButtons([['bold', 'italic'], ['link']])
```

Or with actions:

```php
use Kirschbaum\Commentions\Filament\Actions\CommentsAction;

CommentsAction::make()
->mentionables(User::all())
->toolbarButtons(['bold', 'italic', 'link'])
```

Pass an empty array (`->toolbarButtons([])`) to hide the toolbar for a single component.

### Translations

You can publish the package translation files and override any strings used by the UI.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"require": {
"spatie/laravel-package-tools": "^1.18",
"league/html-to-markdown": "^5.1",
"symfony/html-sanitizer": "^7.0|^8.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"livewire/livewire": "^3.5|^4.0",
Expand Down
24 changes: 24 additions & 0 deletions config/commentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@
'allowed' => ['👍', '❤️', '😂', '😮', '😢', '🤔'],
],

/*
|--------------------------------------------------------------------------
| Editor toolbar
|--------------------------------------------------------------------------
|
| The formatting toolbar shown above the comment editor. Set `enabled` to
| false to hide it globally, or override the buttons per component with
| CommentsEntry::make()->toolbarButtons([...]). Buttons may be a flat list
| or grouped into arrays to render visual separators between groups.
|
| Available buttons: bold, italic, underline, strike, h1, h2, h3,
| blockquote, bulletList, orderedList, code, link.
|
*/
'toolbar' => [
'enabled' => env('COMMENTIONS_TOOLBAR_ENABLED', true),

'buttons' => [
['bold', 'italic', 'underline'],
['bulletList', 'orderedList'],
['link'],
],
],

/*
|--------------------------------------------------------------------------
| Subscriptions
Expand Down
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
"dependencies": {
"@tailwindcss/cli": "^4.0.6",
"@tiptap/core": "^2.11.2",
"@tiptap/extension-link": "^2.27.2",
"@tiptap/extension-mention": "^2.11.2",
"@tiptap/extension-placeholder": "^2.11.2",
"@tiptap/extension-underline": "^2.27.2",
"@tiptap/pm": "^2.11.2",
"@tiptap/starter-kit": "^2.11.2",
"@tiptap/suggestion": "^2.11.2",
Expand Down
36 changes: 36 additions & 0 deletions resources/css/commentions.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,39 @@
.mention-item:hover {
@apply comm:bg-gray-100;
}

.tip-tap-container {
@apply comm:rounded-lg comm:border comm:border-gray-300 comm:dark:border-gray-700 comm:bg-white comm:dark:bg-gray-900 comm:overflow-hidden comm:focus-within:ring-2 comm:focus-within:ring-blue-500/50 comm:focus-within:border-blue-500;
}

.commentions-toolbar {
@apply comm:flex comm:flex-wrap comm:items-center comm:gap-1 comm:p-1 comm:border-b comm:border-gray-300 comm:dark:border-gray-700 comm:bg-gray-50 comm:dark:bg-gray-800;
}

.commentions-toolbar-group {
@apply comm:flex comm:items-center comm:gap-0.5;
}

.commentions-toolbar-group:not(:last-child) {
@apply comm:pr-1 comm:mr-1 comm:border-r comm:border-gray-300 comm:dark:border-gray-700;
}

.commentions-toolbar-button {
@apply comm:flex comm:items-center comm:justify-center comm:p-1.5 comm:rounded comm:text-gray-600 comm:dark:text-gray-300 comm:cursor-pointer;
}

.commentions-toolbar-button:hover {
@apply comm:bg-gray-200 comm:dark:bg-gray-700;
}

.commentions-toolbar-button:focus-visible {
@apply comm:bg-gray-200 comm:dark:bg-gray-700 comm:outline-none;
}

.commentions-toolbar-button-active {
@apply comm:bg-gray-200 comm:dark:bg-gray-700 comm:text-blue-600 comm:dark:text-blue-400;
}

.tiptap a.comm-link {
@apply comm:text-blue-600 comm:dark:text-blue-400 comm:underline;
}
2 changes: 1 addition & 1 deletion resources/dist/commentions.css

Large diffs are not rendered by default.

42 changes: 23 additions & 19 deletions resources/dist/commentions.js

Large diffs are not rendered by default.

89 changes: 86 additions & 3 deletions resources/js/commentions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Mention from '@tiptap/extension-mention'
import Placeholder from '@tiptap/extension-placeholder'
import Underline from '@tiptap/extension-underline'
import Link from '@tiptap/extension-link'
import suggestion from './suggestion'

const debounce = (func, wait) => {
Expand All @@ -16,11 +18,64 @@ const debounce = (func, wait) => {
};
};

const SAFE_LINK_PROTOCOLS = ['http:', 'https:', 'mailto:'];

const isSafeUrl = (url) => {
try {
return SAFE_LINK_PROTOCOLS.includes(new URL(url, window.location.origin).protocol);
} catch {
return false;
}
};

const promptForLink = (editor, labels = {}) => {
if (editor.isActive('link')) {
editor.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}

const previousUrl = editor.getAttributes('link').href;
const url = window.prompt(labels.prompt ?? 'Enter the URL', previousUrl || 'https://');

if (url === null) {
return;
}

if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}

if (! isSafeUrl(url)) {
window.alert(labels.invalid ?? 'That link uses an unsupported or unsafe URL.');
return;
}

editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
};

// Maps a toolbar button name to the TipTap command it runs and the predicate
// used to highlight it when the active selection already has that formatting.
const toolbarCommands = {
bold: { run: (e) => e.chain().focus().toggleBold().run(), active: (e) => e.isActive('bold') },
italic: { run: (e) => e.chain().focus().toggleItalic().run(), active: (e) => e.isActive('italic') },
underline: { run: (e) => e.chain().focus().toggleUnderline().run(), active: (e) => e.isActive('underline') },
strike: { run: (e) => e.chain().focus().toggleStrike().run(), active: (e) => e.isActive('strike') },
h1: { run: (e) => e.chain().focus().toggleHeading({ level: 1 }).run(), active: (e) => e.isActive('heading', { level: 1 }) },
h2: { run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(), active: (e) => e.isActive('heading', { level: 2 }) },
h3: { run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(), active: (e) => e.isActive('heading', { level: 3 }) },
blockquote: { run: (e) => e.chain().focus().toggleBlockquote().run(), active: (e) => e.isActive('blockquote') },
bulletList: { run: (e) => e.chain().focus().toggleBulletList().run(), active: (e) => e.isActive('bulletList') },
orderedList: { run: (e) => e.chain().focus().toggleOrderedList().run(), active: (e) => e.isActive('orderedList') },
code: { run: (e) => e.chain().focus().toggleCode().run(), active: (e) => e.isActive('code') },
link: { run: (e, labels) => promptForLink(e, labels), active: (e) => e.isActive('link') },
};

document.addEventListener('alpine:init', () => {
Alpine.data('editor', (content, mentions, component, placeholder, editorCssClasses, componentAlias = null) => {
Alpine.data('editor', (content, mentions, component, placeholder, editorCssClasses, componentAlias = null, toolbarLabels = {}) => {
let editor

const defaultEditorCssClasses = `comm:prose comm:dark:prose-invert comm:prose-sm comm:sm:prose-base comm:lg:prose-lg comm:xl:prose-2xl comm:focus:outline-none comm:p-4 comm:min-w-full comm:w-full comm:rounded-lg comm:border comm:border-gray-300 comm:dark:border-gray-700`;
const defaultEditorCssClasses = `comm:prose comm:dark:prose-invert comm:prose-sm comm:sm:prose-base comm:lg:prose-lg comm:xl:prose-2xl comm:focus:outline-none comm:p-4 comm:min-w-full comm:w-full`;

return {
updatedAt: Date.now(),
Expand All @@ -36,7 +91,17 @@ document.addEventListener('alpine:init', () => {
editor = new Editor({
element: this.$refs.element,
extensions: [
StarterKit,
StarterKit.configure({
heading: { levels: [1, 2, 3] },
}),
Underline,
Link.configure({
openOnClick: false,
validate: (url) => isSafeUrl(url),
HTMLAttributes: {
class: 'comm-link',
},
}),
Mention.configure({
HTMLAttributes: {
class: 'mention',
Expand Down Expand Up @@ -82,6 +147,24 @@ document.addEventListener('alpine:init', () => {
isActive(type, opts = {}) {
return editor.isActive(type, opts)
},

runToolbarCommand(name) {
const command = toolbarCommands[name]

if (command && editor) {
command.run(editor, toolbarLabels)
}
},

isToolbarButtonActive(name) {
// Touch `updatedAt` so Alpine re-evaluates this on every
// selection/content change, keeping button highlights in sync.
void this.updatedAt

const command = toolbarCommands[name]

return !! (command && editor && command.active(editor))
},
}
})
})
18 changes: 18 additions & 0 deletions resources/lang/en/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,22 @@
'notification_subscribed' => 'Subscribed to notifications',
'notification_unsubscribed' => 'Unsubscribed from notifications',
'notification_comment_deleted' => 'Comment deleted',

'toolbar' => [
'aria_label' => 'Text formatting',
'bold' => 'Bold',
'italic' => 'Italic',
'underline' => 'Underline',
'strike' => 'Strikethrough',
'h1' => 'Heading 1',
'h2' => 'Heading 2',
'h3' => 'Heading 3',
'blockquote' => 'Quote',
'bullet_list' => 'Bullet list',
'ordered_list' => 'Numbered list',
'code' => 'Code',
'link' => 'Link',
'link_prompt' => 'Enter the URL',
'link_invalid' => 'That link uses an unsupported or unsafe URL.',
],
];
1 change: 1 addition & 0 deletions resources/views/comment-list.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class="comm:w-8 comm:h-8 comm:text-gray-400 comm:dark:text-gray-500"
:comment="$comment"
:mentionables="$mentionables"
:tip-tap-css-classes="$tipTapCssClasses"
:toolbar-buttons="$toolbarButtons"
/>
@endforeach

Expand Down
3 changes: 2 additions & 1 deletion resources/views/comment.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1"
@if ($editing)
<div class="comm:mt-2">
<div class="tip-tap-container comm:mb-2" wire:ignore>
<div x-data="editor(@js($commentBody), @js($mentionables), 'comment', null, @js($this->getTipTapCssClasses()), @js($commentionsComponentPrefix . 'comment'))">
<div x-data="editor(@js($commentBody), @js($mentionables), 'comment', null, @js($this->getTipTapCssClasses()), @js($commentionsComponentPrefix . 'comment'), @js(['prompt' => __('commentions::comments.toolbar.link_prompt'), 'invalid' => __('commentions::comments.toolbar.link_invalid')]))">
@include('commentions::partials.toolbar', ['toolbarButtons' => $this->getToolbarButtons()])
<div x-ref="element"></div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions resources/views/comments-modal.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
:sidebar-enabled="$sidebarEnabled ?? true"
:show-subscribers="$showSubscribers ?? true"
:tip-tap-css-classes="$tipTapCssClasses ?? null"
:toolbar-buttons="$toolbarButtons ?? null"
/>
</div>
4 changes: 3 additions & 1 deletion resources/views/comments.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
{{-- tiptap editor --}}
<div class="comm:relative tip-tap-container comm:mb-2" x-on:click="wasFocused = true" wire:ignore>
<div
x-data="editor(@js($commentBody), @js($this->mentions), 'comments', @js($this->getPlaceholder()), @js($this->getTipTapCssClasses()), @js($commentionsComponentPrefix . 'comments'))"
x-data="editor(@js($commentBody), @js($this->mentions), 'comments', @js($this->getPlaceholder()), @js($this->getTipTapCssClasses()), @js($commentionsComponentPrefix . 'comments'), @js(['prompt' => __('commentions::comments.toolbar.link_prompt'), 'invalid' => __('commentions::comments.toolbar.link_invalid')]))"
>
@include('commentions::partials.toolbar', ['toolbarButtons' => $this->getToolbarButtons()])
<div x-ref="element"></div>
</div>
</div>
Expand Down Expand Up @@ -42,6 +43,7 @@
:load-more-label="$loadMoreLabel ?? __('commentions::comments.show_more')"
:per-page-increment="$perPageIncrement ?? null"
:tip-tap-css-classes="$tipTapCssClasses"
:toolbar-buttons="$this->getToolbarButtons()"
/>
</div>

Expand Down
Loading
Loading