MTAppjQuery で特定のユーザーに記事の「公開」「削除」をさせない方法
目次
Movable Type で記事を運用していると、「保存や更新はしてほしいけれど、公開だけは承認を経てからにしたい」という相談をいただくことがあります。編集担当のかたには自由に下書きを作ってもらいつつ、公開の判断は責任者が行う、という運用ですね。あわせて、「記事一覧の画面で、うっかり公開したり削除したりしてしまうのを防ぎたい(下書きに戻すのは許可してよい)」というご要望をいただくこともあります。
MTAppjQuery には「公開ボタンを隠す」といった専用の機能はありません。ですが、user.js に少しだけコードを書くことで、この要望に近い形を実現できます。今回は編集画面と一覧画面の両方について、その方法を、つまずきやすいポイントも含めてご紹介します。
はじめに知っておきたい MT の「公開ボタン」の仕組み
はじめに、Movable Type の編集画面の作りを確認しておきましょう。ここを理解しておくと、なぜ単純に「ボタンを消す」だけでは済まないのかが見えてきます。
記事編集画面の右側には、青い保存ボタンが並んでいます。一見すると「公開」という独立したボタンがあるように思えますが、実はそうではありません。MT の編集画面には、ステータスを選ぶ <select name="status"> があり、その値に応じて保存ボタンの動作とラベルが「公開」「下書き保存」「更新」と切り替わる仕組みになっています。
つまり、保存ボタンそのものを非表示にしたり無効化したりすると、公開だけでなく保存や更新まで一緒に止まってしまいます。これでは「保存はさせたい」という要望を満たせません。
そこで今回は、ボタンではなくステータスの選択肢のほうを制御するアプローチを取ります。ステータスから「公開」だけを選べなくすれば、保存と更新は通常どおり行いつつ、公開だけを抑えられます。
公開済みの記事を壊さないためのポイント
方針が決まったので、「公開」の <option> に disabled を付けて選べなくしていきます。
ここで一つ、選択肢の見分け方に触れておきます。「公開」という表示テキストで判定することもできますが、ラベルは環境によって変わることがあります。たとえば「下書き」が「未公開(原稿)」と表示される環境もあります。一方、Movable Type のステータスは値が固定されていて、公開は 2 です(未公開は 1、日時指定は 4)。ですから、表示テキストよりも value で判定するほうが確実です。なお <option> の value は文字列として扱われるため、数値の 2 ではなく文字列の '2' と比較します。
これを踏まえて素直に書くと、こうなります。冒頭の 1 行は、対象となるステータス選択欄を取得しているところです。
const statusSelect = document.querySelector('select[name="status"]');
// 公開 = ステータス値 2。option.value は文字列なので '2' と比較する
Array.from(statusSelect.options).forEach((option) => {
if (option.value === '2') {
option.disabled = true;
}
});
一見これで良さそうなのですが、ここに落とし穴があります。
HTML の仕様では、disabled を付けた <option> は、たとえ選択された状態であってもフォーム送信のデータから除外されます。すると、すでに公開済みの記事を対象のユーザーが開いて更新しようとしたときに問題が起きます。公開済みの記事は最初から公開が選択されていますから、その状態で disabled を付けると、status の値が送信されずに保存時のエラーにつながってしまうのです。
対処はシンプルです。ページを読み込んだ時点ですでに公開が選ばれている記事は、disabled を付けないようにします。未公開の記事だけ公開を選べなくすれば、公開済みの記事の更新はこれまでどおり通ります。
const statusSelect = document.querySelector('select[name="status"]');
// 読み込み時点で公開(値 2)が選ばれているか。select.value も文字列で返る
const initiallyPublished = statusSelect.value === '2';
if (!initiallyPublished) {
Array.from(statusSelect.options).forEach((option) => {
if (option.value === '2') {
option.disabled = true;
}
});
}
注釈を「他のフィールドと同じ余白」で添える
選択肢を制限するだけだと、編集担当のかたは「なぜ公開できないのだろう」と戸惑ってしまいます。そこで、公開ウィジェットの下に説明の一文を添えておきましょう。
ここでも小さな工夫が要ります。公開ウィジェット(#entry-publishing-widget)に直接テキストを追加すると、左右の余白が付かず、枠線ギリギリに文字が寄ってしまいます。保存ボタンと同じ内側のコンテナに追加すると、他のフィールドと揃った自然な余白になります。
const publishingWidget = document.querySelector('#entry-publishing-widget');
const note = document.createElement('p');
note.className = 'text-muted small mt-2 mb-0';
note.textContent = 'このアカウントでは公開できません。公開には承認者による操作が必要です。保存・更新は通常どおり行えます。';
const primaryButton = publishingWidget.querySelector('.btn-primary[name="status"]');
const noteContainer = primaryButton?.parentElement ?? publishingWidget;
noteContainer.appendChild(note);
一覧画面でも「公開」と「削除」を防ぐ
ここまでは編集画面のお話でした。ここからは、記事の一覧画面についても見ていきます。「一覧でうっかり公開してしまった」「一覧でうっかり削除してしまった」というのは、実際によく起きる事故です。一方で「下書きに戻す(非公開化)」はしてもらってよい、というのが今回のご要望でした。
まず、一覧画面のアクションがどういう作りになっているかを確認しましょう。記事一覧の上部には、チェックを入れた記事に対して一括操作を行うアクションバーがあります。ここには「公開」ボタンと「削除」ボタンが並び、さらに「アクション...」のドロップダウンの中に「記事の公開を取り消し」(=非公開化)などが入っています。
MT はこれらの操作を data-action-id という属性で識別しています。実機で確認したところ、次のように対応していました。
data-action-id="publish"… 公開data-action-id="delete"… 削除data-action-id="set_draft"… 公開を取り消し(非公開化)
つまり、publish と delete を隠し、set_draft はそのまま残せば、ご要望どおりの「公開・削除はさせないが、非公開化はできる」という状態になります。この data-action-id は、パソコン表示のボタンにもスマートフォン表示のドロップダウン項目にも同じ値が付いているので、まとめて対象にできます。
再描画に強い CSS で隠す
ここで一つ気をつけたい点があります。一覧画面は、ページ送りや絞り込みのたびに一覧部分が描き直されます。読み込み直後に一度だけボタンを消しても、描き直されたときに元に戻ってしまうのです。
そこで、要素を直接いじるのではなく、<style> を差し込んで CSS で隠す方法を取ります。CSS のルールは、あとから作り直された要素にも自動で適用されるため、描き直しのたびに処理を走らせる必要がありません。監視の仕組みを組まずに済み、シンプルで確実です。
const style = document.createElement('style');
style.textContent = `
[data-is^="list-actions"] [data-action-id="publish"],
[data-is^="list-actions"] [data-action-id="delete"] {
display: none !important;
}
`;
document.head.appendChild(style);
[data-is^="list-actions"] で一覧のアクション領域に絞り込んだうえで、publish と delete だけを隠しています。set_draft(非公開化)には触れていないので、下書きに戻す操作はこれまでどおり行えます。この方法は、記事の一覧だけでなくコンテンツデータの一覧でも同じように使えます。
完成したコード
ここまでの内容をまとめたものが、次のコードです。user.js に貼り付けて使えます。編集画面での公開制限と、一覧画面での公開・削除の非表示を、1 つのコードでまかなえます。restrictedAuthorIds に、制限したいユーザーの author_id を並べてください。
(function ($) {
// 公開・削除に承認が必要なユーザーの author_id 一覧
// ログイン中のユーザーで mtapp.debug() を実行するとコンソールに author_id が表示されます
const restrictedAuthorIds = [12, 34];
// 対象ユーザー以外は何もしない
if (!restrictedAuthorIds.includes(mtappVars.author_id)) {
return;
}
const screenId = mtappVars.screen_id;
// ============================================================
// 1. 編集画面: ステータスの「公開」を選べないようにする + 注釈を追加
// ============================================================
if (['edit-entry', 'edit-content-type-data'].includes(screenId)) {
const statusSelect = document.querySelector('select[name="status"]');
const publishingWidget = document.querySelector('#entry-publishing-widget');
if (statusSelect && publishingWidget) {
// MT のステータスは値が固定。公開 = 2(未公開 = 1、日時指定 = 4)。
// ラベル文字列(「公開」等、環境で「未公開(原稿)」のように変わりうる)よりも
// value で判定するほうが確実。option.value / select.value は文字列で返るため '2' と比較する。
const PUBLISH_STATUS = '2';
// ページ読み込み時点で既に公開状態か(公開済み記事かどうか)を記録
const initiallyPublished = statusSelect.value === PUBLISH_STATUS;
// 「公開」の選択肢を非活性化
// すでに公開が選択されている記事(公開済み記事)は disabled にしない。
// disabled な option は selected であっても送信データから除外されるため、
// ここで disabled にすると公開済み記事の更新時に status が送信されずエラーになる。
if (!initiallyPublished) {
Array.from(statusSelect.options).forEach((option) => {
if (option.value === PUBLISH_STATUS) {
option.disabled = true;
}
});
}
// 公開ウィジェットの下に注釈を追加
// #entry-publishing-widget に直接追加すると左右の余白が付かないため、
// 保存ボタンなどと同じ内側のコンテナ(左右のパディングを持つ要素)に追加する
const note = document.createElement('p');
note.className = 'text-muted small mt-2 mb-0';
note.textContent = 'このアカウントでは公開できません。公開には承認者による操作が必要です。保存・更新は通常どおり行えます。';
const primaryButton = publishingWidget.querySelector('.btn-primary[name="status"]');
const noteContainer = primaryButton?.parentElement ?? publishingWidget;
noteContainer.appendChild(note);
// 保険:何らかの理由で公開が選択された状態のまま送信された場合にブロック
const form = statusSelect.closest('form');
if (form) {
form.addEventListener('submit', (e) => {
if (statusSelect.value === PUBLISH_STATUS && !initiallyPublished) {
e.preventDefault();
alert('このアカウントでは公開できません。下書きとして保存してください。');
}
});
}
}
}
// ============================================================
// 2. 一覧画面: 「公開」「削除」アクションを非表示にする
// (「公開を取り消し」=非公開化は残す)
// ============================================================
if (['list-entry', 'list-content_data'].includes(screenId)) {
// 一覧のアクションは data-action-id で識別できる。
// publish … 公開
// delete … 削除
// set_draft … 公開を取り消し(非公開化) ← これは残す
// publish と delete は PC 用ボタンとモバイル用ドロップダウン項目の
// 両方に同じ data-action-id が付くため、一括で対象にできる。
//
// 一覧はページ送りやフィルタで再描画されるが、CSS ルールなら
// 再描画後に作り直された要素にも適用されるため監視処理は不要。
const style = document.createElement('style');
style.textContent = `
[data-is^="list-actions"] [data-action-id="publish"],
[data-is^="list-actions"] [data-action-id="delete"] {
display: none !important;
}
`;
document.head.appendChild(style);
}
})(jQuery);
対象ユーザーの author_id は、そのユーザーでログインした状態で mtapp.debug() を実行するとコンソールに表示されます。あわせて画面ごとの条件文も確認できるので、カスタマイズの下調べに便利です。
注意点
このカスタマイズは、あくまで管理画面の見た目と操作性を整えるものです。JavaScript による制御なので、ブラウザの開発者ツールを使えば技術的には回避できてしまいます。
ですから、これは「うっかり公開・削除してしまうのを防ぎ、運用ルールをわかりやすく示す」ための仕組み、と捉えていただくのが正確です。「権限のないユーザーには何があっても絶対に公開・削除させない」という厳格なアクセス制御が必要な場面では、まず Movable Type 標準の権限(ロール)設定で公開や削除の権限を外せないかを検討し、それで表現しきれない細かな要件については、Perl 側のコールバック(pre_save.entry など)でサーバーサイドの検証を組み合わせることをおすすめします。用途に応じて、どちらのレベルの対応が必要かを見極めていただければと思います。
まとめ
MT の「公開ボタン」は独立したボタンではなく、ステータス選択の値で動作が変わる保存ボタンでした。この性質と、一覧画面のアクションの作りを踏まえると、今回のカスタマイズは次のように整理できます。
編集画面。
- ボタンではなく、ステータスの公開(値
2)の選択肢のほうを非活性にする。ラベル文字列より値で判定するほうが確実 - 公開済みの記事は選択肢を触らない(
disabledなoptionは送信されないため) - 注釈は保存ボタンと同じコンテナに添えて、余白を他のフィールドと揃える
一覧画面。
- アクションは
data-action-idで見分けられる。publish(公開)とdelete(削除)を隠し、set_draft(非公開化)は残す - 一覧は再描画されるので、要素を直接いじらず CSS で隠す(描き直しにも強い)
そして共通して、厳格な制御が必要なら、サーバーサイドの検証も組み合わせて検討する。
user.js だけで、承認フローに近い運用の土台を用意できます。編集担当のかたと責任者で役割を分けたい場面があれば、ぜひ試してみてください。