4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FileMakerでタグ付け機能を実装する

Posted at

コンテンツ系のサイトなどで,タグ付け機能をよく見ますよね。

↑こんなの。

  • 思いついたキーワードをどんどん登録していける
  • 入力補完(サジェスト)機能が使えるのである程度データの正規化が可能
  • キーワードから選んで検索が可能

といった利点を持った便利なUIだと思います。このタグ付け機能,FileMakerにも欲しいぞ!ということでWebビューアを利用して実装しました。せっかくなので,「FileMakerは結構使いこなしてきているけど,WebビューアとかJavaScriptとかはまだ良くわからん」といった方を対象にStep by Stepで解説してみます。

どうやってやろう?

まぁだいたいこういうのはJavaScriptのライブラリがありそうだよね,と考えて「JavaScript library tagging」で検索して一番上に出てきたのがこちら。

Taggle.js

紙パックのジュースを2本同時飲みする愉快なエンジニアが作成してくれたようである。ソースも軽そうだし依存関係もほぼ無し,というわけでFileMakerへの組み込みにはちょうど良さそう。というわけでこちらを利用させて頂きましょう。ちなみにライセンスはMIT。

Taggle.jsの基本的な使い方

詳しくは上記の公式サイト(英語)を参照すれば調べられますが,基本的なコーディングはとてもシンプル。

HTML
<div id="example"></div>
JavaScript
new Taggle('example', {
    tags: ['tag1', 'tag2', 'tag3']
});

これで「tag1」「tag2」「tag3」がデフォルトとして表示されたタグ入力フィールドが出来上がります。

コンストラクタ関数の第1引数はタグ付けフィールドにすべき<div>タグのid,第2引数に各種設定を記載したオブジェクトです。設定オブジェクトには色々な項目が設定できますが,最低限設定しておくべきは

tags
デフォルトで表示するタグ(配列形式)
preserveCase
入力された英字の大文字/小文字を保持するか。なぜかデフォルトだと false になっているのだが,日本人的には大文字で入力された英字が勝手に小文字にされるのは不自然なので true にしておく方がお勧め。
placeholder
空欄の時に表示されるメッセージ。デフォルトは英語(Enter tags...)なので適宜日本語へ。
onTagAdd, onTagRemove
タグの追加や削除に応じて発火するコールバック関数。これを用いてFileMaker側とやり取りを行う(後述)。

といったところ。重複を許可したり,ホワイトリスト/ブラックリストを設定したり,タグ数の上限を決めたり……といったことも可能なので必要であれば公式ドキュメントを参照して下さい。

FileMakerでJavaScriptライブラリを使用する時の小ネタ

Taggle.jsに限らず,FileMakerのWebビューアでJavaScriptライブラリを利用する時に問題になるのが「.jsファイルや.cssファイルなど,必要なライブラリファイルをどこから読み込むのか」ということ。

もちろん普通にCDNとかから引っ張って来たって良いんだけど……

<script src='https://cdnjs.cloudflare.com/ajax/libs/taggle/1.15.0/taggle.min.js'></script>

ただこれだとWebビューアの表示が行われる度にアクセスが行われることになるし,そもそもFileMakerの場合インターネットに接続されていない環境で動く場合も多いので,できればライブラリファイルはローカル(データベース内)に保存しておきたいわけです。

今まで色々な先達のやり方を見てきた結果,

  • Webビューアの「Webアドレス」に(HTMLを含めて)ベタ打ちで実装
  • ライブラリの中身を吐き出すカスタム関数を作成しておく
  • ライブラリの中身を入力したオブジェクトを作成し,getLayoutObjectAttribute関数を用いて中身を取り出す

といったやり方が観察されたのだけど,どれも可読性・メンテナンス性に乏しいので最近私が落ち着いたのが「ライブラリファイル専用のテーブルを1つ用意し,ファイルごとにグローバルフィールドを作成して格納する」という方法。これならば可読性も高いし,ライブラリがアップデートされた時でもコピペするだけなので更新が容易でおすすめ。

個人的にはテーブル名はsourcesにしているけど,これは何でもOK。

登録しておきたいファイルごとにフィールドを作成します。

オプションで「グローバル格納を使用する」にチェックしておくのを忘れずに。

データベースの設定が終了したら,レイアウトも作成しておきましょう。タブコントロールで切り替えられるようにしておくと,ライブラリファイルが増えても管理しやすいです。フィールドはちょっと「それっぽく」なるようにフォントを等幅にするなり,背景を黒にするなり,まぁこの辺はお好みで。

フィールドはスクロールバーの表示と,「視覚的なスペルチェックを適用しない」にチェックしておくとよいです。

レイアウトが終了したらブラウズモードに戻ってフィールドにファイル内容をコピペします。Taggle.jsであればダウンロードしたファイルの「src」フォルダにある「taggle.js」と「minimal.css」をそれぞれコピペ。

この状態でライブラリを使いたい時はこのグローバルフィールドを参照するのでも良いんですが,グローバル変数に読み込んでおいた方がメモリに保持されるので動作が速くなるし,何かと使いやすいです。基本的にライブラリはデータベースの使用中に内容が変わるわけではないので,データベースの起動時にグローバル変数にロードするスクリプトを組んでおきましょう。

loadSources
# ファイルを開いた際にsourcesテーブルの各ファイルをグローバル変数にロードする
エラー処理 [オン]
ユーザによる強制終了を許可 [オフ]

変数を設定 [$$taggle.js; 値: sources::taggle.js]
変数を設定 [$$taggle.css; 値: sources::taggle.css]

このスクリプトを「ファイルオプション...」の「スクリプトトリガ」からonFirstWindowOpenに設定しておけば,あとはライブラリファイルを使用したいところでグローバル変数を呼び出せば良いことになります。

データ保持用のフィールドを作成する

さて,今回作成するタグ付け機能だけど,当然ながらあくまでWebビューアはUIとして利用するだけなので入力されたタグのデータを保持するフィールドを作成しておく必要があります。JavaScriptとのやり取りをすることを考えるとJSON形式でデータ保持しておいた方が何かと便利なので,Taggle.jsの形式にあわせて

{"tags": ["foo", "bar"]}

というJSON形式のテキストを保持するフィールドを作成します。

今回はファイル管理用のデータベースの想定で,メインのテーブルは「files」テーブルです。ここにtags_jsonという名前のテキストフィールドを作成し,フィールドオプションで「入力値の自動化」を「データ」にして{"tags":[]}という初期値を設定しておきましょう。

Webビューア側からのデータを受け取るスクリプトを作成する

タグのデータを保管するフィールドは作成しましたので,次にWebビューア内で入力されたタグのデータをこのフィールドへ保存するためのスクリプトを組んでいきましょう。

前述のようにTaggle.jsの設定オブジェクトには,タグが追加されたり削除されたりといったイベントに応じてフックされるコールバック関数を設定することが出来ます。

{
  onTagAdd: function(event, tag) {
    // タグが追加された時の処理
  },
  onTagRemove: function(event, tag) {
    // タグが削除された時の処理
  }
}

コールバック関数の引数であるtagには追加/削除されたタグの文字列が渡されます。

Webビューア内のJavaScriptからはFileMaker.PerformScriptWithOption関数を呼び出すことでFileMaker側のスクリプトにデータを渡して処理を引き継ぐことができますので,コールバック関数からこれを呼び出してタグのデータを渡してあげれば良さそうです。

FileMaker.PerformScriptWithOption関数の詳しい使い方はこちら↓

さて,先ほど見たようにTaggle.jsのイベントフックされるコールバック関数は,引数として追加/削除されたタグの文字列が利用できます。もちろんこのデータをFileMaker側に渡し,スクリプトでtags_jsonフィールドの中身を書き換えるのでも良いのですが,追加と削除で2つスクリプトを用意しなきゃならないし,ちょっと面倒くさい。

Taggle.jsにはgetTagValues()という,設定されている全タグの文字列を配列形式で返してくれる便利なメソッドが用意されていますので,これを利用すれば追加でも削除でも「最新のデータ」をFileMaker側に渡すことができます。そうすれば,FileMaker側では受け取ったデータをその都度保存用のtags_jsonフィールドに設定するだけで処理が済むのでシンプルです。

というわけで,JavaScript側からタグ文字列の配列データが渡される想定でFileMaker側のスクリプトを組んでおきましょう。

updateTags
# Webビューアのタグが追加/削除された際,フックされたコールバック関数から渡されるタグのデータ(文字列の配列)をtags_jsonフィールドにJSON形式で保存する
エラー処理 [オン]
ユーザによる強制終了を許可 [オフ]

フィールド設定 [files::tags_json; JSONSetElement ( "{}" ; "tags" ; Get ( スクリプト引数 ) JSONArray ) ]

実質1行の単純なスクリプトです。Get ( スクリプト引数 )でJavaScript側から渡された文字列の配列データを取り出せるので,これをJSONSetElement関数でJSON形式にしてからtags_jsonフィールドに設定しているだけです。

Webビューアに設定するHTMLを作成する

いよいよWebビューア用のHTML & JavaScriptをコーディングしていきます。

Webビューア用のHTMLでは,<script>タグ内のJavaScriptでTaggle.jsのコンストラクタ関数を呼び出す際,先ほど作成したtags_jsonフィールドの内容を初期値として設定しなければなりません。また,先ほどグローバル変数から利用できるようにしておいたライブラリファイルも入れ込む必要があります。そのため,予めテンプレートのHTMLを用意しておき,ライブラリファイルやタグの初期値を置き換えてからWebビューアに表示するという手法を取ることにします。

テンプレートのHTMLの完成形が以下の通りです。

tagViewer.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <style type="text/css">
      body {
        margin: 0;
        padding: 8px;
        overflow: auto;
        font-family: sans-serif;
        font-size: 16px;
        line-height: 1.5;
      }
    </style>
    <style type="text/css">
      $$taggle.css
    </style>
  </head>
  <body>
    <div id="taggle"></div>
    <script>
      $$taggle.js
    </script>
    <script type="text/javascript">
      var taggle = new Taggle('taggle', {
        tags: ~tagsArray,
        preserveCase: true,
        placeholder: 'タグを入力...',
        onTagAdd: function(event, tag) {
          FileMaker.PerformScriptWithOption ( 'updateTags', JSON.stringify( taggle.getTagValues() ), '0' );
        },
        onTagRemove: function(event, tag) {
          FileMaker.PerformScriptWithOption ( 'updateTags', JSON.stringify( taggle.getTagValues() ), '0' );
        }
      });
    </script>
  </body>
</html>

<meta>タグの中のcharset="utf-8"を忘れるとWindows版のFileMakerでは日本語が文字化けするので忘れないようにしましょう。

1つめの<style>タグの中にはWebビューア全体としてのスタイル設定をベタ打ちしています。overflow: auto;に設定することでタグが増えてきても自動的にスクロールバーが表示されるようになります。font-familyは設定しておかないとMacの場合はセリフ体(明朝体っぽいやつ),Windowsの場合はサンセリフ体になります。font-sizeline-heightはお好みで。

    <style type="text/css">
      body {
        margin: 0;
        padding: 8px;
        overflow: auto;
        font-family: sans-serif;
        font-size: 16px;
        line-height: 1.5;
      }
    </style>

2つめの<style>タグの中身は後でライブラリファイルの内容に置き換えますので,ここでは先ほど設定したグローバル変数名を入れているだけです。

    <style type="text/css">
      $$taggle.css
    </style>

同様に,1つめの<script>タグの中身もグローバル変数名のみとしています。

    <script>
      $$taggle.js
    </script>

2つめの<script>タグの中が実際の処理になります。

    <script type="text/javascript">
      var taggle = new Taggle('taggle', {
        tags: ~tagsArray,
        preserveCase: true,
        placeholder: 'タグを入力...',
        onTagAdd: function(event, tag) {
          FileMaker.PerformScriptWithOption ( 'updateTags', JSON.stringify( taggle.getTagValues() ), '0' );
        },
        onTagRemove: function(event, tag) {
          FileMaker.PerformScriptWithOption ( 'updateTags', JSON.stringify( taggle.getTagValues() ), '0' );
        }
      });
    </script>

設定オブジェクトのtagsには本来であればタグ文字列の配列データが入りますが,後でFileMaker側のフィールドデータから置き換えるためここでは~tagsArrayという文字列のみ記述しておきます。

onTagAddonTagRemoveのイベントフックの記述はどちらも共通です。FileMaker.PerformScriptWithOptionで先ほど作成したFileMaker側のスクリプトを呼び出し,taggle.getTagValues()で取得したタグデータをJSON.stringifyで文字列に変換して渡しています。

このテンプレートHTMLも,先ほどのライブラリファイルと同じようにsourcesテーブルにグローバルフィールドを作成し,その中に記述しておきましょう。

データベース起動時の読込スクリプトにも追加しておくのを忘れずに。

loadSources
  # ファイルを開いた際にsourcesテーブルの各ファイルをグローバル変数にロードする
  エラー処理 [オン]
  ユーザによる強制終了を許可 [オフ]
  
  変数を設定 [$$taggle.js; 値: sources::taggle.js]
  変数を設定 [$$taggle.css; 値: sources::taggle.css]
+ 変数を設定 [$$taggle.css; 値: sources::taggle.css]

終わったら一度スクリプトを実行してグローバル変数にロードしておきましょう。

Webビューアを設定する

さあ,準備は整いました!早速Webビューアの設定をしていきます。

メインのテーブルのレイアウトにWebビューアを設置します。

「Webビューアの設定」で「進行状況バーの表示」「ステータスメッセージの表示」のチェックを外し,「JavaScriptによるFileMakerスクリプトの実行を許可」にチェックを入れておきます。

その上で「Webアドレス」には以下の様に記述します。

Substitute ( $$tagViewer.html ; 
  [ "$$taggle.js" ; $$taggle.js ] ;
  [ "$$taggle.css" ; $$taggle.css ] ;
  [ "~tagsArray" ; JSONGetElement ( files::tags_json ; "tags" ) ]
)

Substitute関数はテキストの置換を行う関数です。ここではグローバル変数$$tagViewer.htmlに読み込んだテンプレートHTMLを対象として,$$taggle.js $$taggle.cssの部分をそれぞれグローバル変数に読み込んだライブラリファイルに置換し,さらに~tagsArrayで記述しておいた既存タグの部分をtags_jsonフィールドに保存してあるJSON形式のタグデータからJSONGetElement関数で抽出した配列に置き換えています。

置き換え前の文字列はダブルクォーテーションで括るのを忘れずに!

動作確認&微調整

ではブラウズモードに切り替えて動作を確認してみます。

先ほどレイアウト画面で設置したWebビューアにHTMLが読み込まれ,設定した「タグを入力...」というプレースホルダテキストが表示されています。

テキストを入力してreturnキーやenterキーで確定すると……

タグになりました!

もちろんタグはいくつでも追加していくことができますし……

タグにマウスポインタを合わせると削除ボタンが表示されるので,簡単に削除もできます。


良さげです!😆

検索用フィールドを作成する

さてこれでひとまずタグ入力はできるようになりましたが,このままでは検索ができません。データ保存用のtags_jsonフィールドを用いて検索をしても良いのですが,JSON形式にするための余計な文字列が入っているので,FileMakerのリスト形式(改行区切りの文字列)でタグデータを保持するフィールドを作成しておきましょう。

tags_listという名前で計算フィールドを作成し,内容を以下のように記述しておきます。

JSONListValues ( tags_json ; "tags" )

計算結果を「テキスト」にしておくのを忘れずに。

検索モードでWebビューアの代わりに表示するようにしたいので,レイアウト画面でWebビューアに重ねてtags_listフィールドを配置します。

検索モード以外で表示する必要はないので,インスペクタの「次の場合にオブジェクトを隠す」をGet ( ウインドウモード ) ≠ 1に設定し,「検索モードで適用」をチェックしておきます。同様にWebビューアはGet ( ウインドウモード ) = 1で隠すように設定しておきましょう。

これで検索モードにしてみると……

Webビューアが隠れ,代わりにtags_listフィールドが表示されるようになりました。実際に検索が可能になっていることを確かめておきましょう。

サジェスチョン(オートコンプリート)っぽい機能を追加する

最初にも述べたように,タグ入力UIの大きな利点の一つが「既に他のレコードに入力されているタグを候補として出すことで,ある程度のデータ正規化が可能」という点があります。

「Javas」しか打ってなくても「JavaScript」やら「JavaSilver」やらが出てきてるこれですね。(「JavaScirpt」やら「JavaScriptpt」やらもあって「ちゃんと正規化出来てないやんけ!」というのはさておき……)

Taggle.jsをはじめとしたタグ付け用のライブラリには,大抵jQueryなどと連携してサジェスチョン機能を実現するためのAPIが実装されています。しかしこれをFileMakerのWebビューアで利用しようとするとうまくいきません。

というのも,当然のことながらWebビューアはレイアウト画面で配置したサイズ内でしかコンテンツの描画が出来ないのですが,JavaScriptを用いてサジェスチョンの候補を表示する場合,普通は入力しているテキストの下に新たな<div>要素を描画する形になるので,Webビューアの描画可能領域をはみ出てしまうのです。

したがってFileMakerでサジェスチョン機能を実現しようとすると,Webビューアの中ではなく外に機能を実装するのが現実的な解になります。

具体的にはサジェスチョン候補用の値一覧を作成し,タグ追加用のフィールドを新たに設定した上でスクリプトからタグの追加を行います。

サジェスチョン候補用の値一覧の作成は簡単です。検索用にタグデータをリスト形式で保持するtags_listフィールドを既に作成しているので,このフィールドから値一覧を作成すればOK。今回はexistingTagsという名称にしました。

値一覧を作成し,「フィールドの値を使用」に設定します。

「フィールド設定」ダイアログで検索用に作成したtags_listフィールドを指定すればOKです。

次にこの値一覧からタグを選択したり,入力をオートコンプリートさせるためのフィールドを設定します。テキストフィールド(今回はtagSuggestionという名称にしました)を作成し,Webビューアの下に配置しておきましょう。

コントロールスタイルは「ドロップダウンリスト」に設定し,値一覧を先ほど作成したexistingTagsに設定して「一覧の表示切り替え用矢印を表示」「値一覧を使用してオートコンプリート」にチェックします。検索モードで表示する必要はないので,Webビューアと同じく検索モードで隠すように設定しておきましょう。また,タグに複数行のテキストを入力することはないので「次のオブジェクトへの移動に使用するキー」の「Return」「Enter」にもチェックを入れておきます。

スクリプトも単純です。tagSuggestionフィールドに設定されているタグをtags_jsonフィールドのタグ一覧に追加してあげればOK。ただしtagSuggestionフィールドが空白でないかのチェックと,値一覧には既に登録されているタグも表示されてしまうので重複チェックもしておきましょう。

こんな感じ↓

addTag
# サジェスチョン用フィールドからタグを追加する
エラー処理 [オン]
ユーザによる強制終了を許可 [オフ]

# 空白&重複チェック
If [ files::tagSuggestion = "" ]
    現在のスクリプト終了[ テキスト結果: ]
Else If [ PatternCount ( files::tags_list & "¶" ; files::tagSuggestion & "¶" ) > 0 ]
    カスタムダイアログを表示 ["そのタグは既に登録されています。"]
Else
    # 重複していなければ tags_json フィールドの内容を更新
    フィールド設定 [ files::tags_json ; Let ( ~n = ValueCount ( files::tags_list ) ; JSO...]
End If

# サジェスチョン用フィールドを消去して終了
フィールド設定 [ files::tagSuggestion ; "" ]

重複チェックではPatternCount関数を用いて検索用のtags_listフィールドのテキストに対してtagSuggestionフィールドの内容で検索を行っています。双方に& "¶"を追加しているのは,タグ文字列の一部分にヒットしてしまわないようにするためです(これをしておかないと例えば「アイスクリーム」が登録されている場合「アイス」が重複と判断されてしまいます)。

重複していなかった際のtags_jsonフィールドの内容は以下の様にLet関数を用いた計算式で設定しています。

Let ( 
  ~n = ValueCount ( files::tags_list )
;
  JSONSetElement ( files::tags_json ; "tags[" & ~n & "]" ; files::tagSuggestion ; JSONString )
)

新規に追加するタグは既存のタグリストの最後に追加しますので,まず既に設定されているタグ数を取得します。これは検索用のtags_listフィールドに対してValueCount関数を用いればOK。

その後,JSONSetElement関数でtags_jsonフィールドに入力されているタグデータの最後にtagSuggestionフィールドの内容を設定します。JSONの配列索引は0からスタートするので,先ほど取得したタグ数をそのまま用いて"tags[~n]"という配列索引を指定すればリストの最後に追加できます。

最後にスクリプトを実行するためのボタンをレイアウトに追加し,「ボタン設定」を「スクリプト実行」にして先ほど作成したスクリプトを指定しましょう。検索モードで隠す設定にしておくのを忘れずに。

完成したらブラウズモードに切り替え,新しいレコードから既存のレコードのタグが選択できること,ボタンを押すとWebビューア側でタグが追加されることを確認しましょう。フィールドに直接入力すればオートコンプリートも効くはずです。

ちょっとしたUX向上の工夫

もちろんこのままでも十分使えますが,少し工夫することでユーザエクスペリエンスを向上させましょう。

まず今のままだと,tagSuggestionフィールドが確定された際,「追加」ボタンではなく「タイトル」フィールドへフォーカスが移動してしまいます。

そこでレイアウト画面の「レイアウト」→「タブ順設定」から,tagSuggestionフィールドを確定したら「追加」ボタンにフォーカスが移動するように設定します。

「設定」ボタンのタブ順がtagSaggestionフィールドのタブ順の次になるように設定します。

せっかくですから,フォーカスしていることがユーザにわかりやすいよう,フォーカス時のボタンのアピアランスを変更しておきましょう。

インスペクタの「外観」タブから,フォーカス時のボタンの塗りつぶしを目立つ色に変更します。これでtagSuggestionフィールドを確定すると「追加」ボタンの外観が変化し,ユーザへボタンクリックを促すことが可能となりました。

また,複数のタグを連続して入力するような場合,タグを追加した後は自動的にtagSuggestionフィールドへフォーカスが戻ってくれると便利ですよね。そこでaddTagスクリプトの最後に次の一文を追加しておきましょう。

addTag(追加分)
フィールドへ移動 [files::tagSuggestion]

これでボタンからタグを追加すると,tagSuggestionフィールドにフォーカスが戻ってくるようになりました。キーボードからの入力でも,「return/enter」キーで確定→追加→次のタグを入力……とマウスに触れることなく作業を進めていくことが出来るので捗ります。

なおtagSuggestionフィールドを確定した状態でタグを追加せずに他の操作に移る場合,tagSuggestionフィールドはリセットしておいた方が親切かもしれません。その場合,レイアウトのonRecordCommitスクリプトトリガに以下のスクリプトを設定しておきましょう。

onRecordCommit
フィールド設定 [files::tagSuggestion ; "" ]

おしまい。

サンプルファイル

下のリンクから実際のファイルをダウンロードできますのでご自由にどうぞ。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?