コンテンツタイプのフィールドタイプを増やしてみよう
HAPPY HOLIDAY !!
こんにちわ。シックス・アパートの高山です。
この記事は、Movable Type Advent Calendar 2018 の 25日目の記事です。
Movable Type 7 といえば、コンテンツタイプです。これまでの記事やウェブページにカスタムフィールドを加えるような感覚でありながら、まったく新しいコンテンツ構造をいちから設計できる機能になります。
端的に説明すると、以下のような特徴を持っています。
- コンテンツタイプには、自由にコンテンツの名称を付けられます。これにより。「記事」や「ウェブページ」という用語を読み替える、或いは管理画面をカスタマイズしてラベルを変更する。といったことが必要ありません。
- 「本文」や「タイトル」といった、システムから提供する(押し付ける)フィールドは一つもありません。(データ識別ラベルをユーザー入力とした場合を除く)すべてのフィールドを自由に設計できます。
- カスタムフィールドで利用できるフィールドの種類以上に利用できるフィールドの種類が存在します。
- コンテンツタイプ同士をリンクすることができるので、関連するコンテンツや使い回しができる情報を別のコンテンツタイプとして作成・管理することができます。これにより、様々な場所で利用されている共通の情報を更新するとき、利用しているコンテンツを検索、編集することなく、一つのコンテンツを編集するだけで済ませることができます。
さて、Movable Type といえばプラグインによる機能拡張(自由度高め)が特徴的ですが、コンテンツタイプのフィールドの種類(以後フィールドタイプと呼びます)を増やすことももちろんプラグインでできます。
この記事では、フィールドタイプを追加する方法について説明します。
(ようやく本題にはいります。)新しいフィールドタイプを登録する
フィールドタイプを追加するには、「content_field_types」というレジストリにフィールドの定義を追加する必要があります。
今回は、タイトルと説明を組み合わせて入力する事ができるフィールド(説明リストや定義リストと言われるもの DL タグ)を追加しますので、「content_field_types」の下に「description_list」というキーで登録します。このキー名がコンテンツデータのの中でも利用されますので、カブりは禁止です。
なお、今回説明に利用したプラグインは GitHub で公開しています。例によってテクニカルサポート非対応にはなりますが、ご利用ください。(将来的にはコアに取り込むかも)
すべての定義を記入したものがこちらです。
id: DescriptionListField
name: DescriptionListField
version: 1.0
author_name: Six Apart Ltd.
author_link: https://movabletype.org
description: <__trans phrase="This plugin adds a description list content field type.">
l10n_lexicon:
ja:
This plugin adds a description list content field type.: "説明リストコンテンツフィールドを追加"
Description List: 説明リスト
Title: 用語
Value: 説明
Add new [_1]: 新しい[_1]を追加
Title Label: タイトルのラベル
Value Label: 説明のラベル
content_field_types:
description_list:
label: Description List
data_type: 'text'
order: 210,
icon_class: 'ic_list'
can_data_label_field: 0
data_load_handler: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::data_load_handler
field_html: 'field_html/field_html_description_list.tmpl'
field_html_params: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::field_html_params
field_value_handler: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::field_value_handler
replaceable: 1
tag_handler: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::tag_handler
feed_value_handler: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::feed_value_handler
preview_handler: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::preview_handler
replace_handler: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::replace_handler
search_handler: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::search_handler
list_props:
description_llist:
base: '__virtual.string'
col: 'value_varchar'
display: 'none'
filter_editable: 0
html: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::list_prop_html
terms: $DescriptionListField::DescriptionListField::ContentFieldType::DescriptionListField::list_prop_terms
options_html: 'content_field_type_options/description_list.tmpl'
options:
- label
- description
- required
- display
- title_label
- value_label
レジストリの登録内容
それぞの内容について解説します。
- label
- コンテンツタイプの編集画面で表示されるフィールドタイプ名です。自動翻訳の対象となります。
- data_type
- データベースにデータを格納する際のデータ型を指定します。今回は、説明文が長くなったりすることを考慮して「text」とします。それ以外にも、「varchar」や「integer」などが利用できます。
- order
- コンテンツタイプ編集画面のフィールド一覧におけるフィールドの表示順です。既存のフィールドタイプは、10 から始まり 200 まで 10 刻みで定義されています。既存のフィールドタイプの間や先頭に表示したいという場合は、この値で調整します。
- icon_class コンテンツタイプ編集画面のフィールド一覧におけるフィールドのアイコンを指定します。利用できるアイコンの種類は MT7 Style Guide に掲載されています。
- can_data_label_field
- 1 を指定すると、コンテンツタイプの編集画面でデータ識別ラベルとして利用するフィールドの選択に登場します。保存されている値がそのまま利用されるので、今回のように特殊な値の場合は 0 を指定します。
- data_load_handler
- コンテンツデータ入力画面から、フィールドの値を取得するハンドラーを登録します。
- field_html
- コンテンツデータ編集画面で表示されるフィールドのテンプレートを指定します。
- field_html_params
- field_html で利用するテンプレート変数を仕込むためのハンドラーを指定します。
- field_value_handler
- MTContentFieldValue タグの結果を返すハンドラーを指定します。指定がない場合は、入力された値をそのまま利用します。
- replaceable
- 管理画面の検索と置換により、置換ができる場合は 1 を指定します。
- tag_handler
- MTContentField タグのハンドラーを指定します。複数の値を保つ場合、ループの処理を行ったり、他のオブジェクトへのリンクなどでMTContentField タグのコンテクストを変更する場合に指定します。今回は、複数の説明が存在する可能性があるので指定します。
- feed_value_handler
- コンテンツデータの一覧から取得できるコンテンツデータフィードに表示する内容を出力するためのハンドラーを指定します。指定がない場合は、入力された値をそのまま利用します。
- preview_handler
- 個別アーカイブが存在しないときにデータのプレビューをおこなうと、フィールドのラベルと入力データを簡易的に表示しています。このときの内容を出力するためのハンドラーを指定します。指定がない場合は、入力された値をそのまま利用します。
- replace_handler
- 「replaceable」が 1 のとき、実際の価の置換処理をおこなうハンドラーを登録します。
- search_handler
- 検索と置換、コンテンツデータ検索などの際に実際の検索処理をおこなうハンドラーを登録します。
- list_props
- コンテンツデータ一覧におけるフィールドの出力に関して設定します。詳しくはリスティングフレームワークの解説を御覧ください。今回は、値が特殊であるため一覧には表しません
- options_html
- コンテンツタイプ編集画面において、コンテンツフィールドの設定内容を表示するためのテンプレートを指定します。
- options
- コンテンツフィールドのオプション情報として利用可能なものを列挙します。label, description, required, display については、必ず定義する必要があります。今回は、新しいオプションとして、データ編集画面で表示する際のラベルを指定するようにしました。
- ss_validator
- コンテンツデータ編集画面において、サーバーサイドの入力検証処理をおこなうハンドラーを登録します。必須チェックは共通で処理されるため記述する必要はありません。
- site_data_import_handler
- データインポートにおいて、特殊な処理をおこなう必要がある場合にハンドラーを指定します。
- site_import_handler
- データインポートにおいて、特殊な処理をおこなう必要がある場合にハンドラーを指定します。
- theme_import_handler
- テーマの適用時に特殊な処理を行う必要がある場合にハンドラーを指定します。
- theme_export_handler
- テーマのエクスポート時に特殊な処理を行う必要がある場合にハンドラーを指定します。
- options_validation_handler
- コンテンツタイプ編集画面において、入力されたオプションの値を検証するためのハンドラーを指定します。
- options_pre_save_handler
- コンテンツタイプ編集画面において、入力されたオプションの値を保存する直前に呼び出されるハンドラーを指定します。
- search_class
- 他のオブジェクトへのリンクを保持するタイプのフィールドのとき、検索を実行するときに実際に検索する対象となるオブジェクトのクラス名を指定します。
- search_columns
- 他のオブジェクトへのリンクを保持するタイプのフィールドのとき、検索を実行するときに実際に検索する対象となるオブジェクトのフィールド名を列挙します。
上記以外にも、いくつかの項目が存在しますが、今回は利用しません。詳しくは以下をご覧ください。
各種ハンドラーについて
登録するハンドラーについて、実際のコードを交えながら説明します。
data_load_handler
引数
- $app
- 現在のアプリケーションオブジェクト
- $field_data
- コンテンツフィールドの情報
戻り値
取得したユーザーからの入力値を返す。サンプルコード
sub data_load_handler {
my ( $app, $field_data ) = @_;
my $id = $field_data->{id};
my @titles = $app->multi_param('description-list-field-' . $field_data->{id} . '-title');
my @values = $app->multi_param('description-list-field-' . $field_data->{id} . '-value');
my @val;
for (my $i = 0; $i < scalar @titles; $i++ ) {
push @val, {
title => $titles[$i],
value => $values[$i]
};
}
return \@val;
}
field_html_params
引数
- $app
- 現在のアプリケーションオブジェクト
- $field_data
- コンテンツフィールドの情報
戻り値
変数名 => 値となるハッシュ値を返す。サンプルコード
sub field_html_params {
my ( $app, $field_data ) = @_;
my $data = $field_data->{value};
if ( !defined $data ) {
push @$data, {
title => '',
value => ''
}
}
my $required
= $field_data->{options}{required} ? 'data-mt-required="1"' : '';
{
description_list_data => $data,
required => $required,
field_label => $field_data->{options}{label},
};
}
field_value_handler
引数
- $ctx
- テンプレートのビルドに利用されるコンテクストオブジェクト
- $args
- MTContentFieldValue タグで指定されたモディファイア
- $cond
- 未使用
- $field_data
- コンテンツフィールドの情報
- $value
- コンテンツデータの値
戻り値
タグの処理結果 MTContentFieldValue タグには定義されていないモディファイアを利用することもできる。サンプルコード
sub field_value_handler {
my ( $ctx, $args, $cond, $field_data, $value ) = @_;
my $part = lc $args->{part} || 'value';
return if ($part ne 'title' and $part ne 'value' );
return $value->{$part}
}
tag_handler
引数
- $ctx
- テンプレートのビルドに利用されるコンテクストオブジェクト
- $args
- MTContentFieldValue タグで指定されたモディファイア
- $cond
- 未使用
- $field_data
- コンテンツフィールドの情報
- $value
- コンテンツデータの値
戻り値
内部ブロックの処理結果サンプルコード
sub tag_handler {
my ( $ctx, $args, $cond, $field_data, $value ) = @_;
my $tok = $ctx->stash('tokens');
my $builder = $ctx->stash('builder');
my $vars = $ctx->{__stash}{vars} ||= {};
my $out = '';
my $i = 1;
my $glue = $args->{glue};
for my $v ( @{$value} ) {
local $vars->{__first__} = $i == 1;
local $vars->{__last__} = $i == scalar @{$value};
local $vars->{__odd__} = ( $i % 2 ) == 1;
local $vars->{__even__} = ( $i % 2 ) == 0;
local $vars->{__counter__} = $i;
local $vars->{__value__} = $v;
defined(
my $res = $builder->build(
$ctx, $tok,
{ %{$cond},
ContentFieldHeader => $i == 1,
ContentFieldFooter => $i == scalar @$value,
}
)
) or return $ctx->error( $builder->errstr );
if ( $res ne '' ) {
$out .= $glue
if defined $glue && $i > 1 && length($out) && length($res);
$out .= $res;
$i++;
}
}
$out;
}
feed_value_handler
引数
- $app
- 現在のアプリケーションオブジェクト
- $field_data
- コンテンツフィールドの情報
- $value
- コンテンツデータの値
戻り値
フィード内の表示内容サンプルコード
sub feed_value_handler {
my ( $app, $field_data, $values ) = @_;
return '' unless @$values;
my $contents = '';
for my $v (@$values) {
my $encoded_v = MT::Util::encode_html($v->{value});
my $encoded_t = MT::Util::encode_html( $v->{title} );
$contents .= "<dt>$encoded_t</dt><dd>$encoded_v</dd>";
}
return "<dl>$contents</dl>";
}
preview_handler
引数
- $app
- 現在のアプリケーションオブジェクト
- $field_data
- コンテンツフィールドの情報
- $value
- コンテンツデータの値
戻り値
プレビューの表示内容サンプルコード
sub preview_handler {
my ( $app, $field_data, $values ) = @_;
unless ( ref $values eq 'ARRAY' ) {
$values = [$values];
}
return '' unless @$values;
my $contents = '';
for my $v (@$values) {
my $encoded_v = MT::Util::encode_html($v->{value});
my $encoded_t = MT::Util::encode_html( $v->{title} );
$contents .= "<dt>$encoded_t</dt><dd>$encoded_v</dd>";
}
return "<dl>$contents</dl>";
}
replace_handler
引数
- $search_regex
- 検索内容の regex オブジェクト
- $replace_string
- 置換内容
- $field_data
- コンテンツフィールドの情報
- $value
- コンテンツデータの値
- $content_data
- コンテンツデータオブジェクト
戻り値
置換対象が存在する場合 1 を返す。サンプルコード
sub replace_handler {
my ($search_regex, $replace_string, $field_data, $values, $content_data ) = @_;
return 0 unless defined $values;
$values = [$values] unless ref $values eq 'ARRAY';
my $replaced = 0;
for (@$values) {
$replaced += $_->{title} =~ s!$search_regex!$replace_string!g;
$replaced += $_->{value} =~ s!$search_regex!$replace_string!g;
}
$replaced > 0;
}
search_handler
引数
- $search_regex
- 検索内容の regex オブジェクト
- $field_data
- コンテンツフィールドの情報
- $value
- コンテンツデータの値
- $content_data
- コンテンツデータオブジェクト
戻り値
検索に該当するデータが存在する場合 1 を返すサンプルコード
sub search_handler {
my ( $search_regex, $field_data, $values, $content_data ) = @_;
return 0 unless defined $values;
$values = [$values] unless ref $values eq 'ARRAY';
(
grep {
defined $_
? ( $_->{title} =~ /$search_regex/ ) || $_->{value} =~ /$search_regex/
: 0
} @$values
) ? 1 : 0;
}
必要なテンプレートについて
field_html
field_html で指定するテンプレートには、コンテンツデータ編集画面における個々のコンテンツフィールドの html を生成する役割があります。これは、新規作成時にデータを受け付ける方法や、既存データの表示を行なうことを意味します。
今回の例では、説明リストのタイトルと内容を入力するためのふたつのフィールドを用意します。また、複数の内容を登録できるようにするため、その組み合わせを任意に増やしたり減らす仕組みも用意します。また、順番を変更できるように並び替えについても実装しています。
サンプルコード
かなり長くなりますが、今回のテンプレートは以下のようになっています。<div id="description-list-field-<mt:var name="content_field_id" escape="html">" class="description-list-field-container" <mt:var name="required">>
<mt:loop name="description_list_data">
<div class="mt-draggable" draggable="true" aria-grabbed="false">
<div class="col-auto">
<mtapp:svgicon id="ic_move" title="Draggable">
</div>
<div class="col text-wrap">
<div class="mt-flexBreak"></div>
<div class="mt-draggable__content description-list-data">
<label><mt:var name="title_label" escape="html"></label>
<input type="text" class="form-control" name="description-list-field-<mt:var name="content_field_id" escape="html">-title" value="<mt:var name="title" escape="html">" mt:watch-change="1">
<label><mt:var name="value_label" escape="html"></label>
<textarea name="description-list-field-<mt:var name="content_field_id" escape="html">-value" class="form-control" rows="3" mt:watch-change="1"><mt:var name="value" escape="html"></textarea>
</div>
</div>
<div class="col-auto mr-3 d-none d-md-block">
<a href="javascript:void(0)" class="remove">
<__trans phrase="Remove">
</a>
</div>
</div>
</mt:loop>
<div class="mt-3">
<a href="javascript:void(0)" class="add-description-list-button d-block d-md-inline-block" data-mt-content-field-id="<mt:var name="content_field_id">"><mtapp:svgicon id="ic_add" title="<__trans phrase="Add new pair">" size="sm"><__trans phrase="Add new [_1]" params="<mt:var name="field_label" escape="html" escape="html">"></a>
</a>
</div>
</div>
<mt:unless name="description-list-boilerplate">
<mt:setvar name="description-list-boilerplate" value="1">
<div class="description-list-boilerplate" style="display: none;">
<div class="mt-draggable" draggable="true" aria-grabbed="false">
<div class="col-auto">
<mtapp:svgicon id="ic_move" title="Draggable">
</div>
<div class="col text-wrap">
<div class="mt-flexBreak"></div>
<div class="mt-draggable__content description-list-data">
<label><mt:var name="title_label" escape="html"></label>
<input type="text" class="form-control" value="" mt:watch-change="1">
<label><mt:var name="value_label" escape="html"></label>
<textarea class="form-control" rows="3" mt:watch-change="1"></textarea>
</div>
</div>
<div class="col-auto mr-3 d-none d-md-block">
<a href="javascript:void(0)" class="remove">
<__trans phrase="Remove">
</a>
</div>
</div>
</div>
</mt:unless>
<mt:unless name="setup_description_list_js">
<mt:var name="setup_description_list_js" value="1">
<mt:setvarblock name="jq_js_include" append="1">
function removeDescriptionList() {
jQuery(this).parents('.mt-draggable').remove();
setDirty(true);
app.setDirty();
}
function addDescriptionList() {
var $this = jQuery(this);
var required = $this.data('mt-required') ? true : false;
var $boilerPlate = jQuery('.description-list-boilerplate > div');
var $newDiv = $boilerPlate.clone();
$newDiv.find("input").prop('name', 'description-list-field-<mt:var name="content_field_id" escape="html">-title')
$newDiv.find("textarea").prop('name', 'description-list-field-<mt:var name="content_field_id" escape="html">-value')
$this.parent().before($newDiv.get(0));
setDirty(true);
(app.getIndirectMethod('setDirty'))();
}
window.descriptionListFieldSortableChanged = {};
jQuery('a.add-description-list-button').click(addDescriptionList)
jQuery('.description-list-field-container').on('click', 'a.remove', removeDescriptionList);
jQuery('#description-list-field-<mt:var name="content_field_id" escape="html">').sortable({
items: 'div.mt-draggable[draggable=true][aria-grabbed]',
handle: MT.Util.isMobileView() ? '.col-auto:first' : false,
placeholder: 'placeholder',
distance: 3,
opacity: 0.8,
cursor: 'move',
forcePlaceholderSize: true,
containment: '.mt-mainContent',
start: function (event, ui) {
ui.item.attr('aria-grabbed', true);
if (window.descriptionListFieldSortableChanged[<mt:var name="content_field_id" escape="js">]) {
ui.helper.offset({
top: ui.helper.offset().top + jQuery('body').scrollTop()
});
}
},
sort: function (event, ui) {
if (window.descriptionListFieldSortableChanged[<mt:var name="content_field_id" escape="js">]) {
ui.helper.offset({
top: ui.helper.offset().top + jQuery('body').scrollTop()
});
}
},
change: function (event, ui) {
window.descriptionListFieldSortableChanged[<mt:var name="content_field_id" escape="js">] = true;
},
stop: function (event, ui) {
ui.item.attr('aria-grabbed', false);
}
});
jQuery.mtValidateAddRules({
'div.description-list-field-container': function ($e) {
var required = $e.data('mt-required') ? true : false;
var selectedCount = $e.children('.mt-draggable').length;
if ( required && selectedCount === 0 ) {
this.error = true;
this.errstr = trans('This field is required');
return false;
}
return true;
}
});
</mt:setvarblock>
</mt:unless>
options_html
options_html は、コンテンツフィルドのオプション設定について記述したテンプレートを用意します。type モディファイアにフィールドタイプのキー名を指定します。
サンプルコード
<mt:app:ContentFieldOptionGroup
type="description_list">
<mtapp:ContentFieldOption
id="description-list-title-label"
label="<__trans phrase="Title Label">">
<input ref="title_label" type="text" name="title_label" id="description-list-title-label-fld" class="form-control w-25" value={ options.title_label || 0 }>
</mtapp:ContentFieldOption>
<mtapp:ContentFieldOption
id="description-list-value-label"
label="<__trans phrase="Value Label">">
<input ref="value_label" type="text" name="value_label" id="description-list-value-label-fld" class="form-control w-25" value={ options.value_label || 0 }>
</mtapp:ContentFieldOption>
</mt:app:ContentFieldOptionGroup>
独自に追加するオプションが存在しない場合でもテンプレートは用意する必要があります。
最低限のサンプルコード
<mt:app:ContentFieldOptionGroup
type="description_list">
</mt:app:ContentFieldOptionGroup>
実際に使ってみる
今回作成したコンテンツフィールドを早速使ってみましょう。
コンテンツタイプの編集
コンテンツタイプを新規に作成して、今回作成したフィールドを追加してみましょう。今回のコンテンツタイプは、レシピ集です。(サンプルは伝統的にレシピとなっているとかなっていないとか)
フィールドを追加して、タイトルと説明のラベルを設定します。
コンテンツデータを作成する
では、さっそくレシピを登録しましょう。
<コンテンツデータの編集画面を開くと、先程設定したラベルのペアを入力できるフィールドがあるのを確認できます。アーカイブテンプレートを作成する
折角なので、データを出力する方法にも触れておきましょう。
今回の説明リストの内容を出力するには、以下のようなテンプレートを記述します。
<h3>工程</h3>
<mt:ContentField content_field="工程">
<mt:ContentFieldHeader>
<dl>
</mt:ContentFieldHeader>
<dt><mt:ContentFieldValue part="title" escape="html"></dt>
<dd><mt:ContentFieldValue part="value" escape="html"></dd>
<mt:ContentFieldFooter>
</dl>
</mt:ContentFieldFooter>
</mt:ContentField>
カスタムフィールドとは違い、個々のコンテンツフィールドについて新しいタグを覚える必要はありません。MTContentField タグでフィールドデータのコンテクストを作成し、MTContentFieldValue タグで値を出力する。新しく追加したフィールドタイプですが、他のフィールドと使い方は同じですね。
ポイントとしては、MTContentFieldValue タグに新しいモディファイアを追加していて、タイトルか説明かを指定しているという点です。これは「field_value_handler」のサンプルコードに書いてある内容ですね。
他のフィールドについてもテンプレートを記述して再構築をするとこんなページが出来上がります。
まとめ
コンテンツタイプのフィールドタイプは、標準で 20 種類用意されていますので、大体のことには対応ができると考えていますが、今回のサンプルで用意した説明リストや、Google Map の地図など、あると便利なフィールドタイプはまだまだあると思います。
Movable Type のコアやプラグインなどでも増やしていくことは検討しますが、プラグインがかける皆さまに於かれましては、ぜひ新しいフィールドタイプを作成し、是非プラグインディレクトリで公開してください。救われる人がいるかもしれません。
最後に
今年も無事に Movable Type Advent Calendar は完走です。いつも立ち上げてくれる MT 蝦夷のジャクスタポジション・西山さん、ありがとうございます。感謝しています。
また、参加してくださった全国のコミュニティーメンバーの皆さん。ありがとうございました。
Movable Type Advanced、MT 7 のニューリリースを(切実に)心待ちにしてくださっている皆さん。年内と申し上げていながら年明けのリリースとなり、ご迷惑をおかけしております・・・。その分、しっかりとした品質のものに仕上げってきていると感じています。
現在、Movable Type 及び Movable Type Advanced のベータ版を公開しています。あのバグどうなったかな?あの機能はどうなる?など、いち早くご確認いただけます。ベータ版のフィードバックは年内いっぱい程度で一旦打ち切りとさせていただきますが、もし、あれ直ってないよ!というものがございましたら、フィードバックフォームよりお問い合わせください。
Movable Type 7 では、これからもの Content Hub Platform として利用できる CMS として成長を続けます。来年もまた Movable Type をよろしくお願いします。
では、良いお年を。