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
2 changes: 1 addition & 1 deletion config/filament-tinyeditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
],
'licence_key' => env('TINY_LICENSE_KEY', 'no-api-key'),
],
'provider' => 'cloud', // cloud|vendor
'provider' => 'vendor', // cloud|vendor (vendor is faster - loads from local files)
// 'direction' => 'rtl',

/**
Expand Down
2 changes: 1 addition & 1 deletion resources/dist/filament-tinymce-editor.js

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions resources/js/shortcodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// TinyMCE Shortcodes Plugin
tinymce.PluginManager.add('shortcodes', function(editor) {
// Get shortcodes from TinyMCE init config (using getParam for unregistered options)
var shortcodes = editor.getParam('shortcodes_data', []);
var shortcodesLabel = editor.getParam('shortcodes_label', 'Shortcodes');

if (!shortcodes || shortcodes.length === 0) {
console.warn('TinyMCE Shortcodes: No shortcodes configured');
return;
}

// Build lookup map for converting shortcodes to labels
var shortcodeMap = {};
shortcodes.forEach(function(s) {
shortcodeMap[s.value] = s.text;
});

// Create shortcode tag HTML
function createShortcodeTag(value, text) {
return '<span class="shortcode-tag" data-shortcode="' + value + '" contenteditable="false">' + text + '</span>';
}

// Convert {{ shortcode }} to visual tags when content is set
editor.on('BeforeSetContent', function(e) {
if (e.content) {
e.content = e.content.replace(/\{\{\s*(\w+)\s*\}\}/g, function(match, code) {
var label = shortcodeMap[code];
if (label) {
return createShortcodeTag(code, label);
}
return match;
});
}
});

// Convert visual tags back to {{ shortcode }} when getting content
editor.on('GetContent', function(e) {
if (e.content) {
// Convert span tags back to shortcode format
e.content = e.content.replace(/<span[^>]*class="shortcode-tag"[^>]*data-shortcode="(\w+)"[^>]*>[^<]*<\/span>/gi, function(match, code) {
return '{{ ' + code + ' }}';
});
}
});

// Add CSS styles for shortcode tags
editor.on('init', function() {
var css = '.shortcode-tag { ' +
'display: inline-block; ' +
'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); ' +
'color: white; ' +
'padding: 2px 8px; ' +
'border-radius: 12px; ' +
'font-size: 12px; ' +
'font-weight: 500; ' +
'margin: 0 2px; ' +
'cursor: default; ' +
'user-select: none; ' +
'-webkit-user-select: none; ' +
'vertical-align: baseline; ' +
'}';

var head = editor.getDoc().head;
var style = editor.getDoc().createElement('style');
style.type = 'text/css';
style.appendChild(editor.getDoc().createTextNode(css));
head.appendChild(style);
});

// Register the menu button
editor.ui.registry.addMenuButton('shortcodes', {
text: shortcodesLabel,
icon: 'bookmark',
tooltip: shortcodesLabel,
fetch: function(callback) {
var items = shortcodes.map(function(shortcode) {
return {
type: 'menuitem',
text: shortcode.text,
onAction: function() {
editor.insertContent(createShortcodeTag(shortcode.value, shortcode.text));
}
};
});
callback(items);
}
});

return {
getMetadata: function() {
return {
name: 'Shortcodes Plugin',
url: 'https://github.com/amidesfahani/filament-tinyeditor'
};
}
};
});
68 changes: 68 additions & 0 deletions resources/js/tinymce.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export default function tinyeditor({
remove_script_host = true,
convert_urls = true,
custom_configs = {},
shortcodes = [],
shortcodesLabel = 'Shortcodes',
setup = null,
disabled = false,
locale = "en",
Expand All @@ -80,6 +82,12 @@ export default function tinyeditor({
key,
}) {

// Store shortcodes globally for the shortcodes plugin to access
if (shortcodes && shortcodes.length > 0) {
window.tinyMceShortcodes = shortcodes;
window.tinyMceShortcodesLabel = shortcodesLabel;
}

let editors = window.filamentTinyEditors || {};

return {
Expand Down Expand Up @@ -361,9 +369,69 @@ export default function tinyeditor({
images_upload_base_path: images_upload_base_path,
license_key: license_key,

// Add shortcodes CSS via content_style (more efficient than JS injection)
content_style: shortcodes && shortcodes.length > 0 ?
'.shortcode-tag { display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; margin: 0 2px; cursor: default; user-select: none; -webkit-user-select: none; vertical-align: baseline; }' : '',

...custom_configs,

setup: function (editor) {
// Register shortcodes plugin inline (external plugin can't access init config)
if (shortcodes && shortcodes.length > 0) {
// Build lookup map for converting shortcodes to labels
var shortcodeMap = {};
shortcodes.forEach(function(s) {
shortcodeMap[s.value] = s.text;
});

// Create shortcode tag HTML
function createShortcodeTag(value, text) {
return '<span class="shortcode-tag" data-shortcode="' + value + '" contenteditable="false">' + text + '</span>';
}

// Convert {shortcode} or {{ shortcode }} to visual tags when content is set
editor.on('BeforeSetContent', function(e) {
if (e.content) {
// Match both single {code} and double {{ code }} braces
e.content = e.content.replace(/\{+\s*(\w+)\s*\}+/g, function(match, code) {
var label = shortcodeMap[code];
if (label) {
return createShortcodeTag(code, label);
}
return match;
});
}
});

// Convert visual tags back to {{ shortcode }} when getting content
editor.on('GetContent', function(e) {
if (e.content) {
e.content = e.content.replace(/<span[^>]*class="shortcode-tag"[^>]*data-shortcode="(\w+)"[^>]*>[^<]*<\/span>/gi, function(match, code) {
return '{{ ' + code + ' }}';
});
}
});

// Register the menu button
editor.ui.registry.addMenuButton('shortcodes', {
text: shortcodesLabel,
icon: 'bookmark',
tooltip: shortcodesLabel,
fetch: function(callback) {
var items = shortcodes.map(function(shortcode) {
return {
type: 'menuitem',
text: shortcode.text,
onAction: function() {
editor.insertContent(createShortcodeTag(shortcode.value, shortcode.text));
}
};
});
callback(items);
}
});
}

if (!window.tinySettingsCopy) {
window.tinySettingsCopy = [];
}
Expand Down
18 changes: 5 additions & 13 deletions resources/views/tiny-editor.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,7 @@
$textareaID = 'tiny-editor-' . str_replace(['.', '#', '$'], '-', $getId()) . '-' . rand();
@endphp

<div
x-data="{ isModalOpen: false }"
x-init="$el.closest('.fi-modal')?.addEventListener('open-tinyeditor-modal', () => { isModalOpen = true; });
$el.closest('.fi-modal')?.addEventListener('close-tinyeditor-modal', () => { isModalOpen = false; });
$watch('isModalOpen', value => { $dispatch('modal-visibility-changed', { isOpen: value }); });"
>
<div>
<x-filament::input.wrapper
x-cloak
:valid="!$errors->has($statePath)"
Expand Down Expand Up @@ -110,16 +105,13 @@
@if ($getImagesUploadUrl !== false) images_upload_url: @js($getImagesUploadUrl()), @endif
image_advtab: @js($imageAdvtab()),
image_description: @js($getImageDescription()),
image_class_list: @js($getImageClassList()),
@if (is_array($getImageClassList())) image_class_list: @js($getImageClassList()), @endif
license_key: '{{ $getLicenseKey() }}',
custom_configs: {{ $getCustomConfigs() }},
shortcodes: @js($getShortcodes()),
shortcodesLabel: @js($getShortcodesLabel()),
uploadingMessage: '{{ $getUploadingMessage() ?: 'Uploading image...' }}',
key: '{{ $getKey() }}',
setup: (editor) => {
editor.on('blur change keyup', () => {
$wire.set('{{ $statePath }}', editor.getContent(), false);
})
},
key: '{{ $getKey() }}'
})"
wire:ignore
wire:key="{{ $livewireKey }}.{{ substr(md5(serialize([$isDisabled])), 0, 64) }}"
Expand Down
54 changes: 52 additions & 2 deletions src/TinyEditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class TinyEditor extends Field implements Contracts\CanBeLengthConstrained
protected bool $imageDescription = true;
protected bool|string $resize = false;
protected bool $textPattern = true;
protected array $shortcodes = [];
protected string $shortcodesLabel = 'Shortcodes';

protected string $tiny;
protected string $languageVersion;
Expand Down Expand Up @@ -176,6 +178,11 @@ public function getToolbar(): string
$toolbar = config('filament-tinyeditor.profiles.' . $this->profile . '.toolbar');
}

// Prepend shortcodes button if shortcodes are enabled
if (!empty($this->shortcodes) && !str_contains($toolbar, 'shortcodes')) {
$toolbar = 'shortcodes | ' . $toolbar;
}

return $toolbar;
}

Expand Down Expand Up @@ -646,8 +653,13 @@ public function setConvertUrls(bool $convertUrls): static

public function getExternalPlugins(): string
{
if (config('filament-tinyeditor.profiles.' . $this->profile . '.external_plugins')) {
return str_replace('"', "'", json_encode(config('filament-tinyeditor.profiles.' . $this->profile . '.external_plugins')));
$plugins = config('filament-tinyeditor.profiles.' . $this->profile . '.external_plugins', []);

// Note: shortcodes plugin is now registered inline in tinymce.js setup callback
// External plugins cannot access TinyMCE init config at load time

if (!empty($plugins)) {
return str_replace('"', "'", json_encode($plugins));
}

return '{}';
Expand Down Expand Up @@ -776,6 +788,44 @@ public function textPattern(bool $textPattern = true): static
return $this;
}

/**
* Enable shortcodes plugin with the specified shortcodes array.
*
* @param array $shortcodes Array of shortcodes: [['text' => 'Label', 'value' => 'code'], ...]
*/
public function shortcodes(array $shortcodes): static
{
$this->shortcodes = $shortcodes;
return $this;
}

/**
* Set the label for the shortcodes toolbar button.
*/
public function shortcodesLabel(string $label): static
{
$this->shortcodesLabel = $label;
return $this;
}

public function getShortcodes(): array
{
return $this->shortcodes;
}

public function getShortcodesLabel(): string
{
return $this->shortcodesLabel;
}

public function getShortcodesJson(): string
{
if (empty($this->shortcodes)) {
return '[]';
}
return json_encode($this->shortcodes, JSON_UNESCAPED_UNICODE);
}

public function fileAttachmentProvider(?FileAttachmentProvider $provider): static
{
$this->fileAttachmentProvider = $provider?->attribute($this);
Expand Down
5 changes: 5 additions & 0 deletions src/TinyeditorServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ function (InstallCommand $command) {
else if (file_exists(base_path('vendor/tinymce/tinymce'))) {
$this->publishes([base_path('vendor/tinymce/tinymce') => public_path('vendor/tinymce')], 'public');
}

// Publish shortcodes plugin
$this->publishes([
__DIR__ . '/../resources/js/shortcodes.js' => public_path('js/amidesfahani/filament-tinyeditor/shortcodes.js'),
], ['public', 'filament-tinyeditor-shortcodes']);
}

public function packageRegistered(): void {}
Expand Down