ガイド

目次

一覧画面にアクションを追加する


エントリー一覧・モジュール一覧に、独自のボタン・一括操作・メニュー項目を追加します。

対応バージョン: Ver. 3.2.38 以降

実験的機能

この機能は Ver. 3.2 の間は実験的機能として提供し、Ver. 3.3 で正式に提供する予定です。

  • 変わりうる範囲: 登録 API の名前と引数、イベント名とイベントが受け取る値の構造は、マイナーバージョンアップで変更される可能性があります。
  • 変わらない範囲: 従来の ACMS.Config.entryAdminConfig / moduleAdminConfig による一覧のカスタマイズは、今後も動作します。
  • 公開 API の線引き: このページに掲載している登録 API とイベントを公開 API として扱います。掲載外のものも利用できますが、告知なく変更されることがあります。
  • サポート対象外: 管理画面に追加した独自の JavaScript・CSS は、バージョンアップ後の動作を保証しません。これらを原因とする不具合は、サポートの対象外です。

すでに entryAdminConfig でカスタマイズしている場合

従来の ACMS.Config.entryAdminConfig / moduleAdminConfig の getActions / getBulkActions / getMenus は今後も動作します。書き換えは不要です。このページの方法と併用もできます。

行にボタンを追加する

各行の操作ボタンを追加します。次の例は、公開中のエントリーの行に「ID をコピー」ボタンを追加します。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-entry-copy-id', {
    setup(ctx) {
      ctx.dataview.registerAction('entry', 'acme-entry-copy-id/copy', {
        id: 'acme-entry-copy-id/copy',
        label: 'ID をコピー',
        condition: (entry) => entry.status === 'open',
        onAction: async (entry) => {
          await navigator.clipboard.writeText(String(entry.id));
        },
      });
    },
  });
});
指定できる項目説明
idアクションの識別子(必須)
labelボタンのラベル。関数を渡すと行の内容によって変えられます
onAction(行のデータ)クリック時の処理
condition(行のデータ)false を返した行にはボタンを表示しません
type'primary' / 'secondary' / 'tertiary'(見た目)
buttonPropsボタンに渡す属性。行のデータを受け取る関数も渡せます

第1引数は対象の一覧です。エントリー一覧は 'entry'、モジュール一覧は 'module' を指定します。

行のデータは、エントリー一覧ではエントリーの情報(id / title / status / blog.id / category.id など)、モジュール一覧ではモジュールの情報(id / identifier / name / status など)です。

リンクとして表示する

onAction の代わりに getHref を指定すると、ボタンではなくリンクとして描画されます。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-entry-open-page', {
    setup(ctx) {
      ctx.dataview.registerAction('entry', 'acme-entry-open-page/open', {
        id: 'acme-entry-open-page/open',
        label: '公開ページを開く',
        getHref: (entry) => ACMS.Library.acmsLink({ bid: entry.blog.id, eid: entry.id }),
      });
    },
  });
});

一括操作を追加する

複数選択したあとに実行する操作です。一括操作は、選択したあとに操作を選び、フォームの送信ボタンを押して実行する形です。送信ボタンは renderForm で自分で用意します。 renderForm を省略すると、操作を選んでも実行するボタンが表示されません。

ACMS.adminReady(() => {
  const { createElement } = ACMS.Library.React;

  ACMS.plugins.register('acme-entry-bulk', {
    setup(ctx) {
      ctx.dataview.registerBulkAction('entry', 'acme-entry-bulk/list-ids', {
        id: 'acme-entry-bulk/list-ids',
        label: '選択したエントリーの ID を表示',
        renderForm: () =>
          createElement(
            'button',
            { type: 'submit', className: 'acms-admin-btn-admin acms-admin-btn-admin-primary' },
            '実行'
          ),
        onAction: async (formData, entries) => {
          await ACMS.Library.dialog.alert(entries.map((entry) => entry.id).join(', '));
        },
      });
    },
  });
});

onAction には、フォームの内容(FormData)と、選択された行の配列が渡されます。renderForm で入力欄を返せば、その値も FormData に入ります。condition(選択された行の配列) で、操作を選択肢に出すかどうかを切り替えることもできます。

メニュー項目を追加する

一覧上部のメニューに項目を追加します。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-entry-menu', {
    setup(ctx) {
      ctx.dataview.registerMenu('entry', 'acme-entry-menu/count', {
        id: 'acme-entry-menu/count',
        label: '表示件数を確認',
        onAction: async (entries) => {
          await ACMS.Library.dialog.alert(`${entries.length} 件のエントリーを表示しています`);
        },
      });
    },
  });
});

onAction には、一覧に表示されている行の配列が渡されます。

カラムの型を追加する

一覧に表示するカラムの、セルの見た目を追加します。ここで登録した型は、カスタムカラムを追加するときの型の選択肢に label の名前で加わります。

ACMS.adminReady(() => {
  const { createElement } = ACMS.Library.React;

  ACMS.plugins.register('acme-entry-column', {
    setup(ctx) {
      ctx.dataview.registerColumnType('acme-entry-column/badge', {
        label: 'バッジ',
        getValue: (info) =>
          createElement('span', { className: 'acms-admin-label acms-admin-label-info' }, String(info.getValue() ?? '')),
      });
    },
  });
});
指定できる項目説明
getValue(セルの情報)セルに表示する内容を返す関数。info.getValue() でセルの値を取得できます
labelカスタムカラムの型の選択肢に表示する名前(省略可)
filterabletrue にすると、絞り込み条件の対象のカラムに含まれます(省略可)

標準のカラム型と同じ名前で登録すると、標準の定義が上書きされ、その型を使うすべての一覧に影響します。 独自の名前を付けてください。

カラムの並び替えや削除は、この API では行えません。従来の getColumns を使ってください。

一括操作の前に処理を挟みたい場合

「公開状態を一括変更する前に確認したい」といった割り込みは、レジストリではなくイベントを使います。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-entry-confirm', {
    setup(ctx) {
      ctx.events.on('entry.bulk-status.before', async (payload, gate) => {
        const ok = await ACMS.Library.dialog.confirm('選択したエントリーの公開状態を変更します。よろしいですか?');
        if (!ok) {
          gate.cancel(); // 呼ぶと一括変更を中止する
        }
      });
    },
  });
});

一覧画面で使えるイベントは イベント一覧 の「エントリー」「モジュール」を参照してください。

名前の付け方

同じ名前で2回登録するとエラーになります。 プラグイン名/機能名 の形にしておくと衝突しません。'entry' と 'module' は別扱いなので、同じ名前を両方に登録できます。

このページの例を1つのファイルにまとめて試すときは、プラグイン名(ACMS.plugins.register() の第1引数)が重複しないようにしてください。

関連ページ

変更履歴

バージョン変更内容
Ver. 3.2新規追加(実験的機能)

アップロード前にチェックを入れる


画像やファイルをアップロードする前・保存する前に、独自のチェックを挟みます。ファイルサイズの制限、命名規則の確認、外部システムへの問い合わせなどに使えます。

対応バージョン: Ver. 3.2.33 以降

実験的機能

この機能は Ver. 3.2 の間は実験的機能として提供し、Ver. 3.3 で正式に提供する予定です。

  • 変わりうる範囲: イベント名と、イベントが受け取る値の構造は、マイナーバージョンアップで変更される可能性があります。
  • 変わらない範囲: 従来の DOM イベント(acms.media-updated など)と ACMS.addListener は、今後も動作します。
  • 公開 API の線引き: このページに掲載しているイベントを公開 API として扱います。掲載外のものも利用できますが、告知なく変更されることがあります。
  • サポート対象外: 管理画面に追加した独自の JavaScript・CSS は、バージョンアップ後の動作を保証しません。これらを原因とする不具合は、サポートの対象外です。

基本の形

操作の直前に呼ばれるイベント(.before で終わるもの)のハンドラは、(payload, gate) の2つの引数を受け取ります。

引数内容
payload操作の対象になる値(アップロードするファイル、保存する内容など)。直接書き換えられます
gate操作を中止するための gate.cancel() を持つオブジェクト

ハンドラでできることは次の4つです。

やりたいこと書き方
何もしない(見るだけ)何もしない
操作を中止するgate.cancel() または gate.cancel('理由') を呼ぶ
値を書き換えるpayload のプロパティを書き換える
非同期の処理を待たせるasync 関数にする

確認ダイアログを出す

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.upload.before', async (payload, gate) => {
        const ok = await ACMS.Library.dialog.confirm(`${payload.files.length} 件をアップロードします。よろしいですか?`);
        if (!ok) {
          gate.cancel();
        }
      });
    },
  });
});
  • gate.cancel() を呼ぶとアップロードされません。
  • ハンドラを async にすると、a-blog cms はその処理が終わるまでアップロードを待ちます。

理由を表示して中止する

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.upload.before', (payload, gate) => {
        const tooLarge = payload.files.filter((file) => file.size > 5_000_000);
        if (tooLarge.length > 0) {
          gate.cancel(`5MB を超えるファイルはアップロードできません: ${tooLarge[0].name}`);
        }
      });
    },
  });
});

gate.cancel('理由') のように理由を渡すと、その文字列がそのまま利用者に表示されます。理由を伝えたい場合は、理由つきで呼んでください。理由を渡さない gate.cancel() は、何も表示せずに中止します。

チェックを通す場合は何もしなくてよいです(そのまま処理が続きます)。

gate.cancel() を呼ぶと、そのイベントの残りのハンドラは実行されません。

一部のファイルだけ除外する

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.upload.before', (payload) => {
        payload.files = payload.files.filter((file) => file.type !== 'image/gif');
      });
    },
  });
});

payload を書き換えると、その内容が次のハンドラと、実際の処理に渡ります。

アップロードでは「除外」だけができます

media.upload.before では、すでに選択されているファイルを取り除くことはできますが、新しいファイルに差し替えることはできません(圧縮したファイルに置き換える、といった使い方はできません)。差し替えたい場合は、アップロードを中止して独自の処理でアップロードしてください。

保存内容を書き換える

メディアの詳細画面で保存する直前に、送信内容を書き換えられます。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.update.before', (payload) => {
        // field_3 が alt テキスト
        if (!payload.formData.get('field_3')) {
          payload.formData.set('field_3', payload.item.media_title);
        }
      });
    },
  });
});

payload.formData は保存される内容そのものです。値を追加・変更すると、その内容で保存されます。メディアの項目名は、field_1 がキャプション、field_3 が alt テキストです。

alt テキストを必須にしたい場合は、中止を使います。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.update.before', (payload, gate) => {
        if (payload.item.media_type === 'image' && !payload.formData.get('field_3')) {
          gate.cancel('alt テキストを入力してください');
        }
      });
    },
  });
});

alt テキストの入力欄が表示されない設定のとき

alt テキストの入力欄は、設定によっては詳細画面に表示されません。入力欄が見えない状態で alt を必須にすると、利用者が保存できなくなります。

「別名で保存」には、media.update-as-new.before が別に用意されています。受け取る値は media.update.before と同じです。

外部システムに問い合わせる

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.update.before', async (payload, gate) => {
        const response = await fetch('/api/acme/validate-media/', {
          method: 'POST',
          body: payload.formData,
        });
        const result = await response.json();
        if (!result.ok) {
          gate.cancel(result.message);
        }
      });
    },
  });
});

/api/acme/validate-media/ は、検証を行う自前のエンドポイントの例です。非同期のチェックが終わるまで保存は待たされます。ただしユーザーは待たされている状態になるため、時間のかかる処理は避けてください。ハンドラが例外を投げた場合も、操作は中止されます。

アップロード後に処理する

完了後の通知は .success のイベントで受け取ります。こちらは中止できません。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.upload.success', (payload) => {
        console.log(payload.items); // 登録されたメディア
      });
    },
  });
});

削除を止める

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-media-rules', {
    setup(ctx) {
      ctx.events.on('media.delete.before', (payload, gate) => {
        if (payload.mediaIds.length > 10) {
          gate.cancel('一度に削除できるのは 10 件までです');
        }
      });
    },
  });
});

削除では対象の絞り込みはできません。payload.mediaIds を書き換えても、実際に削除される対象は変わりません。一部だけ除外したい場合は、全体を中止したうえで、必要な操作を利用者に案内してください。

どの画面に効くか

メディア関連のイベントは、メディア選択を使うすべての画面で発火します。メディア管理の画面だけでなく、カスタムフィールド・ブロックエディターの画像/ファイル・リッチエディター・ユニットなどからのアップロードにも同じチェックが効きます。

同じプラグイン名を使う場合

このページの例は、どれも同じプラグイン名(acme-media-rules)で書いています。複数の例を1つのファイルで試す場合は、ACMS.plugins.register() を1回にまとめ、setup(ctx) の中に ctx.events.on() を並べてください。同じ名前で2回登録するとエラーになります。

関連ページ

変更履歴

バージョン変更内容
Ver. 3.2新規追加(実験的機能)

オリジナルブロックを作る


ブロックエディターに、独自のブロック(オリジナルブロック)を追加します。

実験的機能

この機能は Ver. 3.2 の間は実験的機能として提供し、Ver. 3.3 で正式に提供する予定です。

  • 変わりうる範囲: 登録 API の名前と引数、ブロック定義に指定できる項目は、マイナーバージョンアップで変更される可能性があります。
  • 変わらない範囲: PHP 側のフック処理(HTML のサニタイズの拡張など)は、この実験的機能の対象外です。従来どおりの仕様として提供しています。
  • 公開 API の線引き: このページに掲載している登録 API とブロック定義の項目を公開 API として扱います。掲載外のものも利用できますが、告知なく変更されることがあります。
  • サポート対象外: 管理画面に追加した独自の JavaScript・CSS は、バージョンアップ後の動作を保証しません。これらを原因とする不具合は、サポートの対象外です。

全体像

オリジナルブロックは2つの部分でできています。

部分何をするか必要な場合
ブロックの定義メニューに出すラベル・アイコンと、挿入したときの動作を決めるほぼ必ず必要
要素の定義(スキーマ)エディターが扱える新しい要素そのものを追加する既存の要素の組み合わせでは表現できない場合

ブロックメニューに表示するかどうかは、管理者が編集設定で選びます。 プラグイン側は「使える種類を提供する」までを行い、メニューへの表示は設定に任せる形です。

ブロックの定義と要素の定義を登録する

次の例は、注意書きを目立たせる「コールアウト」ブロックを追加します。ブロックの定義と要素の定義は、どちらも setup(ctx) の中で登録します。

ACMS.adminReady(() => {
  ACMS.plugins.register('acme-blocks', {
    setup(ctx) {
      // 要素の定義: エディターが扱える「callout」要素を追加する
      ctx.blockEditor.registerExtension('callout', ({ core }) =>
        core.Node.create({
          name: 'callout',
          group: 'block',
          content: 'paragraph+',
          parseHTML: () => [{ tag: 'div[data-callout]' }],
          renderHTML: () => ['div', { 'data-callout': '' }, 0],
        })
      );

      // ブロックの定義: メニューに出すラベルと、挿入したときの動作
      ctx.blockEditor.registerBlockDefinition('callout', () => ({
        label: 'コールアウト',
        description: '注意書きを目立たせるブロック',
        icon: 'info',
        isTextMenu: false,
        aliases: ['callout', 'note'],
        createActions: (cmd, actionCtx) => ({
          action: (editor) => {
            actionCtx.recordUsage(cmd);
            editor
              .chain()
              .focus()
              .insertContent({ type: 'callout', content: [{ type: 'paragraph' }] })
              .run();
          },
          convert: () => {},
          isDisabled: (editor) => editor.isActive('callout'),
          isActive: (editor) => editor.isActive('callout'),
        }),
      }));
    },
  });
});

ブロックの定義に指定できる項目は次のとおりです。

項目説明
labelメニューに表示する名前
descriptionメニューの説明文
iconアイコン名(Material Symbols の名前)
isTextMenu選択したテキストに対する変換メニューに出すかどうか
aliasesメニュー検索で引っかかるキーワード
menuCategory編集設定の選択肢での分類(省略すると「プラグイン」の分類にまとめられます)
createActions挿入・変換・状態判定の処理を返す関数

createActions が返すもの:

関数呼ばれるとき
action(editor)メニューから選ばれたとき(ブロックを挿入する)
convert(editor, pos)既存のブロックをこの種類に変換するとき
isDisabled(editor, pos)変換先として選べないかどうかの判定。true を返すと選べません
isActive(editor)現在のカーソル位置がこのブロックかどうかの判定

上の例のように、他のブロックからの変換に対応しない場合は、convert を何もしない関数にします。

既存のブロック(段落・見出し・画像など)の組み合わせで表現できる場合は、要素の定義(registerExtension)は不要で、ブロックの定義(registerBlockDefinition)だけで足ります。

ブロックエディターは Tiptap をベースにしています。要素の定義方法は Tiptap のドキュメントを参照してください。

Tiptap を自分で読み込まないでください

registerBlockDefinition / registerExtension に渡す関数は、引数として Tiptap を受け取ります。上の例の ({ core }) がそれです。この引数を使ってください。

プラグイン側で @tiptap/core などを import して使うと、ブロックエディター本体とは別の Tiptap が読み込まれ、エディターが正しく動作しません。TypeScript の型として参照する場合(import type)は問題ありません。詳細は 開発環境と型定義 を参照してください。

ブロックメニューに表示する

登録しただけではメニューに出ません。管理画面で表示する設定を行います。

  1. 管理画面の「編集設定」→「ブロックエディター設定」→「ブロックメニュー」を開きます。編集画面セットを使っているサイトでは、使っているセットの「ブロックエディター設定」を開きます(使っていないセットに保存しても、メニューには出ません)。
  2. 「追加」で行を増やし、「ブロック」の選択肢から、登録したブロックを選びます。選択肢には**「プラグイン」の分類**が追加されており、ブロックの定義の label(例: 「コールアウト」)で表示されます。
  3. 保存します。

PHP 側の追加実装は必要ありません。 選択肢は、登録されているブロックの種類から自動的に作られます。

保存した HTML からタグが消える場合

ブロックが出力する HTML は、保存時や表示時に、不正な HTML を防ぐための仕組み(HTML のサニタイズ)を通ります。多くの標準の HTML 要素と、Ver. 3.2.36 以降では data- で始まる属性が、そのまま通ります。上の例の <div data-callout> も、Ver. 3.2.36 以降では設定なしで保存されます。Ver. 3.2.35 以前では、data- 属性も次の Hook で許可が必要です($def->addAttribute('div', 'data-callout', 'CDATA');)。

独自の要素名(例: <acme-note>)や、標準にない属性を出力する場合は、取り除かれないように PHP 側で許可します。extension/acms/Hook.php に、次のメソッドを追加してください。

public function extendsHtmlPurifierDefinition(\HTMLPurifier_HTMLDefinition $def, \HTMLPurifier_Config $config): void
{
    $def->addElement('acme-note', 'Block', 'Flow', 'Common');
}

許可する要素・属性の定義はキャッシュされます。変更したあとは、ダッシュボードからキャッシュをクリアしてください。詳細は フック処理 を参照してください。

関連ページ

変更履歴

バージョン変更内容
Ver. 3.2新規追加(実験的機能)

オリジナルユニットを作る


エントリー本文に、独自の編集画面と保存データを持つユニット(オリジナルユニット)を追加します。

対応バージョン: Ver. 3.2.38 以降

実験的機能

JavaScript 側の登録 API は、Ver. 3.2 の間は実験的機能として提供し、Ver. 3.3 で正式に提供する予定です。

  • 変わりうる範囲: JavaScript 側の登録 API(registerUnitDefinition)の名前と引数、ユニット定義に指定できる項目は、マイナーバージョンアップで変更される可能性があります。
  • 変わらない範囲: PHP 側のユニット登録(unit-registry)は、この実験的機能の対象外です。従来どおりの仕様として提供しています。
  • 公開 API の線引き: このページに掲載している登録 API とユニット定義の項目を公開 API として扱います。掲載外のものも利用できますが、告知なく変更されることがあります。
  • サポート対象外: 管理画面に追加した独自の JavaScript・CSS は、バージョンアップ後の動作を保証しません。これらを原因とする不具合は、サポートの対象外です。

カスタムユニットとオリジナルユニット

a-blog cms には、もともとカスタムユニットという標準の機能があります。このページで作るオリジナルユニットとは別のものです。

カスタムユニット(標準機能)オリジナルユニット(このページ)
作り方ユニットメニューに custom_ で始まるモードを追加し、編集画面用と表示用のテンプレートに入力欄と出力を書くJavaScript で編集画面を、PHP で保存と表示を実装する
独自の編集 UIテンプレートに書いた入力欄自由に作れる(React などで実装できる)
独自の保存形式カスタムフィールドとして保存される自由に決められる
PHP の実装不要必要
向いているケース入力欄の組み合わせで足りる専用の入力画面・独自の保存形式が必要

まずカスタムユニットで足りないかを検討してください。 カスタムユニットなら PHP の実装が不要で、運用も簡単です。入力欄の組み合わせで足りない場合に、オリジナルユニットを作ります。

標準のカスタムユニットの編集画面そのものを JavaScript で差し替えることもできますが、その差し替えはすべてのカスタムユニットに適用されます。特定のカスタムユニットだけを差し替えることはできないため、個別の編集画面が必要な場合は、オリジナルユニットを作ってください。

作業のチェックリスト

オリジナルユニットは、次の5つが揃わないと動きません。どれか1つでも欠けると、保存できない・表示されない・データが消えるといった状態になります。

  • JavaScript で編集画面を登録する
  • PHP でユニットを登録する
  • 編集設定でユニットメニューに追加する
  • ユニット設定で、メニューにユニットを割り当てる
  • テーマにユニットの表示テンプレートを書く

JavaScript で編集画面を登録する

次の例は、文字列を1つ入力できる「グラフ」ユニットの編集画面です。入力欄の name は、ユニットの種類名_項目名_ユニットID の形にします。PHP 側の保存処理がこの名前で値を受け取ります。

ACMS.adminReady(() => {
  const { createElement } = ACMS.Library.React;

  function ChartEdit({ editor, unit }) {
    return createElement(
      'div',
      { className: 'acms-admin-unit-content' },
      createElement('input', {
        type: 'text',
        name: `my-chart_value_${unit.id}`,
        className: 'acms-admin-form-width-full',
        'aria-label': 'グラフの値(カンマ区切りの数値)',
        placeholder: '10,20,30',
        value: unit.attributes.value ?? '',
        onChange: (event) => editor.commands.setUnitAttributes(unit.id, { value: event.target.value }),
      }),
      createElement(
        'button',
        { type: 'button', className: 'acms-admin-btn', onClick: () => editor.commands.moveUpUnit(unit.id) },
        '上へ'
      ),
      createElement(
        'button',
        { type: 'button', className: 'acms-admin-btn', onClick: () => editor.commands.moveDownUnit(unit.id) },
        '下へ'
      ),
      createElement(
        'button',
        { type: 'button', className: 'acms-admin-btn', onClick: () => editor.commands.removeUnit(unit.id) },
        '削除'
      )
    );
  }

  ACMS.plugins.register('acme-units', {
    setup(ctx) {
      ctx.unitEditor.registerUnitDefinition('my-chart', {
        type: 'my-chart',
        name: 'グラフ',
        icon: 'bar_chart',
        edit: ChartEdit,
        supports: {
          multiple: true,
          nested: true,
        },
      });
    },
  });
});

登録は ACMS.adminReady() の中で行います

ユニットの登録は、ほかのプラグインの登録と同じく ACMS.adminReady() の中で行います。管理画面を表示したあとに登録した場合も、表示済みのエントリー編集・ダイレクト編集・ユニット設定の画面に、そのユニットが反映されます。

プラグインの JS は、<script type="module"> で読み込みます(はじめてのプラグイン を参照)。これは、実行順を揃えるための指定です。

edit に渡されるのは、unit(このユニットのデータ)と editor(ユニットエディター)などです。unit.attributes が、PHP 側の getAttributes() で返した値です。editor.commands で、データの更新・移動・削除などの操作ができます。

この例では、ユニットの移動と削除のボタンを自分で用意しています。標準のユニットが持つツールバーは、オリジナルユニットでは自動では付きません。

項目説明
typeユニットの種類名(登録名と同じにします)
name管理画面に表示する名前
iconアイコン名(Material Symbols の名前)
edit編集画面を返す関数
config編集設定画面での設定 UI を返す関数(省略可)
inplaceEditダイレクト編集用の UI を返す関数(省略可)。省略すると、ダイレクト編集では「対応していません」と表示されます
supports複数配置・入れ子・複製・移動の可否(省略可)

この登録だけで、エントリー編集・ダイレクト編集・編集設定の3つの画面すべてに反映されます。

種類名にアンダースコア(_)を使わないでください

a-blog cms は _ より前の部分をユニットの種類として扱います。my_chart は my として解釈され、意図しない動作になります。ハイフン区切り(my-chart)を使ってください。入力欄の name の区切りに使う _ は、この制約とは関係ありません。

PHP でユニットを登録する

保存・読み込み・サイト表示側の出力を担当するクラスを登録します。プラグインの ServiceProvider の init() に、次のように書きます。

use Acms\Services\Facades\Application;
use Acms\Services\Unit\Registry;

Application::bootstrap('unit-registry', function (Registry $registry) {
    $registry->bind('my-chart', \Acms\Plugins\AcmeUnits\Unit\MyChart::class);
});

登録するクラスは、Acms\Services\Unit\Contracts\Model を継承します。次の例は、JavaScript の例に対応する最小の実装です。

<?php

namespace Acms\Plugins\AcmeUnits\Unit;

use Acms\Services\Unit\Contracts\Model;
use Template;

/**
 * @extends \Acms\Services\Unit\Contracts\Model<array{value: string}>
 */
class MyChart extends Model
{
    /**
     * @var array{value: string}
     */
    private $attributes = ['value' => ''];

    public static function getUnitType(): string
    {
        return 'my-chart';
    }

    public static function getUnitLabel(): string
    {
        return 'グラフ';
    }

    public function getAttributes()
    {
        return ['value' => $this->getField1()];
    }

    public function setAttributes($attributes): void
    {
        $this->attributes = $attributes;
    }

    public function setDefault(string $configKeyPrefix, int $configIndex): void
    {
        $this->setField1(config("{$configKeyPrefix}field_1", '', $configIndex));
    }

    public function extract(array $request): void
    {
        $id = $this->getId();
        if (is_null($id)) {
            throw new \LogicException('Unit ID must be set before calling extract');
        }
        $value = $request["my-chart_value_{$id}"] ?? '';
        $this->setField1(is_string($value) ? $value : '');
    }

    public function canSave(): bool
    {
        return $this->getField1() !== '';
    }

    public function handleDuplicate(): void
    {
    }

    public function handleRemove(): void
    {
    }

    public function getSearchText(): string
    {
        return $this->getField1();
    }

    public function getSummaryText(): array
    {
        return [$this->getField1()];
    }

    public function render(Template $tpl, array $vars, array $rootBlock): void
    {
        if ($this->getField1() === '') {
            return;
        }
        $vars += ['value' => $this->getField1()];
        $tpl->add(array_merge(['unit#' . $this->getType()], $rootBlock), $vars);
    }

    public function renderEdit(Template $tpl, array $vars, array $rootBlock): void
    {
    }

    protected function getLegacy(): array
    {
        return ['value' => $this->getField1()];
    }
}

値は、ユニットが共通で持つ field1 に保存しています。getUnitType() が返す種類名は、bind() に渡した名前と同じにします。getUnitLabel() の値は、編集設定の入力候補やユニットメニューでの名前に使われます。

この登録を忘れると、データが警告なく失われます

PHP 側に登録されていない種類のユニットは、保存時・読み込み時・表示時のすべてで、エラーもログも出さずに無視されます。「保存したはずのユニットが消えている」という形で表面化するため、原因が分かりにくい不具合になります。

オリジナルユニットを作る場合は、JavaScript の登録と PHP の登録を必ずセットで行ってください。

Application::bootstrap() に渡すコールバックは、unit-registry が使われるたびに実行されます。bind() のように何度実行しても同じ結果になる処理だけを書いてください(カウントアップや外部通信は書かないでください)。

編集設定でユニットメニューに追加する

  1. 管理画面の「編集設定」→「ユニットメニュー」を開きます。編集画面セットを使っているサイトでは、使っているセットの「編集設定」を開きます。
  2. 「モード」の入力欄に種類名(例: my-chart)を入力します。PHP に登録済みの種類は入力候補として表示されます。 「ラベル」「アイコン」も必要に応じて入力します。
  3. 保存します。

PHP 側の追加実装は必要ありません。 入力候補は登録済みのユニットから自動的に作られます。

ユニット設定で、メニューにユニットを割り当てる

メニューの項目を追加しただけでは、エントリー編集画面のユニットメニューに出ません。どのユニットを追加する項目なのかを、「ユニット設定」で指定します。

  1. 管理画面の「編集設定」の隣にある「ユニット設定」を開きます(編集画面セットを使っているサイトでは、同じセットのもの)。
  2. 追加した項目(例: 「グラフ」)の欄に、ユニットの種類のボタンが並びます。「グラフ」のボタンを押して、ユニットを1つ追加します。
  3. 保存します。

保存すると、エントリー編集画面の「ユニットを追加」に、「グラフ」が出ます。

テーマに表示テンプレートを書く

サイトに表示するときの HTML は、テーマのテンプレートに書きます。テーマの include/unit/extend.html に、unit#種類名 のブロックを追加します。render() で渡した値が、ブロックの中の変数になります。


<div class="my-chart" data-values="{value}">{value}</div>

テンプレートに書かれていない種類のユニットは、サイトに何も表示されません。

ツールバーにボタンを追加する

標準のツールバーを使うユニット(標準のユニットなど)には、UI の差し込み(Slot / Fill)でボタンを追加できます。次の例は、ツールバーに「ID をコピー」ボタンを追加します。

ACMS.adminReady(() => {
  const { createElement } = ACMS.Library.React;
  const { withFillProps } = ACMS.plugins;

  ACMS.plugins.register('acme-unit-toolbar', {
    setup(ctx) {
      ctx.ui.fill(
        'unit-editor.unit-toolbar',
        withFillProps(({ unit }) =>
          createElement(
            'button',
            {
              type: 'button',
              className: 'acms-admin-btn',
              onClick: () => navigator.clipboard.writeText(unit.id),
            },
            'ID をコピー'
          )
        )
      );
    },
  });
});

→ UI の差し込み(Slot / Fill)

関連ページ

変更履歴

バージョン変更内容
Ver. 3.2新規追加(実験的機能)

管理画面と同じ見た目で画面を作る


プラグインの設定画面や独自の管理ツールを、a-blog cms の管理画面と同じデザインで作ります。方法は2つあり、必要に応じて使い分けます。

対応バージョン: CSS クラスを使う方法は制限なし。React コンポーネントを管理画面と共有する方法は Ver. 3.2.38 以降

実験的機能

この機能は Ver. 3.2 の間は実験的機能として提供し、Ver. 3.3 で正式に提供する予定です。

  • 変わりうる範囲: React コンポーネントの名前・props と、ACMS.Library で共有するもの(React・コンポーネント)は、マイナーバージョンアップで変更される可能性があります。
  • 変わらない範囲: CSS クラスを使う方法と、PHP 側のテンプレート挿入・アセット API は、実験的機能の対象ではありません。
  • 公開 API の線引き: npm パッケージ のページに掲載しているものを公開 API として扱います。掲載外のコンポーネントも利用できますが、告知なく変更されることがあります。
  • サポート対象外: 管理画面に追加した独自の JavaScript・CSS は、バージョンアップ後の動作を保証しません。これらを原因とする不具合は、サポートの対象外です。
方法必要な環境向いているケース
CSS クラスを使うなしフォーム・テーブル・ボタン中心の設定画面
React コンポーネントを使うビルド環境検索・並び替え・モーダルなど動きのある UI

CSS クラスを使う

管理画面のスタイルは CSS フレームワークとして提供しています。テンプレートに管理画面と同じクラス名を書くだけで、同じ見た目になります。フォームのクラスは、acms-admin-form を付けた要素の内側で有効になります。

<div class="acms-admin-form">
  <label for="api-key" class="acms-admin-form-label">API キー</label>
  <input type="text" id="api-key" name="api_key" class="acms-admin-form-width-full">
  <button type="submit" class="acms-admin-btn acms-admin-btn-primary">保存</button>
</div>

管理画面の中に差し込む場合、CSS は管理画面側で読み込まれているため、追加の読み込みは不要です。

管理画面の外で使う

独自のツール画面など、管理画面のテンプレートの外で使う場合は、CSS を npm から取得できます。

npm install @ablogcms/acms.css@3.2.37
import '@ablogcms/acms.css';

バージョンは、利用している a-blog cms と同じものを指定してください(3.2.37 は例です)。

React コンポーネントを使う

管理画面で使っている React コンポーネントを npm パッケージとして公開しています。

npm install @ablogcms/components@3.2.37

バージョンは、利用している a-blog cms と同じものを指定してください(3.2.37 は例です)。

次の例は、管理画面に差し込んだテンプレートの <div id="acme-settings"></div> の中に、設定フォームを描画します。

import { createRoot } from 'react-dom/client';
import { Button } from '@ablogcms/components/button';
import { Field, FieldControl, FieldLabel } from '@ablogcms/components/field';
import { Input } from '@ablogcms/components/input';

function SettingsForm() {
  return (
    <form className="acms-admin-form">
      <Field>
        <FieldLabel>API キー</FieldLabel>
        <FieldControl>
          <Input name="api_key" width="full" />
        </FieldControl>
      </Field>
      <Button type="submit" variant="primary">保存</Button>
    </form>
  );
}

ACMS.adminReady(() => {
  const container = document.getElementById('acme-settings');
  if (container !== null) {
    createRoot(container).render(<SettingsForm />);
  }
});

コンポーネントのスタイルは、acms-admin-form を付けた要素の内側で有効になります。上の例のように、フォームを acms-admin-form で囲んでください。

実際の表示と指定できる値を確認する

コンポーネントの一覧・実際の表示・指定できる props は Storybook で確認できます。

Storybook に載っているコンポーネントの扱い

Storybook には管理画面で使っているコンポーネントを掲載しています。利用は自由ですが、npm パッケージ のページで説明しているもの以外は、告知なく変更されることがあります。バージョンを固定して利用してください。

React と管理画面のコンポーネントを同梱しないでください

管理画面の中で動かす場合、React と react-dom はプラグイン側でバンドルしないでください。管理画面が読み込んでいる React を使います。ACMS.Library.React と ACMS.Library.ReactDomClient から取得できます。

ACMS.adminReady(() => {
  const { createElement, useState } = ACMS.Library.React;
  const { createRoot } = ACMS.Library.ReactDomClient;

  function Counter() {
    const [count, setCount] = useState(0);
    return createElement(
      'button',
      { type: 'button', className: 'acms-admin-btn', onClick: () => setCount(count + 1) },
      `クリック数: ${count}`
    );
  }

  const container = document.getElementById('acme-counter');
  if (container !== null) {
    createRoot(container).render(createElement(Counter));
  }
});

@ablogcms/components のコンポーネントも、同梱せずに管理画面のものを共有できます(ACMS.Library.components.button のような形で参照できます)。共有されていないコンポーネント(dataview / date-picker / time-picker / syntax-highlight / rich-select / select / device-mode / icon-picker)は、プラグイン側に同梱されます。ビルド設定で React とコンポーネントを外部依存にする方法は 開発環境と型定義 を参照してください。

設定値を JavaScript に渡す

PHP 側の設定値は ctx.config から読み取れます。PHP で jsModule() に、プラグイン名をキャメルケースにしたキーで渡します(acme-tools のプラグインなら acmeTools)。

jsModule('acmeTools', ['apiEndpoint' => '/api/acme/']);
ACMS.adminReady(() => {
  ACMS.plugins.register('acme-tools', {
    setup(ctx) {
      console.log(ctx.config.apiEndpoint);
    },
  });
});

値は配列(連想配列)で渡します。数値や真偽値も、JavaScript 側では文字列として届きます。

API キーなどの秘密の情報は渡さないでください(ブラウザから閲覧できます)。制約の詳細は プラグイン API を参照してください。

関連ページ

変更履歴

バージョン変更内容
Ver. 3.2新規追加(実験的機能)