要約
Redmineのチケットなどの編集画面のボタンのカスタマイズにRedmine jsToolbar Extensionプラグインを修正しながら細々と使ってたんですが、これぐらいの修正ならview customize pluginでチャチャっとできるんじゃないかと調べ始めたらなかなか苦労したので、手順をまとめます。
環境
以下の環境で確認しています。
Redmine 6.0.6.stable
view customize plugin: 3.5.2
手順
以下、ツールバーの「コード」ボタンの右に「区切り線」、「引用解除」ボタンの右に「折りたたみ(Collapse)」ボタンを追加するサンプルになります。
view customize pluginをインストールしてあるRedmineで、plugins/view_customize/lib/redmine_view_customize/view_hook.rb に以下のパッチを当てます。
@@ -5,15 +5,16 @@
def view_layouts_base_html_head(context={})
path = sanitize(Redmine::CodesetUtil.replace_invalid_utf8(context[:request].path_info));
- html = "\n<!-- [view customize plugin] path:#{path} -->\n"
- html << stylesheet_link_tag("view_customize", plugin: "view_customize")
- html << "<script type=\"text/javascript\">\n//<![CDATA[\n"
- html << "ViewCustomize = { context: #{create_view_customize_context(context).to_json} };"
- html << "\n//]]>\n</script>\n"
-
- html << create_view_customize_html(context, ViewCustomize::INSERTION_POSITION_HTML_HEAD)
-
- return html
+ context[:hook_caller].content_for(:header_tags) do
+ safe_join([
+ raw("\n<!-- [view customize plugin] path:#{path} -->\n"),
+ stylesheet_link_tag("view_customize", plugin: "view_customize"),
+ raw("<script type=\"text/javascript\">\n//<![CDATA[\n"),
+ raw("ViewCustomize = { context: #{create_view_customize_context(context).to_json} };"),
+ raw("\n//]]>\n</script>\n"),
+ create_view_customize_html(context, ViewCustomize::INSERTION_POSITION_HTML_HEAD),
+ ], "\n")
+ end
end
def view_layouts_base_body_bottom(context={})
Redmine管理画面の「表示のカスタマイズ」を開いて、以下のようなJavaScriptコードを追加します。
| 項目 | 値 |
|---|---|
| パスのパターン | / |
| プロジェクトのパターン | (空白) |
| 挿入位置 | 全ページのヘッダ |
| 種別 | JavaScript |
| コード | (表下のコード) |
| コメント | jsToolBarのカスタマイズ(とか) |
| 有効 | はい |
| プライベート | いいえ |
(function() {
if (typeof jsToolBar === 'undefined') return;
var i, e = {}, p = jsToolBar.prototype;
for (i in p.elements) {
e[i] = p.elements[i];
if (i === 'code') {
// Horizontal Rule ボタン追加
e.hr = {
type: 'button',
title: 'Horizontal Rule',
fn: {
wiki: function () {
this.encloseLineSelection('\n---\n', '', function (str) {
return str.length > 0 ? str + '\n' : str;
});
}
}
};
} else if (i === 'unbq') {
// Collapse ボタン追加
e.collapse = {
type: 'button',
title: 'Collapse',
fn: {
wiki: function () {
this.encloseLineSelection('{{collapse(View details...)\n', '\n}}');
}
}
};
}
}
p.elements = e;
})();
次に、ボタン用のSVGをCSSとして登録します。
| 項目 | 値 |
|---|---|
| パスのパターン | / |
| プロジェクトのパターン | (空白) |
| 挿入位置 | 全ページのヘッダ |
| 種別 | CSS |
| コード | (表下のコード) |
| コメント | jsToolBar追加ボタン用SVG(とか) |
| 有効 | はい |
| プライベート | いいえ |
.jstb_hr { background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-minus"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /></svg>') }
.jstb_collapse { background-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" fill="%23000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>collapse-vertical</title> <g id="Layer_2" data-name="Layer 2"> <g id="invisible_box" data-name="invisible box"> <rect width="48" height="48" fill="none"></rect> </g> <g id="icons_Q2" data-name="icons Q2"> <g> <path d="M22.6,15.4a1.9,1.9,0,0,0,2.8,0l6-5.9a2.1,2.1,0,0,0,.2-2.7,1.9,1.9,0,0,0-3-.2L26,9.2V4a2,2,0,0,0-4,0V9.2L19.4,6.6a1.9,1.9,0,0,0-3,.2,2.1,2.1,0,0,0,.2,2.7Z"></path> <path d="M25.4,32.6a1.9,1.9,0,0,0-2.8,0l-6,5.9a2.1,2.1,0,0,0-.2,2.7,1.9,1.9,0,0,0,3,.2L22,38.8V44a2,2,0,0,0,4,0V38.8l2.6,2.6a1.9,1.9,0,0,0,3-.2,2.1,2.1,0,0,0-.2-2.7Z"></path> <path d="M6,22H42a2,2,0,0,0,0-4H6a2,2,0,0,0,0,4Z"></path> <path d="M42,26H6a2,2,0,0,0,0,4H42a2,2,0,0,0,0-4Z"></path> </g> </g> </g> </g></svg>') }
各アイコンのSVGは以下のURLの物を使わせていただいています。
| アイコン | URL |
|---|---|
| .jstb_hr | https://tabler.io/icons/icon/minus |
| .jstb_collapse | https://www.svgrepo.com/svg/445655/collapse-vertical |
記述のコツ
- ボタンの位置は、前のボタンの名前を調べて
if (i === 'code') {という条件式で囲めば、'code'の後ろに'hr'ボタンを追加、といった処理になります。名前はHTMLのソースコードのクラス名(次項参照)から取得します。 - JavaScriptの記述中の変数名として記述した'hr'や'collapse'に対応するCSSのクラスが'jstb_hr', 'jstb_collapse'になります("jstb_" + "変数名"という書式)。
- SVGは、上記URLから取得したテキストの '#' 記号を '%23' に変換して、 background-image: url('data:image/svg+xml, [ここにコピぺ]')しています(ここはお好みの方法で)。
- SVGのサイズ指定として「width="16" height="16"」を使うと前後のボタンとバランスの取れた表示になります
解説
当初、JavaScript(のfunction版)とCSSの登録だけでいけるはずだと思っていたんですが、JavaScriptの挿入位置の関係上、
- コメントの”編集"ボタンなどで開く編集フォームには反映される
- チケットの編集フォームやコメントの追加用フォームには反映されない
という状況でした。一部のプラグイン(DMSFなど)ではボタンの追加もできているので、何が違うのか調べたところ、生成されるHTMLの
が、<!-- [view customize plugin] path:/projects/#####/issues/new -->
:
(view customizeで「全ページのヘッダ」に指定したコード)
:
<!-- page specific tags -->
:
(jstoolbar.jsやdatepicker.jsなどのRedmine同梱JSライブラリの<script>タグ)
:
(編集画面のツールバーを書き換えているプラグインのコード)
という順番になっていて、view customize プラグインのコードによるカスタマイズが反映できない状態でした。
では、とChatGPTと壁打ちしながら
// 既存のツールバーを再描画する
const textarea = document.getElementById('issue_description');
if (textarea) {
console.log('✅ Redrawing toolbar with new buttons');
const wikiToolbar = new jsToolBar(textarea);
wikiToolbar.setHelpLink('/help/wiki_syntax');
wikiToolbar.setPreviewUrl('/issues/preview?project_id=general_work');
wikiToolbar.draw();
}
とかやったりしてみたんですが、上記のように安直な再描画処理ではツールバーが2重表示になってDOMの構造が壊れたり、チケットの説明フォームとコメントの追加フォームでそもそも構造が違っているなどあって、うまくいきませんでした。
で、他のプラグインのコードをあらためて漁って書いたパッチが最初のやつになります。これで、
タグ内の末尾にview customizeの「全ページのヘッダ」で指定したコードが、<!-- page specific tags -->
:
(jstoolbar.jsやdatepicker.jsなどのRedmine同梱JSライブラリの<script>タグ)
:
(編集画面のツールバーを書き換えているプラグインのコード)
:
<!-- [view customize plugin] path:/projects/#####/issues/new -->
:
(view customizeで「全ページのヘッダ」に指定したコード)
という順番で生成されるようになりました。
なお、(あまり考えにくいですが)元々の順番でうまく動いていたコードが動かなくなる可能性もありますので、パッチ当てる際は、あくまで自己責任で慎重に作業することをお勧めします。
