概要
- 次はWeb屋さんに転職するので、勉強も兼ねて Vue.js + Vuetify でUIを作っていた矢先。
- せっかくだからレスポンシブにして、実機で確認していたら、その時は訪れました。
- クリック(タップ)すると編集モード(テキストフィールド)になるようなUIで、
- 編集モードだけ表示されるボタンを置いたら、
- PC版Chromeのエミュレーターではタイトルのイベントが飛ぶのに、実機では飛びませんでした。
- 自分へのメモも兼ねています。
環境
プラットフォームの関係からNode.jsを使わずにJavascriptだけで書いています。jqueryも使える環境なので結構使ってます。
最初の実装
今回作っていたのは、名前と予定が一覧表示され、クリックするだけで自由に編集できる、会社によくあるホワイトボードのようなものです。以下、usersにユーザオブジェクトがリストされていてのループでuserを表示するくだりです。
:
<template v-for="(user) in users">
<v-layout wrap class="user">
<template v-if="user.editTask">
<v-flex>
<v-text-field v-model="user.task" @keyup.enter="updateTask(user)" @blur="updateTask(user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
</v-flex>
</template>
<template v-else>
<v-flex class="user-name" @click="editTask(user)">{{user.name}}</v-flex>
<v-flex class="user-task" @click="editTask(user)">{{user.task}}</v-flex>
</template>
:
,
methods: {
editTask: function(user) {
user.editTask = true;
this.$nextTick(function() {
var ref = this.$refs['edit' + user.id][0];
ref.focus();
});
},
updateTask: function(user) {
user.editTask = false;
// ここでDBを更新する処理など...
},
:
ユーザの名前と予定が一覧表示されていて、クリックすると editTaskメソッドが呼ばれ、予定を入力するためのテキストフィールドが現れます。尚、$nextTickのくだりは、出現したテキストフィールドにフォーカスを当てるためのものです。Enterを押すか、フォーカスが外れたらupdateTaskメソッドが呼ばれ、名前と予定の表示に戻ります。ここまでは大きな問題もなく、PCでもモバイルでも想定通り動作しました。例にはちゃんと書いてませんが、レイアウトはVuetifyのGrid systemを活用しました。元々Bootstrapを使っていたので、特に迷うことなく移行できました。
使い勝手を改善する
その後、予定を文章で入力するのがめんどくさい、という人のために、テキストフィールドの横に一発で文章を入力するためのボタンを置こうと思いつき、次のようなテンプレートに変更しました。
:
<template v-if="user.editTask">
<v-flex>
<v-text-field v-model="user.task" @keyup.enter="updateTask(user)" @blur="updateTask(user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
</v-flex>
<v-flex>
<v-btn small round class="quick-task" color="primary" text-color="white" @click="addTask(user,'打合せ')">打合せ</v-btn>
</v-flex>
</template>
:
,
methods: {
:
addTask: function(user,text) {
user.task = user.task + text;
},
UI自体は簡単にできたのですが、ボタンのクリックよりもフォーカスが外れるイベントが先に飛ぶのでしょうね、ボタンをクリックしようとしてもボタンは消え、実際にはクリックされずにaddTaskメソッドが呼ばれません。なんとなく押したような感じにはなるのが憎いです。で、何か手立てはないか探してみたところ、event.relatedTargetに関連する要素が入るようで、ボタンをクリックしたときはここにボタンの要素が入ってました。そこで、以下のように修正しました。
:
<v-text-field v-model="user.task" @keyup.enter="updateTask($event,user)" @blur="updateTask($event,user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
:
,
methods: {
:
updateTask: function(event, user) {
if (event && event.relatedTarget && $(event.relatedTarget).hasClass('quick-task')) {
event.stopPropagation();
return;
}
user.editTask = false;
// ここでDBを更新する処理など...
},
addTask: function(user,text) {
user.task = user.task + text;
this.updateTask(null, user);
},
:
先にupdateTaskメソッドが呼ばれ、event.relatedTargetがあれば、クラスを使って該当のボタンであればイベントをキャンセルするようにします。そうすると消えずにボタンがクリックできるので、addTaskメソッドが呼ばれます。指定した文章を追加した後に、updateTaskメソッドをevent引数をnullで呼び、イベントチェックの部分をパスさせます。そうして、名前と予定の表示に戻す処理を行うことで、最初のUIと同じ動きになりました。Chromeのdevice toolbarでiPhoneのエミュレートなどに切り替えても問題無く動作しました。しかし、そう甘くは無かったのです!
実機だと動かない
実現しようとしているプラットフォームは、Viewやスクリプトでカスタマイズできるようになっていて、モバイル端末用のアプリやMobile Safariなんかでも動作します。既述のようにエミュレータでも動作したので、実機で確認してみました。が、ダメでしたorz... event.relatedTargetが null のままのようです。stack overflow には以下のような記事が。
Javascript: on blur getting the event's relatedTarget on mobile safari
記事にあったリンク先に、mousedownで予めフラグを立てといてblur時にごにょごにょする、というようなアイディアがあったので、touchstartでごにょごにょすることにしました。まず、以下のようにイベントハンドラを追加します。
<v-btn small round class="quick-task" color="primary" text-color="white" @touchstart="touch" @click="addTask(user,'打合せ')">打合せ</v-btn>
touchメソッドではevent.targetをチェックしてフラグをtrueにします。が、ハンドラを記述した要素からしか呼ばれないでしょうから、要素のチェック処理は要らないかもしれません。updateTaskメソッドの方は、this.touch = true の場合もblurイベントを止めるようにします。
touch: function(event) {
if (event && event.target && $(event.target).parent().hasClass('quick-task')) {
this.touch = true;
}
},
updateTask: function(event, user) {
if (this.touch || event && event.relatedTarget && $(event.relatedTarget).hasClass('quick-task')) {
this.touch = false;
event.stopPropagation();
return;
}
this.touch = false;
user.editTask = false;
// ここでDBを更新する処理など...
},
最後に
この実装で、実機はもちろんですが、Chromeのエミュレータ環境でも動作するようになりました。ちょっとworkaroundな気もしますが、アイディアの一つとしてシェアしておきます。Vue.jsやVuetifyは本質的には関係無いと思いますが、あくまで題材ということでご容赦ください。また、潜在的な課題として、PC環境でTABでフォーカスを移すとちょうど該当のボタンに当たってしまい、フォーカス処理が働かない(クリックも飛ばない)というのがありますが、そちらはまたなんとかしようと思います。なにぶんフロント初心者ですので、それはやっちゃダメ、こういうやり方もあるよ、というご意見が助けになりますので、ぜひお待ちしております。