Best practice for extending RTE Table: Custom buttons for Cell Background Color and CSS Class
Hi everyone,
I am working on extending the AEM Rich Text Editor (RTE) to add custom functionality for tables. Specifically, I want to add two buttons to the table toolbar:
-
Cell Background Color (
origintable#cellbg) -
Cell CSS Class (
origintable#cellclass)
I have implemented a custom Plugin and Command using CUI.rte.plugins.Plugin and CUI.rte.commands.Command. While the current implementation works using window.prompt, I would like to know if this approach is considered "best practice" or if there are more "AEM-native" ways to handle this (e.g., using Coral UI dialogs instead of browser prompts).
Current Implementation Details:
-
Plugin Registration: Using
CUI.rte.plugins.PluginRegistry.register. -
UI Initialization: Adding elements to the
tableEditOptionstoolbar viatbGenerator.addElement. -
Data Persistence: Manually triggering the
changeevent on the editable container after updating the DOM via jQuery.
Code Snippets:
1. Plugin Logic (cell-bg-plugin.js):
(function ($, CUI) {
'use strict';
var GROUP = "origintable",
FEATURE_BG = "cellbg",
FEATURE_CLASS = "cellclass";
window.originPlugins = window.originPlugins || {};
function registerPlugin() {
// 1. 依存オブジェクトのロード待ち
if (typeof window.Class === 'undefined' || !window.CUI || !window.CUI.rte || !window.CUI.rte.plugins || !window.CUI.rte.plugins.Plugin) {
if (!window.originRetryCount || window.originRetryCount < 15) {
window.originRetryCount = (window.originRetryCount || 0) + 1;
setTimeout(registerPlugin, 200);
}
return;
}
// 二重登録防止
if (window.originPlugins[GROUP]) {
console.log("Plugin already registered:", GROUP);
return;
}
// 2. プラグインクラスの定義
var TableExtensionPlugin = new window.Class({
extend: CUI.rte.plugins.Plugin,
toString: "TableExtensionPlugin",
getFeatures: function () {
return [FEATURE_BG, FEATURE_CLASS];
},
// ツールバーの初期化(ここで2つのボタンを並べる)
initializeUI: function (tbGenerator) {
// 1. 背景色ボタン
if (this.isFeatureEnabled(FEATURE_BG)) {
this.bgBtn = tbGenerator.createElement(FEATURE_BG, this, false, "セル背景色の設定");
// アイコン設定 (エラー回避のためプロパティ直接指定を試行)
this.bgBtn.icon = "circle";
tbGenerator.addElement(GROUP, CUI.rte.plugins.Plugin.SORT_FORMAT, this.bgBtn, 10);
}
// 2. クラス付与ボタン (ここがポイント!)
if (this.isFeatureEnabled(FEATURE_CLASS)) {
this.classBtn = tbGenerator.createElement(FEATURE_CLASS, this, false, "セルクラスの設定");
this.classBtn.icon = "tag";
tbGenerator.addElement(GROUP, CUI.rte.plugins.Plugin.SORT_FORMAT, this.classBtn, 20);
}
},
updateState: function (selDef) {
// ボタンの状態をリセット
if (this.bgBtn) { this.bgBtn.setSelected(false); }
if (this.classBtn) { this.classBtn.setSelected(false); }
},
execute: function (id, value, env) {
var context = this.editorKernel.editContext;
var $cells = this._getSelectedCells(context);
if ($cells.length === 0) {
alert("セルを選択してからボタンを押してください。");
return;
}
if (id === FEATURE_BG) {
this._handleBgColor($cells, context);
} else if (id === FEATURE_CLASS) {
this._handleClass($cells, context);
}
},
// 共通:選択セルの取得
_getSelectedCells: function(context) {
var $cells = $(context.root).find('.rte-tableselection');
if ($cells.length === 0) {
var win = context.win || window;
var sel = win.getSelection();
if (sel && sel.rangeCount > 0) {
var $closest = $(sel.anchorNode).closest('td, th');
if ($closest.length > 0 && $.contains(context.root, $closest[0])) {
$cells = $closest;
}
}
}
return $cells;
},
// 背景色処理
_handleBgColor: function($cells, context) {
var current = $cells.first().css("background-color");
if (current === "rgba(0, 0, 0, 0)" || current === "transparent") current = "";
var color = window.prompt("背景色を入力 (rgb, rgba等):", current);
if (color !== null) {
$cells.css("background-color", color.trim());
this._save(context);
}
},
// クラス処理
_handleClass: function($cells, context) {
var currentClass = $cells.first().attr("class") || "";
currentClass = currentClass.replace("rte-tableselection", "").trim();
var newClass = window.prompt("クラス名を入力 (複数時はスペース区切り):", currentClass);
if (newClass !== null) {
$cells.each(function() {
var $this = $(this);
var isSelected = $this.hasClass("rte-tableselection");
$this.attr("class", newClass.trim());
if (isSelected) $this.addClass("rte-tableselection");
if (!$this.attr("class")) $this.removeAttr("class");
});
this._save(context);
}
},
_save: function(context) {
var latestHtml = $(context.root).html();
var $container = $(context.root).closest('div[data-cq-richtext-editable]');
if ($container.length === 0) $container = $('div[name="./text"]');
if ($container.length > 0) {
$container.attr("value", latestHtml);
$container.trigger("change");
}
}
});
// 3. 登録の実行
CUI.rte.plugins.PluginRegistry.register(GROUP, TableExtensionPlugin);
window.originPlugins[GROUP] = true;
}
registerPlugin();
})(jQuery, window.CUI);
2. Dialog Configuration (CRX): I have added the plugin to rtePlugins and updated uiSettings/cui/tableEditOptions to include origintable#cellbg and origintable#cellclass.
Specific Questions:
-
Is manually manipulating the cell's CSS/Attributes via jQuery and then triggering a
changeevent the reliable way to ensure the content is saved back to the JCR? -
Is there a built-in way to leverage Coral UI dialogs (popovers) for user input within a custom RTE plugin, rather than using
window.prompt? -
In
_save, I am looking fordiv[data-cq-richtext-editable]. Is there a more standard RTE API to persist the current buffer to the underlying resource?
I would appreciate any feedback or suggestions on how to make this implementation more robust and aligned with AEM's core architecture.
Thanks in advance!