Skip to content

Commit 90007de

Browse files
committed
Add a Rich Text Toolbar field setting with underline, strikethrough, subscript, superscript and inline code options. (#84)
1 parent 5c22146 commit 90007de

28 files changed

Lines changed: 264 additions & 40 deletions

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Added
6+
- Added a **Rich Text Toolbar** field setting to choose the formatting buttons in rich-text cells, with new underline, strikethrough, subscript, superscript and inline code options ([#84](https://github.com/verbb/tablemaker/issues/84)).
7+
38
## 5.1.7 - 2026-11-05
49

510
### Removed

‎docs/feature-tour/field.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Turn on **Enable Caption** when each table needs a descriptive title, such as
2424

2525
Use **Allowed Column Types** to limit the choices in **Configure**. For this schedule, allow Row heading, Date, Dropdown, Number, Lightswitch and Multi-line text. Choose **All** to make every built-in type available. Changing this setting preserves existing columns and their values. An existing column keeps its current type as a choice; new columns and type changes use the allowed choices.
2626

27-
**Rich text** is available as a column choice only when CKEditor 5.0 or later is installed and enabled. Rich-text cells show a compact formatted preview so paragraphs, emphasis, links and lists remain distinguishable from plain multiline text. By default, selecting that preview opens a focused modal editor with bold, italic, link and list controls. The field setting **Rich Text Editing Mode** can instead opt a simple table into inline editing; only the active cell creates an editor, and its row expands while it is being edited. Modal editing remains recommended for tables with several columns. **Rich Text Link Types** controls whether editors can select Entries, Categories and Assets as link targets; standard URL links remain available. Element links retain Craft reference tags so generated table HTML follows later URI changes. If CKEditor is disabled, uninstalled or older than version 5.0, existing rich-text columns and their content are preserved and the cells fall back to raw HTML textareas until a supported version is available again.
27+
**Rich text** is available as a column choice only when CKEditor 5.0 or later is installed and enabled. Rich-text cells show a compact formatted preview so paragraphs, emphasis, links and lists remain distinguishable from plain multiline text. By default, selecting that preview opens a focused modal editor with bold, italic, link and list controls. The field setting **Rich Text Toolbar** chooses which formatting buttons editors see, from bold, italic, underline, strikethrough, subscript, superscript, inline code, links, bulleted lists and numbered lists; undo and redo are always available. Saved HTML keeps every format the toolbar can offer, so removing a button only changes the editor and never strips existing content. The field setting **Rich Text Editing Mode** can instead opt a simple table into inline editing; only the active cell creates an editor, and its row expands while it is being edited. Modal editing remains recommended for tables with several columns. **Rich Text Link Types** controls whether editors can select Entries, Categories and Assets as link targets; standard URL links remain available. Element links retain Craft reference tags so generated table HTML follows later URI changes. If CKEditor is disabled, uninstalled or older than version 5.0, existing rich-text columns and their content are preserved and the cells fall back to raw HTML textareas until a supported version is available again.
2828

2929
Set **Min Rows**, **Max Rows**, **Min Columns** or **Max Columns** when the design requires bounds. For example, a minimum of two columns keeps a plan name and price together, while a maximum of four prevents a comparison table from growing too wide. **Add Row Label** lets you give the add action a name suited to the content, such as “Add a plan”.
3030

‎src/fields/TableMakerField.php‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class TableMakerField extends Field implements CrossSiteCopyableFieldInterface
3737
public const EDIT_COLUMNS_POSITION_TABLE_HEADER = 'tableHeader';
3838
public const RICH_TEXT_EDITING_MODE_MODAL = 'modal';
3939
public const RICH_TEXT_EDITING_MODE_INLINE = 'inline';
40+
public const RICH_TEXT_TOOLBAR_DEFAULT = ['bold', 'italic', 'link', 'bulletedList', 'numberedList'];
4041

4142
private const MIN_CKEDITOR_VERSION = '5.0.0';
4243
private const REJECTED_ERRORS_KEY = '__tableMakerErrors';
@@ -215,6 +216,53 @@ public static function normalizeRichTextLinkTypesSetting(mixed $value): string|a
215216
return count($filtered) === count($all) ? '*' : $filtered;
216217
}
217218

219+
/**
220+
* Canonical storage for the rich-text toolbar: known item handles in toolbar order.
221+
*/
222+
public static function normalizeRichTextToolbarSetting(mixed $value): array
223+
{
224+
// Fields saved before this setting existed keep the original toolbar.
225+
if ($value === null) {
226+
return self::RICH_TEXT_TOOLBAR_DEFAULT;
227+
}
228+
229+
// An unchecked checkbox select posts an empty string.
230+
if ($value === '') {
231+
return [];
232+
}
233+
234+
if (is_string($value)) {
235+
$value = [$value];
236+
}
237+
238+
if (!is_array($value)) {
239+
return self::RICH_TEXT_TOOLBAR_DEFAULT;
240+
}
241+
242+
return array_values(array_intersect(array_keys(self::allRichTextToolbarOptions()), $value));
243+
}
244+
245+
/**
246+
* Toolbar items Table Maker can offer (handle → label), in toolbar order.
247+
* Each one must have matching CKEditor plugins in the field input and tags in
248+
* {@see TableValue}'s rich-text allowlist.
249+
*/
250+
public static function allRichTextToolbarOptions(): array
251+
{
252+
return [
253+
'bold' => Craft::t('tablemaker', 'Bold'),
254+
'italic' => Craft::t('tablemaker', 'Italic'),
255+
'underline' => Craft::t('tablemaker', 'Underline'),
256+
'strikethrough' => Craft::t('tablemaker', 'Strikethrough'),
257+
'subscript' => Craft::t('tablemaker', 'Subscript'),
258+
'superscript' => Craft::t('tablemaker', 'Superscript'),
259+
'code' => Craft::t('tablemaker', 'Code'),
260+
'link' => Craft::t('tablemaker', 'Link'),
261+
'bulletedList' => Craft::t('tablemaker', 'Bulleted list'),
262+
'numberedList' => Craft::t('tablemaker', 'Numbered list'),
263+
];
264+
}
265+
218266
/** Craft element types that can be selected from CKEditor's link UI. */
219267
public static function allRichTextLinkTypeOptions(): array
220268
{
@@ -234,6 +282,8 @@ public static function allRichTextLinkTypeOptions(): array
234282
public string $richTextEditingMode = self::RICH_TEXT_EDITING_MODE_MODAL;
235283
/** Craft element types offered alongside the standard URL link input. */
236284
public mixed $richTextLinkTypes = '*';
285+
/** Formatting buttons shown in rich-text cell editors (#84). */
286+
public mixed $richTextToolbar = self::RICH_TEXT_TOOLBAR_DEFAULT;
237287
public ?string $rowsAddRowLabel = null;
238288

239289
/**
@@ -294,6 +344,7 @@ public function beforeSave(bool $isNew): bool
294344
$this->editColumnsPosition = self::normalizeEditColumnsPosition($this->editColumnsPosition);
295345
$this->richTextEditingMode = self::normalizeRichTextEditingMode($this->richTextEditingMode);
296346
$this->richTextLinkTypes = self::normalizeRichTextLinkTypesSetting($this->richTextLinkTypes);
347+
$this->richTextToolbar = self::normalizeRichTextToolbarSetting($this->richTextToolbar);
297348

298349
return parent::beforeSave($isNew);
299350
}
@@ -401,6 +452,7 @@ public function copyValue(ElementInterface $from, ElementInterface $to): void
401452
if ($copy) {
402453
$copy->siteId = $to->siteId;
403454
}
455+
404456
$to->setFieldValue($this->handle, $copy);
405457
}
406458

@@ -516,6 +568,8 @@ public function getSettingsHtml(): ?string
516568
'settings' => $this->getSettings(),
517569
'allColumnTypeOptions' => self::allColumnTypeSettingOptions(),
518570
'allRichTextLinkTypeOptions' => self::allRichTextLinkTypeOptions(),
571+
'allRichTextToolbarOptions' => self::allRichTextToolbarOptions(),
572+
'richTextToolbar' => self::normalizeRichTextToolbarSetting($this->richTextToolbar),
519573
'ckeditorAvailable' => self::isCkeditorAvailable(),
520574
'unavailableAllowedColumnTypes' => $unavailableAllowedColumnTypes,
521575
]);
@@ -736,6 +790,7 @@ protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inl
736790
foreach ($options as $option) {
737791
if (!empty($option['default'])) {
738792
$defaults[$columnId] = $option['value'];
793+
739794
break;
740795
}
741796
}
@@ -748,6 +803,7 @@ protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inl
748803
while (array_key_exists('row' . $nextIndex, $rows)) {
749804
$nextIndex++;
750805
}
806+
751807
$rows['row' . $nextIndex] = $defaults;
752808
$nextIndex++;
753809
}
@@ -760,6 +816,7 @@ protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inl
760816
while (array_key_exists('col' . $nextIndex, $columns)) {
761817
$nextIndex++;
762818
}
819+
763820
$columns['col' . $nextIndex] = [
764821
'heading' => '',
765822
'align' => 'left',
@@ -801,6 +858,7 @@ protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inl
801858
'ckeditorAvailable' => $ckeditorAvailable,
802859
'richTextEditingMode' => self::normalizeRichTextEditingMode($this->richTextEditingMode),
803860
'richTextLinkOptions' => $ckeditorAvailable ? $this->_richTextLinkOptions($element) : [],
861+
'richTextToolbar' => self::normalizeRichTextToolbarSetting($this->richTextToolbar),
804862
'elementSiteId' => $element?->siteId,
805863
'enableWidthColumn' => $this->enableWidthColumn,
806864
'enableAlignmentColumn' => $this->enableAlignmentColumn,
@@ -905,6 +963,7 @@ private function _richTextLinkOptions(?ElementInterface $element): array
905963
$sources[] = "group:$group->uid";
906964
}
907965
}
966+
908967
$sources = array_merge($sources, $this->_customElementSources(Category::class));
909968

910969
if ($sources !== []) {
@@ -929,6 +988,7 @@ private function _richTextLinkOptions(?ElementInterface $element): array
929988
$sources[] = "volume:$volume->uid";
930989
}
931990
}
991+
932992
$sources = array_merge($sources, $this->_customElementSources(Asset::class));
933993

934994
if ($sources !== []) {
@@ -953,6 +1013,7 @@ private function _richTextEntrySources(?ElementInterface $element): array
9531013
foreach (Craft::$app->getEntries()->getAllSections() as $section) {
9541014
if ($section->type === Section::TYPE_SINGLE) {
9551015
$showSingles = true;
1016+
9561017
continue;
9571018
}
9581019

@@ -965,6 +1026,7 @@ private function _richTextEntrySources(?ElementInterface $element): array
9651026
foreach ($sites as $site) {
9661027
if (isset($siteSettings[$site->id]) && $siteSettings[$site->id]->hasUrls) {
9671028
$sources[] = "section:$section->uid";
1029+
9681030
break;
9691031
}
9701032
}
@@ -1042,6 +1104,7 @@ private function _requestSizeErrors(array $value): array
10421104
$errors[] = Craft::t('tablemaker', 'Table must have at most {count} submitted cells.', [
10431105
'count' => self::MAX_REQUEST_CELLS,
10441106
]);
1107+
10451108
break;
10461109
}
10471110
}

‎src/helpers/TableValue.php‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
*/
2424
class TableValue
2525
{
26-
private const RICH_TEXT_ALLOWED_HTML = 'p,br,strong,b,em,i,a[href|title|target|rel],ul,ol,li';
26+
// Every tag any rich-text toolbar option can produce. Fields share one policy,
27+
// so turning a button off only changes the editor, not what saved HTML keeps.
28+
private const RICH_TEXT_ALLOWED_HTML = 'p,br,strong,b,em,i,u,s,sub,sup,code,a[href|title|target|rel],ul,ol,li';
2729

2830
/**
2931
* @return TableMakerData|null
@@ -414,6 +416,7 @@ public static function validateCell(string $type, mixed $value, ?string &$error
414416
case 'number':
415417
if (!is_numeric($value) || !is_finite((float)$value)) {
416418
$error = Craft::t('tablemaker', 'Number cells must contain a finite number.');
419+
417420
return false;
418421
}
419422

@@ -424,12 +427,15 @@ public static function validateCell(string $type, mixed $value, ?string &$error
424427
}
425428

426429
$validator = new ColorValidator();
430+
427431
break;
428432
case 'url':
429433
$validator = new UrlValidator();
434+
430435
break;
431436
case 'email':
432437
$validator = new EmailValidator();
438+
433439
break;
434440
default:
435441
return true;
@@ -596,6 +602,7 @@ public static function constrainColumnTypes(array $columns, array $allowedTypes,
596602
if ($fallback !== 'select') {
597603
unset($column['options']);
598604
}
605+
599606
$columns[$colId] = $column;
600607
}
601608
}
@@ -746,6 +753,7 @@ private static function canonicalizeRows(mixed $rows, array $columns, bool $from
746753
$decoded = Json::decodeIfJson($raw);
747754
$raw = is_string($decoded) ? $decoded : $raw;
748755
}
756+
749757
$type = $columns[$colId]['type'] ?? 'singleline';
750758
$cells[$colId] = self::normalizeCell($type, $raw, $fromRequest);
751759
}
@@ -886,7 +894,7 @@ private static function renderCellHtml(string $type, mixed $value, ?int $siteId
886894
return Html::encode($string);
887895
}
888896

889-
/** Keep the cell contract as HTML text while enforcing Table Maker's small toolbar vocabulary. */
897+
/** Keep the cell contract as HTML text while enforcing the vocabulary of Table Maker's toolbar options. */
890898
private static function sanitizeRichText(string $value): string
891899
{
892900
return HtmlPurifier::process($value, [

‎src/templates/_field/settings.html‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@
4646
values: settings.richTextLinkTypes ?? '*',
4747
showAllOption: true,
4848
}) }}
49+
50+
{{ forms.checkboxSelectField({
51+
label: 'Rich Text Toolbar' | t('tablemaker'),
52+
instructions: 'Choose the formatting buttons available when editing rich-text cells.' | t('tablemaker'),
53+
name: 'richTextToolbar',
54+
options: allRichTextToolbarOptions,
55+
values: richTextToolbar,
56+
}) }}
57+
{% else %}
58+
{# Keep the saved toolbar while CKEditor is unavailable, including an empty one. #}
59+
<input type="hidden" name="{{ 'richTextToolbar' | namespaceInputName }}" value="">
60+
{% for item in richTextToolbar %}
61+
<input type="hidden" name="{{ 'richTextToolbar[]' | namespaceInputName }}" value="{{ item }}">
62+
{% endfor %}
4963
{% endif %}
5064

5165
{{ forms.lightswitchField({

‎src/translations/en/tablemaker.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,27 @@
1010
'Alignment' => 'Alignment',
1111
'Allowed Column Types' => 'Allowed Column Types',
1212
'Automatic' => 'Automatic',
13+
'Bold' => 'Bold',
14+
'Bulleted list' => 'Bulleted list',
1315
'Caption' => 'Caption',
1416
'Caption Instructions' => 'Caption Instructions',
1517
'Caption Label' => 'Caption Label',
1618
'Caption Placeholder' => 'Caption Placeholder',
19+
'Choose the formatting buttons available when editing rich-text cells.' => 'Choose the formatting buttons available when editing rich-text cells.',
1720
'CKEditor could not be loaded. You can edit the HTML directly instead.' => 'CKEditor could not be loaded. You can edit the HTML directly instead.',
21+
'Code' => 'Code',
1822
'Enable Caption' => 'Enable Caption',
23+
'Italic' => 'Italic',
24+
'Link' => 'Link',
25+
'Numbered list' => 'Numbered list',
26+
'Rich Text Toolbar' => 'Rich Text Toolbar',
27+
'Strikethrough' => 'Strikethrough',
28+
'Subscript' => 'Subscript',
29+
'Superscript' => 'Superscript',
1930
'The caption field instructions shown to editors.' => 'The caption field instructions shown to editors.',
2031
'The caption field label shown to editors.' => 'The caption field label shown to editors.',
2132
'The caption field placeholder text.' => 'The caption field placeholder text.',
33+
'Underline' => 'Underline',
2234
'Whether editors can set a caption for each table value.' => 'Whether editors can set a caption for each table value.',
2335
'Center' => 'Center',
2436
'Select which column types editors can pick when editing columns. Rich text requires CKEditor 5.0 or later to be installed and enabled.' => 'Select which column types editors can pick when editing columns. Rich text requires CKEditor 5.0 or later to be installed and enabled.',

0 commit comments

Comments
 (0)