WordPressの投稿編集画面にガイドを表示する
というプロトタイプを作ってみた。
WordPressで構築したサイト、月に一回くらいしか更新しないので操作や入力項目の意味(どこに表示が反映されるか)が覚えられないという意見があったため、その対応を模索して。
特にカスタムフィールドなどを使っているとけっこう複雑になるので、その更新頻度だと覚えられないのも致し方ないかと。
ひとまず目指す目的は以下の通り。
- 何を入力したらいいのかを分かりやすくする
- 入力の進捗を見えやすくする
- 必須項目を埋めずに公開するという事故を防ぐ
- ヘルプがすぐに見られるようにし、入力項目の意味やどこに表示されるかを確認しやすく
機能としては次のような感じで実装していった。
- 入力項目を列挙したガイドを画面上部に固定で表示
- タイマーで全ての入力欄を監視し、入力されたものはガイド項目をグリーンに
- 投稿のページでのみ表示
- 投稿タイプごとに項目をカスタマイズ
- 必須項目とそうでない項目がわかるといい
- 必須項目を埋めないと公開ボタンを押せないようにする
- ヘルプボタンを押すと入力項目に関するヘルプを表示 モーダルウィンドウで表示する
- ヘルプの内容をテンプレートから読み込んで表示
割とうまくできたのでは(自画自賛)
ガイドの表示と項目の色の変化
画面の上部にガイドを常に固定で表示し、入力の進捗が色で分かるようにした。
タイマーで入力欄を1秒ごとに監視して、入力があればグリーンに、空白になったらグレーに色を変わるようにしてある。
なお本文のビジュアルモードのiframeとテキストモードのテキストエリアの同期は10秒ごとに行われているため両方を監視しているが、そのせいで空白になったときグレーに戻るのに最大10秒かかる。
ユーザー層を考えるとビジュアルモードだけしか監視しないようにしてもいいかもしれない。
またガイドに目が行くようにするかを考えた結果、最初の表示の際と、グリーンとグレーの入れ替わりの際にCSSアニメーションを入れて目立たせることにしてある。
ヘルプの表示
ガイドの各項目の横にヘルプボタンを表示し、押せばHTMLのヘルプがモーダルで表示されるように。
一応Wikiにヘルプがあるが、そもそもWikiに辿り着けない人が多いためこういう措置とした。
月に一回した触らないのでは是非も無い話だ。
テーマのpost_guideというディレクトリにpost_title.htmlのような項目名と対応するHTMLを置いて、それをAJAXで読むようにしてある。
必須項目への対応
WordPressは素のままだと必須項目が設定できない(多分プラグインとかはある)ので、それをJavaScriptのレベルで設定する機能もついでに入れてみた。
必須項目で未入力のものがあれば公開ボタンをdisabledにし、全て入力された時点でそれを解除する。
未入力の場合はガイドの項目名の上に「required」と表示される。
きっと字が小さすぎて読めないが、赤ければなんとなく重要なもので、それが必須項目だと教えれば覚えてくれるだろう。多分。
投稿タイプごとに項目をカスタマイズ
投稿タイプごとに入力項目は異なるのでそれに対応。例えば固定ページでは以下のようなガイドが表示される。
カスタムフィールドが多い投稿タイプの場合はこれがもっとずらっと並ぶ形になる。
コードと軽い解説
functions.php
テーマのfunctions.php。
管理画面でのjsとcssの読み込みと、必要なAJAXのアクションを定義してある。
AJAXは投稿タイプの判別とヘルプのHTMLを読み込むというもので、大したことはしていない。
<?php | |
// 管理画面にCSSとJSの読み込みを追加 | |
function enqueue_post_guide_files() { | |
wp_enqueue_style('post-guide-style' , get_stylesheet_directory_uri() . '/css/post_guide.css'); | |
wp_enqueue_script('post-guide-js' , get_stylesheet_directory_uri() . '/js/post_guide.js'); | |
} | |
add_action('admin_enqueue_scripts', 'enqueue_post_guide_files'); | |
// 指定されたpost_idの投稿の投稿タイプを返す | |
function ajax_post_type_by_post_id() { | |
if ($_POST['post_type'] != '') { | |
// post_typeがわたってきた場合はそのまま返す | |
echo $_POST['post_type']; | |
} else { | |
// post_idがわたってきた場合は投稿タイプを判別して返す | |
echo get_post_type($_POST['post_id']); | |
} | |
die(); | |
} | |
add_action('wp_ajax_ajax_post_type_by_post_id', 'ajax_post_type_by_post_id'); | |
function ajax_post_guide_html() { | |
$guide_content_path = get_stylesheet_directory() . '/post_guide/' . $_POST['item_key'] . '.html'; | |
if (file_exists($guide_content_path)) { | |
$html = file_get_contents($guide_content_path); | |
} else { | |
$html = $guide_content_path; | |
} | |
echo $html; | |
die(); | |
} | |
add_action('wp_ajax_ajax_post_guide_html', 'ajax_post_guide_html'); |
js/post_guide.js
編集画面でガイドを構築して表示し、タイマーで入力を監視してガイドや公開ボタンの状態を変化を制御する。
入力監視をchangeイベントでなくタイマーにしたのは、本文とかタグリストなどにフォームのchangeイベントでは対応できないため。
投稿タイプごとの項目の定義はgetGuideItemsに追加していく。
投稿タイプの判別は新規追加の時にはpost_typeが引数でわたってくるが、既存記事の編集の際にはpost_idしかわたってこないので、AJAXでWordPressに問い合わせると言うことをしている。
非同期処理が入ってしまったのでだーっと書いた処理を後で関数に分割していくというのがつらい。sync/awaitとか使いこなせば簡単になるんじゃろうか。
なおプロトタイプと言うことで汎用化はあまりがんばっていない。
多様なカスタムフィールドに対応するにはwatchProgressのあたりとかに処理を追加していく必要がある。
jQuery(document).ready(function () { | |
// 投稿の新規追加もしくは編集画面以外では表示しない | |
if (location.pathname != '/wp-admin/post-new.php' && location.pathname != '/wp-admin/post.php') { | |
return; | |
} | |
// 投稿ガイドの表示 | |
initPostGuide(); | |
}); | |
// ガイドの初期化と構築 | |
function initPostGuide() { | |
// 投稿タイプの取得 | |
let post_type; | |
let post_id; | |
let query_string = location.search.substring(1); | |
if (query_string) { | |
params = query_string.split('&'); | |
params.forEach(element => { | |
let key_value = element.split('='); | |
// 新規追加の場合はURLからpost_typeを取得 | |
if (key_value[0] == 'post_type') { | |
post_type = key_value[1]; | |
console.log(post_type); | |
} | |
// 編集の場合はAJAXで問い合わせるためにpost_idを記録 | |
if (key_value[0] == 'post') { | |
post_id = key_value[1]; | |
console.log(post_id); | |
} | |
}); | |
} | |
// postの新規追加の場合は引数にpost_typeもpost_idもない | |
if (!post_type && !post_id) { | |
post_type = 'post'; | |
} | |
// AJAXでpost_typeを問い合わせ 引数から分かる場合もコードを共通化するために問い合わせる 非同期になるので注意 | |
$.ajax({ | |
type: 'POST', | |
url: ajaxurl, | |
data: { | |
'action': 'ajax_post_type_by_post_id', | |
'post_id': post_id, | |
'post_type': post_type | |
}, | |
success: function (wp_post_type) { | |
post_type = wp_post_type | |
buildPostGuide(post_type); | |
// 毎秒チェックを開始 | |
var timer = setInterval(function () { | |
console.log(post_type); | |
if (post_type == undefined) { | |
return; | |
} | |
let guide_items = getGuideItems(post_type); | |
let publish_disabled = false; | |
for (let key in guide_items) { | |
watchProgress(key, guide_items[key]); | |
if (guide_items[key]['required'] && !($('#' + key + '-guide').hasClass('done'))) { | |
publish_disabled = true; | |
} | |
} | |
// doneになってないrequiredの項目がひとつでもあれば公開ボタンをdisabledに そうでなければdisabledは解除される | |
$('#publish').prop('disabled', publish_disabled); | |
}, 1000); | |
} | |
}); | |
} | |
// 入力状態の監視と、状態に応じたガイドの表示の変更 | |
function watchProgress(key, items) { | |
// テキストインプットの場合 | |
if (items['tag'] == 'input' && items['type'] && items['type'] == 'text') { | |
if ($('input[name="' + key + '"]').val() != '') { | |
gainProgress($('#' + key + '-guide')); | |
} else { | |
lostProgress($('#' + key + '-guide')); | |
} | |
} | |
// 本文の場合 | |
if (key == 'content') { | |
// ビジュアルモードのiframeからテキストモードのテキストエリアに同期されるのが10秒おきっぽい そのため本文反映が少し遅れる | |
if ($('textarea[name="content"]').val() != '' || $('#content_ifr').contents().find('body').text() != '') { | |
gainProgress($('#content-guide')); | |
} else { | |
lostProgress($('#content-guide')); | |
} | |
} | |
// タグの場合 | |
if ($('ul.tagchecklist li').length > 0) { | |
gainProgress($('#tag-guide')); | |
} else { | |
lostProgress($('#tag-guide')); | |
} | |
// カテゴリーの場合 | |
if ($('ul.categorychecklist :checked').length > 0) { | |
gainProgress($('#category-guide')); | |
} else { | |
lostProgress($('#category-guide')); | |
} | |
} | |
// ガイドの構築 | |
function buildPostGuide(post_type) { | |
console.log('build'); | |
$('#screen-meta-links').before('<div><ul id="post-guide-panel" class="post-guide-panel"></ul></div>'); | |
$('#post-guide-panel').append('<li id="guide-header">投稿ガイド</li>'); | |
// ガイドバーを表示する分、スペースを空ける | |
$('h1.wp-heading-inline').css('padding-top', '60px'); | |
console.log(post_type); | |
let guide_items = getGuideItems(post_type); | |
for (let key in guide_items) { | |
// ガイドバーの要素を追加 | |
let guide_name = key + '-guide' | |
let label; | |
if (guide_items[key]['required']) { | |
// 必須項目の場合はルビで表示 | |
label = '<ruby>' + guide_items[key]['label'] + '<rt>required</rt></ruby>'; | |
} else { | |
label = guide_items[key]['label']; | |
} | |
$('#post-guide-panel').append('<li id="' + guide_name + '" class="empty"><span class="item">' + label + '</span> <span id="' + key + '-help-button" class="help-button" > ? </span></li>'); | |
// ヘルプボタン クリックでモーダル表示 | |
$('#' + key + '-help-button').on('click', function(){ | |
openGuideHelpModal(key); | |
}); | |
} | |
} | |
// ヘルプをAJAXで読み込みモーダルで表示 | |
function openGuideHelpModal(item_key) { | |
$.ajax({ | |
type: 'POST', | |
url: ajaxurl, | |
data: { | |
'action': 'ajax_guide_html', | |
'item_key': item_key | |
}, | |
success: function(html) { | |
$('body').append('<div id="modal-background" class="modal-background"></div>'); | |
$('body').append('<div id="modal" class="modal"><div id="modal-container" class="modal-container">' + html + '</div></div>'); | |
$('#modal').css('left', (window.innerWidth - 800) / 2); | |
$('#modal-container').append('<div id="modal-close" class="modal-close">×</div>'); | |
$('#modal-background').on('click', function(){ | |
$('#modal').remove(); $('#modal-background').remove(); | |
}); | |
$('#modal-close').on('click', function(){ | |
$('#modal').remove(); $('#modal-background').remove(); | |
}); | |
} | |
}); | |
} | |
// 投稿タイプごとに入力ガイドに表示するアイテムを取得 | |
function getGuideItems(post_type) { | |
items = { | |
'post': { | |
'post_title': { label: 'タイトル', tag: 'input', type: 'text', required: true }, | |
'content': { label: '本文', tag: 'textarea', required: true }, | |
'tag': { label: 'タグ', tag: 'input' }, | |
'category': { label: 'カテゴリー', tag: 'input', required: true }, | |
}, | |
'page': { | |
'post_title': { label: 'タイトル', tag: 'input', type: 'text' }, | |
'content': { label: '本文', tag: 'textarea' }, | |
'menu_order': { label: '順序', tag: 'input', type: 'text' }, | |
}, | |
} | |
return items[post_type]; | |
} | |
// 入力があったとき | |
function gainProgress(selector) { | |
selector.removeClass('empty'); | |
selector.css('animation', 'bgcolor_to_gain 1.2s'); | |
selector.addClass('done'); | |
} | |
// 入力が空白の場合 | |
function lostProgress(selector) { | |
selector.addClass('empty'); | |
selector.removeClass('done'); | |
selector.css('animation', 'bgcolor_to_lost 1.2s'); | |
} |
css/post_guide.css
ガイドのスタイルとヘルプが表示されるモーダルのスタイルなど。
CSS力低めなのでまあこんなもんで。
ul.post-guide-panel { | |
position: fixed; | |
table-layout: fixed; | |
top: 30; | |
width: 100%; | |
z-index: 88888; | |
} | |
ul.post-guide-panel li { | |
position: relative; | |
display: inline-block; | |
vertical-align: middle; | |
background-color: #666; | |
color: #fff; | |
text-align: center; | |
padding: 10px 20px 10px 30px; | |
text-decoration: none; | |
} | |
ul.post-guide-panel li span.item { | |
position: relative; | |
} | |
span.help-button { | |
background-color: #999; | |
color: #eee; | |
border-radius: 100%; | |
border: #ddd 1px solid; | |
padding: 0 4px; | |
margin-top: -5px; | |
top: 50%; | |
font-size: smaller; | |
margin-left: 0px; | |
} | |
li rt { | |
color: red; | |
} | |
li.done rt { | |
color: green; | |
} | |
ul.post-guide-panel li:first-child { | |
background-color: #eee; | |
color: #666; | |
padding-left: 20px; | |
font-weight: bold; | |
border: 1px solid #666; | |
margin-right: 10px; | |
} | |
ul.post-guide-panel li.done { | |
background-color: green; | |
} | |
@keyframes bgcolor_to_gain { | |
0% { | |
background-color: #efe; | |
} | |
100% { | |
background-color: green; | |
} | |
} | |
@keyframes bgcolor_to_lost { | |
0% { | |
background-color: #ccc; | |
} | |
100% { | |
background-color: #666; | |
} | |
} | |
.modal { | |
position: fixed; | |
width: 800px; | |
height: 600px; | |
top: 50px; | |
margin: auto; | |
background-color: #fff; | |
z-index: 99999; | |
border: 2px solid gray; | |
overflow-y: scroll; | |
} | |
.modal-container { | |
position: relative; | |
margin: auto; | |
width: 85%; | |
height: 90%; | |
color: #000; | |
padding: 30px; | |
} | |
.modal-background { | |
position: fixed; | |
width: 100%; | |
height: 150%; | |
top: -50px; | |
left: 0px; | |
background-color: #fff; | |
z-index: 99990; | |
opacity: 0.5; | |
} | |
.modal-list { | |
overflow:auto; | |
width: 600px; | |
height: 500px; | |
} | |
.modal-new { | |
text-align: right; | |
position: absolute; | |
bottom: 130px; | |
right: 90px; | |
} | |
.modal-close { | |
text-align: right; | |
position: absolute; | |
padding: 3px 10px 5px 10px; | |
top: 0px; | |
right: -10px; | |
background-color: gray; | |
color: white; | |
font-size: 16px; | |
font-weight: bold; | |
} |
post_guide/post_title.html
モーダル内に表示するヘルプ。本文のヘルプはcontent.htmlになる。
読む側の身になって適当に分かりやすく書いていく。ヘルプが長い場合はモーダル内にスクロールバーが出る。
<h3>タイトル <span style="color: red; font-size: 9pt;">必須項目</span></h3> | |
<p>記事のタイトル。</p> | |
<p>全角28文字以内になることを想定にサイトのデザインは作成されている。</p> | |
<h4>表示される場所</h4> | |
<h5>トップページ</h5> | |
画像入れたりとか | |
<h5>記事の一覧</h5> | |
ごにょごにょ | |
<h5>カテゴリーの一覧</h5> | |
カテゴリー内に属する最新の記事が3件まで表示される。 | |
<h5>記事の個別ページ</h5> | |
タイトルとして堂々と表示される。 | |
すく<br> | |
ろーる<br> | |
ばー<br> | |
を<br> | |
だし<br> | |
たい<br> | |
すく<br> | |
ろーる<br> | |
ばー<br> | |
を<br> | |
だし<br> | |
たい<br> | |
<br> | |
<br> |