Entry_Summaryループ内クラス設定を運営者でも触れるようにする


現在提供中の標準テーマの一覧ページでは、横に並ぶコンテンツの数(2列、3列、4列)は、テンプレートの CSS クラスの設定によって決まる仕様になっています。 この記事では、タイトルの通り、運営側でもこの設定を変更できるようにする方法について考えてみます。


問題点の検証

実際にどのような問題が起こるのかを UTSUWA のテーマ「事業紹介」を例に説明していきます。

納品直後の状態

当初、打ち合わせ時には「事業紹介」については増えないと聞いており、コンテンツとしては 2つなので、col-6 と Class を設定し、横に2枚並ぶようにテンプレートに書かれていました。



事業が追加される事に

これがサイトを運営している途中で、新しい事業が増えて 3つになる事になりました。3つ目のコンテンツは下に表示され隣は空白となります。



3つ並べたいと相談される

同じページ内の「ブログ」コンテンツは横に4枚並べて表示していますし、横に3枚並べた方がいいと思う人も多い事でしょう。



CMS で運用している中で、よくある話ではないかと思います。 詳細ページについては、ある程度レイアウト調整もできるのですが、沢山の情報を管理可能な CMS では一覧ページについて自由度が低くなる傾向があります。

対策を考えてみる

対策としては、その制御を行っているのはテンプレート上に書かれている 1つの Class という事になるので、それを管理画面側から変更できるようにする事になります。

実は準備済みでした

実は Ver. 2.5.1(2015年8月) から、モジュールの設定に ループ内クラス という設定が追加されています。これを活用する事で、管理画面上から変更できるようになります。 テンプレートに直接書かれている Class を {entry:loop.class} という変数に置き換えるだけです。



運用担当者に分かるのか?

次の問題は、この記述内容が運用担当者に理解できるのかというところになります。

acms-col-6 acms-col-sm-4 acms-col-md-3

標準テーマの一覧ページでは、コンテンツの横幅を指定するために acms-col-* というクラスを使用します。 このクラスの数字は 12分割のグリッドシステム に基づいており、1行を12としたときに、各コンテンツが何列分の幅を占めるかを決定します。

具体的な例

  • acms-col-6 → 12列中6列分を使用(2列表示)
  • acms-col-sm-4 → 画面幅が smサイズ以上のとき、12列中4列分を使用(3列表示)
  • acms-col-md-3 → 画面幅が mdサイズ以上のとき、12列中3列分を使用(4列表示)

つまり、画面のサイズによって自動的に 2列 → 3列 → 4列 とレイアウトが変化します。



クラス名 デフォルトのブレイクポイント
acms-col- 全て
acms-col-sm- 480px以上〜
acms-col-md- 768px以上〜
acms-col-lg- 1024px以上〜
acms-col-xl- 1440px以上〜

この設定では、制作者には理解できたとしても、サイトを運用している担当者には難しいと感じるのではないでしょうか。

実際の実装

モジュールフィールドに、各画面の幅の時のカラム数のカスタムフィールドを作る事で実装が可能になります。



さらに工夫を

カスタム設定で設定した SELECTcol-base , col-sm , col-md )を元に、表示設定の「ループ内クラス」{entry:loop.class} を自動で書くことができる JavaScript を用意しました。こうすることでテンプレートは標準的な記述のままで済ますことができ、独自に設定したい時にも対応が可能となります。

モジュールフィールドの設定

<select name="col-base" class="acms-admin-form-width-mini">
  <option value="acms-col-12" {col-base:selected#acms-col-12}>1カラム</option>
  <option value="acms-col-6" {col-base:selected#acms-col-6}>2カラム</option>
  <option value="acms-col-4" {col-base:selected#acms-col-4}>3カラム</option>
  <option value="acms-col-3" {col-base:selected#acms-col-3}>4カラム</option>
</select>
<input type="hidden" name="field[]" value="col-base" />

<select name="col-sm" class="acms-admin-form-width-mini">
  <option value="" {col-sm:selected#}>未設定 (480px以上)</option>
  <option value="acms-col-sm-12" {col-sm:selected#acms-col-sm-12}>1カラム</option>
  <option value="acms-col-sm-6" {col-sm:selected#acms-col-sm-6}>2カラム</option>
  <option value="acms-col-sm-4" {col-sm:selected#acms-col-sm-4}>3カラム</option>
  <option value="acms-col-sm-3" {col-sm:selected#acms-col-sm-3}>4カラム</option>
</select>
<input type="hidden" name="field[]" value="col-sm" />

<select name="col-md" class="acms-admin-form-width-mini">
  <option value="" {col-md:selected#}>未設定 (768px以上)</option>
  <option value="acms-col-md-12" {col-md:selected#acms-col-md-12}>1カラム</option>
  <option value="acms-col-md-6" {col-md:selected#acms-col-md-6}>2カラム</option>
  <option value="acms-col-md-4" {col-md:selected#acms-col-md-4}>3カラム</option>
  <option value="acms-col-md-3" {col-md:selected#acms-col-md-3}>4カラム</option>
</select>
<input type="hidden" name="field[]" value="col-md" />

<span id="col-etc"></span>

今回の JavaScript

最初に INPUT name="entry_summary_loop_class" の値からカスタム設定の SELECT を調整します。その後、SELECT を修正すると INPUT のテキストを書き換え、INPUT 側をダイレクトに修正すると SELECT 側を書き換えるようにしています。

SELECT の OPTION に無い acms-col-2 などが INPUT に書かれた際には、自動的に SELECT に追加し、acms-col- , acms-col-sm- , acms-col-md- でない Class を書いた時には <span id="col-etc"></span> に表示するようにして追加で Class を書いていることも分かるようにしておきました。

<script>
ACMS.addListener("acmsReady", function() {

  const loopClass = document.querySelector('input[name="entry_summary_loop_class"]');

  const colBase = document.querySelector('select[name="col-base"]');
  const colSm = document.querySelector('select[name="col-sm"]');
  const colMd = document.querySelector('select[name="col-md"]');
  const colEtc = document.getElementById("col-etc");

  function updateLoopClass() {
    const values = [colBase.value, colSm.value, colMd.value].filter(value => value !== "");
    loopClass.value = values.join(" ");
  }

  function ensureOptionExists(select, value) {
    if (!value || select.querySelector(`option[value="${value}"]`)) return;
    const existingValues = new Set(Array.from(select.options).map(opt => opt.value));
    if (!existingValues.has(value)) {
      const option = document.createElement("option");
      option.value = value;
      option.textContent = value;
      option.setAttribute("data-custom", "true");
      select.appendChild(option);
    }
  }

  function updateSelectValues() {
    const values = loopClass.value.split(" ").map(val => val.trim()).filter(val => val !== "");
    let baseValue = "", smValue = "", mdValue = "";
    let extraValues = [];

    values.forEach(val => {
      if (/^acms-col-sm-\d+$/.test(val)) {
        smValue = val;
      } else if (/^acms-col-md-\d+$/.test(val)) {
        mdValue = val;
      } else if (/^acms-col-\d+$/.test(val)) {
        baseValue = val;
      } else {
        extraValues.push(val);
      }
    });

    ensureOptionExists(colBase, baseValue);
    ensureOptionExists(colSm, smValue);
    ensureOptionExists(colMd, mdValue);

    colBase.value = baseValue;
    colSm.value = smValue;
    colMd.value = mdValue;

    colEtc.textContent = extraValues.length > 0 ? extraValues.join(", ") : "";
  }

  colBase.addEventListener("change", updateLoopClass);
  colSm.addEventListener("change", updateLoopClass);
  colMd.addEventListener("change", updateLoopClass);

  loopClass.addEventListener("input", updateSelectValues);

  updateSelectValues();
});
</script>

a-blog cms 独自の実装ポイント

通常の管理画面のモジュールID の設定でテストを行っていた際には

document.addEventListener("DOMContentLoaded", function () {
...
});

のように記述して動作していましたが、モジュールIDの管理画面は表の表示画面の「編集」リンクからモーダル表示が可能です。その際には既に DOMコンテンツがロード済みという事になり、ここで書かれた JavaScript を動作させることができません。

そこで a-blog cms の方で用意されている ACMS.addListener("acmsReady" ... を利用します。詳しくは「組み込みJSのイベントハンドラ」をご覧ください。

ACMS.addListener("acmsReady", function() {
...
});

追加のカスタマイズを考える

今回の JavaScript には input[name="entry_summary_loop_class" と Entry_Summary のコンフィグのフィールドが書かれていますが、他のモジュールでも使う事を考慮するような実装も考えられます。

このカスタム設定の SELECT 部分 + JavaScript のファイルを別ファイルにし target_input を変数化します。

@include("/admin/parts/loop-class.html", {"target_input": "entry_summary_loop_class"})   

/admin/parts/loop-class.html の中の JavaScript 部分を

const loopClass = document.querySelector('input[name="{{target_input}}"]');

のように指定する事で、他のモジュールでも同様に扱いたいことがあれば共用して利用することができるようにできます。

最後に

次のバージョン Ver. 3.2 で標準提供するテーマには {entry:loop.class} を活用したテーマにするようにしようと考えています。

また private/config.system.default.yaml

entry_summary_loop_class   : acms-col-6 acms-col-sm-4

のような設定を追加を検討します。どのような初期値がいいかは検討しないといけませんが、こうする事で標準テーマで表示設定にある項目が機能する事になります。

htmxで browser history API を活用する方法と a-blog cms 実装のポイント

htmx は標準で browser history API をサポートしており、属性に hx-push-url="true" を追加することで、これを簡単に利用できます。a-blog cms をバックエンドで使用する際の注意点をいくつか紹介します。

※(例)と書かれている部分は実際には動作しません。

hx-push-url 属性を設定して正しく動作する GET

記事詳細(例)

<a>タグでの GETリクエストに対しては、htmx のドキュメントに従い、hx-get属性と hx-push-url属性を追加することを推奨します。

<a href="entry-1.html" hx-get="entry-1.html" hx-push-url="true">記事詳細</a>

hx-push-url 属性を設定しても正しく動作しない POST

(例)
この部分に結果が表示されます

POSTリクエストのケースでは、例えば a-blog cms でカスタムフィールド station を選択するための <select> を含むフォームを使用し、結果を表示するテンプレートとして include/htmx/result.html を指定する方法があります。

<form hx-post="" hx-push-url="true" hx-trigger="submit" hx-target="#search-result" hx-ext="ajax-header">
 <select name="station"> 
  <option value="Tokyo">Tokyo</option>
  <option value="Osaka">Osaka</option>
  <option value="Nagoya">Nagoya</option>
 </select>
 <input type="hidden" name="field[]" value="station">
  <input type="hidden" name="tpl" value="include/htmx/result.html">
<input type="submit" name="ACMS_POST_2GET" value="検索" >
</form>
<div id="search-result">この部分に結果が表示されます</div>

a-blog cms の仕様として、<form> の POST 時 name="ACMS_POST_2GET" が送られてくると、検索条件( station = Nagoya )から URL を組み立て、その後リダイレクトされ GET に変換され、以下のような URL でアクセスした結果を取得できます。

https://example.com/htmx/result.html/field/station/Nagoya/

ここで問題が発生します。 上記の URL がブラウザに表示され履歴にも登録されますが、この URL にアクセスすると Ajax で追加される部分的な HTML になってしまいます。この検索結果で履歴に登録されて欲しい URLは、テンプレートファイル include/htmx/result.html のパスが無い以下の URL であって欲しいのです。

https://example.com/field/station/Nagoya/

hx-push-url 属性を設定しても正しく動作する POST 解決編

ここまでで a-blog cms POSTリクエスト後に URL を組み立てる ACMS_POST_2GET の処理が、htmxhx-push-url と相性が悪い状況を解決する方法を考えてみました。

以下に示す JavaScriptコードを追加することで、hx-push-url 属性の動作を維持しつつ、希望する URL形式を browser history API に正しく渡すことができるようになります。

<script>
  addEventListener('htmx:beforeHistoryUpdate', function (event) {
    const proposedUrl = event.detail.history.path;
    const customUrl = proposedUrl.replace(/\/include\/htmx\/.*\.html/, '');
    event.detail.history.path = customUrl;
  });
</script>

処理としては htmx で History を更新する前の path を取得し、/include/htmx/ 〜 .html 部分を削除するという処理を追加しています。

一般的に a-blog cms の部分的なファイルは include ディレクトリに整理して保存するというルールが定着していますので、htmx で読み込むテンプレートファイルは、その include ディレクトリ内に htmx を用意し、その htmx ディレクトリ内で管理というルールでテーマを構築というルールにする事で上記の JavaScript が有効的に動作させることができます。

Unit_List の使用ケースその2:Google MapのAPI 「MarkerClusterer」

エントリー内のユニットを表示するUnit_Listの使用ケース紹介その2では、Google MapのAPI 「MarkerClusterer」を使ったUnit_Listの活用方法をご紹介します。

たとえば、1つの地図に各エントリーに1つずつ挿入されている地図ユニットをピンでまとめたいときに有効です。

MarkerClustererを使うことにより、同じ位置にエントリーが集中していたとしてもピンが多くなりすぎず、すっきりとした見た目になります。


遠くのエントリーは1件表示だけですが、近くの2件はまとめて表示される

3件のエントリーに1つずつ地図が入力されている場合の MarkerClusterer の例


実装方法

/js/makerclusterer.jsと/images/フォルダをGitHubよりダウンロードしてください。

ソースコード:headタグ

headタグ内に以下を記述してください。

<script type="text/javascript" src="https://maps.google.com/maps/api/js?key=%{GOOGLE_API_KEY}&sensor=false"></script> 
<script type="text/javascript" src="/js/markerclusterer.js"></script>

テーマフォルダに/images/フォルダを設置するときは、/js/makerclusterer.jsの以下の記述のパスを変更する必要があります(例: '/theme/site/images/m'; など)。

MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m';

ソースコード:MarkerClustererを設置したい場所への記述

HTMLファイル内のMarkerClustererを設置したい場所に以下を記述します。

<div id="map" style="height: 400px;"></div>

ソースコード:地図の基準にしたい緯度経度の情報を登録するカスタムフィールド

ブログのトップに表示させたい場合はブログのカスタムフィールドに、カテゴリーの一覧に表示させたい場合はカテゴリーのカスタムフィールドに記述してください。

※Site2016内の/admin/category/field_realestate.htmlと同じ内容です

<h3 class="acms-admin-admin-title2">一覧ページで表示させる地図の中心の緯度経度設定</h3>
<table class="adminTable acms-admin-table-admin-edit">
<tr>
  <th>地図</th>
  <td class="js-map-editable">
    <div class="acms-admin-form-group">
      <label for="input-text-map-search_text" class="acms-admin-hide-visually">住所で検索する</label>
      <input type="text" name="" value="" id="input-text-map-search_text" class="js-editable_map-search_text" disabled="disabled" size="40" />
      <input type="button" name="" value="検索" class="js-editable_map-search_button acms-admin-btn-admin" disabled="disabled" />
    </div>
    <div class="acms-admin-form-group">
      <img class="js-map_editable-container" src="http://maps.google.com/maps/api/staticmap?center={index_map_lat},{index_map_lng}&zoom={index_map_zoom}&size=640x400&maptype=roadmap&markers={index_map_lat},{index_map_lng}&key=%{GOOGLE_API_KEY}" width="640" height="400" style="display:block;" />
    </div>
  <!-- BEGIN index_map_lat:veil -->
  <label for="input-text-index_map_lat">緯度</label> <input type="text" name="index_map_lat" value="{index_map_lat}" size="9" id="input-text-index_map_lat" class="js-map_editable-lat" />
  <label for="input-text-index_map_lng">経度</label> <input type="text" name="index_map_lng" value="{index_map_lng}" size="10" id="input-text-index_map_lng" class="js-map_editable-lng" />
  <label for="input-text-index_map_zoom">ズーム</label> <input type="text" name="index_map_zoom" value="{index_map_zoom}" size="10" id="input-text-index_map_zoom" class="js-map_editable-zoom" />
  <!-- END index_map_lat:veil -->
  <!-- BEGIN index_map_lat:empty -->
  <label for="input-text-index_map_lat">緯度</label> <input type="text" name="index_map_lat" value="35.172775" size="9" id="input-text-index_map_lat" class="js-map_editable-lat" />
  <label for="input-text-index_map_lng">経度</label> <input type="text" name="index_map_lng" value="136.887466" size="10" id="input-text-index_map_lng" class="js-map_editable-lng" />
  <label for="input-text-index_map_zoom">ズーム</label> <input type="text" name="index_map_zoom" value="7" size="10" id="input-text-index_map_zoom" class="js-map_editable-zoom" />
  <!-- END index_map_lat:empty -->
  <input type="hidden" name="field[]" value="index_map_lat" />
  <input type="hidden" name="field[]" value="index_map_lng" />
  <input type="hidden" name="field[]" value="index_map_zoom" />
  </td>
</tr>
</table>

ソースコード:MarkerClustererを動かすためのJavaScript(ここではHTMLファイルとして使用します。例:map.html)

<script type="text/javascript">
  var map = null;
  var markerclusterer = null;
  var infowindow = new google.maps.InfoWindow();
  var gmarkers = []; 
  // マーカーの作成と、吹き出しの作成
  function createMarker(latlng, info) {
    var marker = new google.maps.Marker({
      position: latlng,
      map: map
    });
    google.maps.event.addListener(marker, 'click', function() {
      infowindow.setContent(info); 
      infowindow.open(map,marker);
    });
    // 情報を保存
    gmarkers.push(marker);
  }
  // クリックイベントを作成する
  function myclick(i) {
    google.maps.event.trigger(gmarkers[i], "click");
  }
  function initialize() {
    // 全体の地図を作成
    <!-- BEGIN_MODULE Category_Field -->
    var myOptions = \{
      zoom: {index_map_zoom},
      center: new google.maps.LatLng({index_map_lat},{index_map_lng}),
      mapTypeControl: true,
      navigationControl: true,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    \}<!-- END_MODULE Category_Field -->
    map = new google.maps.Map(document.getElementById("map"), myOptions);
    google.maps.event.addListener(map, 'click', function() {
      infowindow.close();
    });
    // 場所・吹き出しの情報を定義
    var markers = [
    <!-- BEGIN_MODULE Unit_List id="map" --><!-- BEGIN unit:loop -->
      ["{entry_title}", {unit_field_2},{unit_field_3},"{entry_url}"],<!-- END unit:loop -->[]<!-- END_MODULE Unit_List -->
    ];
    // 吹き出しに入れる要素
    for (var i = 0; i < markers.length; i++) {
      var point = new google.maps.LatLng( markers[i][1],  markers[i][2]);
      var marker = createMarker( point, "<div class='scrollFix'><p class='mapEntryTitle'>" + markers[i][0] + "</p> " + "<p class='mapDetailInfo'>緯度: " + markers[i][1] + "<br> 経度: "  +  markers[i][2] + "</p><p class='mapLinkText'><a href='"+ markers[i][3] +"'>詳しく見る</a></p>" + " </div>") ;
    }
    markers.pop();
    // 地図を表示
    markerCluster = new MarkerClusterer(map, gmarkers);
  }
  window.addEventListener('load', initialize);
</script>

注意点

  • 「MarkerClustererを動かすためのJavaScript」のソースコードは、全てJavaScriptで書かれていますが a-blog cms のモジュールを使っているためHTMLファイル内に記述します
  • 25行目ではCategory_Fieldを使用していますが、もし「地図の基準にしたい緯度経度の情報を登録するためのカスタムフィールド」をカテゴリー以外のカスタムフィールドとして設置した場合、対応するフィールドモジュールに置き換えます

モジュールIDの設定

モジュールID名「map」というUnit_Listのモジュールを作ります。


ユニットタイプの「地図」がチェックされていることを確認してください。

そのほかの設定については、必要に応じて有効にしてください。

SmartPhoto.js を a-blog cms に実装するには


最近のアップルップルの新しい動きとして、a-blog cms に実装する前に JavaScript ライブラリーとして公開して、その後の安定稼働版を取り込んでいくようにしています。ですから、a-blog cms 以外で作られた Webサイトにも利用が可能で、暫定的ではありますが、SmartPhoto.js WordPress 用のプラグイン も準備しています。

2.5.1 / 2.5.1.1 をご利用の方へ アップロード画像がPNG形式に

今回、2.5.1 で実装された新しい機能で、ブラウザ上で画像をリサイズする機能というものがあります。この機能ですが、現状の仕様だとアップロードした画像のほとんどのファイルが形式が png になってしまいます。

もし、現状のバージョンで、png になってしまう事を止めたいという事であれば、 /js/config.js の 510行目のあたりにある resizeImage の設定を off にしてください。

//--------------
// resize image
resizeImage : 'off',

標準が off になっていて、全部 png になってもいいから、大きなファイルでもアップできた方がいい人が on にする仕様にするべきでした。申し訳ありませんでした。

今回の機能としては、canvas を活用してアップロードする前にリサイズ機能をブラウザ側で実装しています。その際に、canvas では png 形式を必須としており一部のブラウザでは、jpeg をサポートしていない事から、このような仕様となっていました。

参考:

今後の改良として、大きな画像というところでは、jpeg である事が多いですので、対応していないブラウザの時のみ png にし、対応しているブラウザであった場合には jpeg になるように、次のバージョン 2.5.1.2 で改良したいと思っています。

近日中にリリース予定ですので、よろしくお願いいたします。

ver2.1.1.4でのモジュールの不具合

ver2.1.1.4をご利用の場合

2015/02/20以前にダウンロードしたver2.1.1.4パッケージで モジュールIDでグローバル利用している場合に正しい表示にならない問題が確認されています。

対応

2015/02/20以前にver2.1.1.4をダウンロードした場合は、 再度パッケージをダウンロードして頂き、/php/ACMS/function.phpを置き換えて下さい。

この度はご迷惑をおかけしてしまい申し訳ございません。 今後ともa-blog cmsをお願い致します。

jQuery2.1.1を使用した場合の不具合

jQuery2系をご利用の場合

a-blog cms ver2.1より jQuery1.11.1, jQuery2.1.1(デフォルト:1.11.1)に対応しましたが、 jQuery2.1.1に切り替えた場合にブラウザの判定周りに問題がある事が分かっています。

現象

IEでの判定周りやテキストユニットの高さを調整する機能がうまく動きません。

対応

次期バージョンで対応予定ですので、それまではjQuery1.11.1をご利用するようにお願い致します。

この度はご迷惑をおかけしてしまい申し訳ございません。 今後ともa-blog cmsをお願い致します。

a-blog cms 2.0.x系のWYSIWYGエディタ(CKEditor)でフル機能を使えるようにする


2.0.x系でCKEditorの機能が少ない問題が発見されました。

以下のバージョンでCKEditorの使えない機能がある事が発見されました。 これは、CKEditorがフルパッケージではなく、スタンダードパッケージが入っている事が原因になります。

使えない機能として、フォントサイズや、フォントカラーなどがあります。

フル機能が使えないバージョン

  • 2.0.1
  • 2.0.0.3
  • 2.0.0.2
  • 2.0.0.1
  • 2.0.0

この問題はver.2.0.1より後に出たバージョンで解決予定です。

CKEditorをフルパッケージに更新する

次のバージョンが出るまで待つのは不便ですので、CKEditorをフルパッケージにする方法をお知らせ致します。

CKEditorのフルパッケージをダウンロード

CKEditor ダウンロードからCKEditorのフルパッケージをダウンロードして下さい。

必須チェックの可変対応方法

お問い合わせフォームなどの入力項目で、チェックボックスの「その他」にチェックをした場合は、その他の内容を必須項目にして、その他にチェックを入れた場合は内容も入れてください。とするケースがあります。 そのやり方を2例紹介します。


必須チェックの可変対応方法

お問い合わせフォームなどの入力項目で、チェックボックスの「その他」にチェックをした場合は、その他の内容を必須項目にして、その他にチェックを入れた場合は内容も入れてください。とするケースがあります。 そのやり方を2例紹介します。