音楽サーバ"Mopidy"のフロントエンドを作る:14 Linuxでハマりやすいところ
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第14回です。
今回は、Asp.Net CoreアプリをLinuxで動かす際にハマるポイントを追っていきます。
ソケットは自動で破棄されない
C#の動作環境である.Net VMはガベージコレクションが付いてます。
なので、インスタンスの破棄を意識しないで書いていても、それなりに動いてくれます。
しかし、LinuxのSocket周りには注意が必要です。
Windows機の場合は、参照されなくなったSocketはガベコレが回収してくれます。
ところがLinuxでは、意図して破棄しないと、Socketをすぐに使い切ってしまいます。
昨今のC#では、TCP/UDPのSocketを生成する場面は少ないかもしれません。
しかし、例えばHttpClient
。
外部のWebサービスを使う際に便利な実装ですが、内部的にはSocketが生成されます。
これをDisposeしないまま放置しておくと、LinuxではやがてSocketを使い切ってしまい、接続出来なくなってしまいます。
例えば、こんな検証コードを書いてみました。
public class Program { // Set your Mopidy Address private const string MopidyRpcUrl = "http://192.168.254.251:6680/mopidy/rpc"; private static int _count = 0; public static void Main(string[] args) { var withUsing = false; if (args.Contains("--withoutusing")) withUsing = false; else if (args.Contains("--withusing")) withUsing = true; Console.WriteLine("Start Socket Overflow Test: " + ((withUsing) ? "with Using" : "without Using")); while (true) { try { Program._count++; var sendJson = $"{{\"jsonrpc\": \"2.0\", \"method\":\"core.playback.get_state\", \"id\": {Program._count}}}"; var content = new StringContent(sendJson, Encoding.UTF8, "application/json"); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine($"Try {((withUsing) ? "with Using" : "without Using")} Count: " + Program._count.ToString()); Console.WriteLine("Send Message: " + sendJson); var result = (withUsing) ? Program.GetResultWithUsing(content) : Program.GetResultWithoutUsing(content); Console.WriteLine("Result: " + result); Task.Delay(200).GetAwaiter().GetResult(); } catch (Exception ex) { Console.WriteLine($"Exception!"); Program.DumpException(ex); Task.Delay(1000).GetAwaiter().GetResult(); } } } private static string GetResultWithUsing(StringContent content) { using (var client = Program.GetHttpClient()) { var message = client.PostAsync(Program.MopidyRpcUrl, content).GetAwaiter().GetResult(); var result = message.Content.ReadAsStringAsync().GetAwaiter().GetResult(); return result; } } private static string GetResultWithoutUsing(StringContent content) { var client = Program.GetHttpClient(); var message = client.PostAsync(Program.MopidyRpcUrl, content).GetAwaiter().GetResult(); var result = message.Content.ReadAsStringAsync().GetAwaiter().GetResult(); return result; } private static HttpClient GetHttpClient() { var result = new HttpClient(); result.DefaultRequestHeaders.Accept.Clear(); result.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json") ); result.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter"); result.Timeout = TimeSpan.FromMilliseconds(1000); return result; } private static void DumpException(Exception ex) { Console.WriteLine("----------------------------------------"); Console.WriteLine($"Message: {ex.Message}"); Console.WriteLine($"StackTrace: {ex.StackTrace}"); if (ex.InnerException != null) Program.DumpException(ex.InnerException); } }
MopidyのJSON-RPCに、単純なクエリを繰り返すコードです。
引数を"--withusing"とすると、HttpClientをusingでラップして破棄するように。
"--withouusing"とすると、破棄しないで放置するようにしています。
このコードで、linux用バイナリを生成します。
# dotnet publish -c Release -r linux-x64
これをUbuntu18.0.4LTSで実行してみます。
ミニマルインストール後に、SSHと、実行ファイルコピー用のSambaを入れました。
まず、"--withoutusing"スイッチを付けて実行してみると...
接続回数1004回目で、落ちてしまいます。
CentOS7.6でも試してみます。
やはり、1004回目で落ちますね。
そして、"--withusing"スイッチを付けて実行してみると。
Ubuntu18では...
問題なし、です。
CentOS7.6でも、問題ありません。
Windowsでも実行してみます。
まずば実行バイナリの生成。
# dotnet publish -c Release -r win-x64
そして、Linuxでは落ちてしまう"--withoutusing"スイッチを付けて実行すると...
こちらは、問題なく動き続けてしまいます。
このように、Windowsでは動作に問題ないコードであっても、Linuxでは動かなくなることがあるんですね。
C#で通信している箇所は要注意、と見ておいたほうが良いでしょう。
一部NuGetパッケージのバージョン暗黙解釈に失敗する
下記は、Asp.NetCore2.2-MVCプロジェクト生成直後のcsprojファイルです。
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" /> </ItemGroup> </Project>
Microsoft.AspNetCore.App
に、バージョンが書かれていないんですね。
しかし、NuGetパッケージマネージャで見てみると。
ここではきちんと、v2.2.0
と表記があります。
どうやら、バージョンを暗黙的に解釈しているようです。
しかしこのままコードを作り進み、Linuxに持って行ってpublishしてみると...
[umuser@ume01srv tmp]$ dotnet publish -c Release -r linux-x64 Microsoft (R) Build Engine version 16.1.76+g14b0a930a7 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version. [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore (>= 2.2.6 && < 2.3.0) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.EntityFrameworkCore (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore.Design from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version. [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore.Design (>= 2.2.6 && < 2.3.0) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.EntityFrameworkCore.Design (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore.Tools from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version. [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore.Tools (>= 2.2.6 && < 2.3.0) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.EntityFrameworkCore.Tools (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] Restore failed in 1.13 sec for /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj.
こんなふうに、NuGetパッケージの整合性が取れずにエラーになることがあります。
この場合は、csprojファイル上のMicrosoft.AspNetCore.App
パッケージに、バージョンを書き加えます。
<PackageReference Include="Microsoft.AspNetCore.App" />
↓
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" />
これでpublishが通るようになりました。
今のところ、Microsoft.AspNetCore.App
以外で、バージョン表記が無いという現象に行き当たったことはありません。
が、時々はcsprojファイルの中身もチェックしといた方が良さそうです。
パス文字列の大文字/小文字
これは言語に関わらず、出てくる問題ですね。
Webプログラミングをやっていれば必ず直面する問題のため、みなさま恐らく普段から気をつけていらっしゃると思います。
私も気を付けてはいたのですが...。
Mopidy.Finderでも、一度直面しました。こちらのコミットです。
キャメルケースのフォルダ名を「Sidebars」→「SideBars」と変更した際の現象です。
Windows上では「SideBars」に変わっていたのですが、WindowsのGitクライアントがその違いを認識できておらず。
Gitリポジトリ上ではずっと、「Sidebars」として保持されていました。
TypeScriptのコンパイルをLinux上で実行することは滅多に無かったため、リリース直前まで気が付きませんでした。
いやはや、お恥ずかしい。
RaspberryPi(Raspbian)は32bitOS
最近のラズパイのCPUは既に64bit化されているのですが、OSはまだ32bit版なんですね。
CentOSやUbuntuで動かしたバイナリをそのままコピペしてしまい、起動しない原因が分からずにしばらく悩みました。
Raspbianで動かすためのバイナリを作るには、publishの引数を下記のようにします。
# dotnet publish -c Release -r linux-arm
でも、乗り越えてしまえば
こまごまと問題に直面することも、あるとはいえ。
それらを乗り切ってしまえば、.Net Coreアプリは快調に動いてくれます。
最近のpublishの処理は、大変優秀です!
ミニマルインストールしたLinuxにバイナリをコピペするだけで、すんなりと動きます。
C#erとしては、まったく良い時代になったもんだ、としみじみ思います。
以上、Linuxのハマりポイントのおはなし、でした!
これで、予定していた記事を全て書き終えました。
またC#ネタが出てきたら、適当に書き連ねようと思います。
長々と駄文にお付き合いいただき、ありがとうございました!
音楽サーバ"Mopidy"のフロントエンドを作る:13 モバイルデバイスでデバッグする
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第13回です。
今回は、iOSとAndroidでのデバッグ方法を追っていきます。
iOSは、エミュレータでお手軽に
iOSでのデバッグは、macさえあれば、とてもお手軽に出来ます。
Xcodeをインストールすると、各種iOSデバイスのエミュレータが付いてきます。
まずはSafariを起動し、開発者ツールを有効にします。
Safariのメニューから、環境設定を選んで...
「詳細」メニューの一番下、「メニューバーに"開発"メニューを表示」にチェック。
すると、Safariのメニューに"開発"が追加されます。
このエミュレータの中のSafariを起動して、デバッグするURLを入力します。
デバッグするアプリの画面が出てきたところで...
mac上のSafariに戻り、"開発"メニューから「Simulator」を選ぶと。
現在エミュレータで表示中のURLが出てきます。
これを選択すれば、Safariの開発者ツールが出てきます。
ツール上でDOMを選択すると、エミュレータ画面上でハイライトしてくれますね。
お手軽ですね!
「macを買わなきゃいけない」という、Windowsユーザーにとっては大変高いハードルを、乗り越えさえすれば...。
Appleさんは、ご自身のご商売をきちんと分かっていらっしゃいますねぇ。
Androidは、まず環境づくりから
一方、AndroidはWindowsでもデバッグ可能です。
Androidの場合、エミュレータよりも実機を使う方が手軽です。
ただし、あらかじめAndroid SDKをインストールしておく必要があるようです。
以前の記事でVisual Studioのセットアップフローを書きましたが、Visual Studio Installerの機能選択時に「.NETによるモバイル開発」(=Xamarin)を選んでいる方は、既にインストールされています。
もし入れていらっしゃらないようなら、Android Studio
をインストールすると、GUIのSDK管理ツールも一緒に入ってきます。
ダウンロードはこちらから。
https://developer.android.com/studio/index.html?hl=ja
インストールして起動すると、起動画面の"Configure"メニューに、Android SDKの管理用GUIツールがあります。
ここで、お手元のAndroid機のOSバージョンに合わせてSDKを選びます。
選んでから、下の「Apply」ボタンを押すと、SDKがインストールされます。
Android機の準備
開発PC側の準備が出来たら、次はAndroidデバイスの準備です。
最近のAndroid機では標準で非表示の、「開発者向けオプション」を表示させます。
デバイスの設定を開き、"端末情報"を選びます。
その中の、"ソフトウェア情報"を選択。
ここに出てくる、"ビルド番号"。これを、連打します。
7回タップすると、開発者モードに切り替わります。
設定メニューに戻ってみると、"開発者向けオプション"が表示されています。
デバイスのメーカーやOSバージョンによって、若干表記が異なります。
例えばこちらは、AmazonのFireTablet-2015年版。
設定メニューから、"端末オプション"に入ります。
その中の、"シリアル番号"を連打すると、開発者モードになります。
MediaPadM5では、"設定"→"システム"→"タブレット情報"の中の、"ビルド番号"でした。
また古いAndroid機では、最初から"開発者向けオプション"が出ているものも。
一度"開発者向けオプション"を出してしまえば、あとはどの端末でも同じです。
"開発者向けオプション"を開き、"USBデバッグ"のチェックをONにします。
ONにするとき、こんな警告が出てきます。
Android機で、いざデバッグ
Android機の準備が出来たところで、USBケーブルでPCにデバイスを繋げます。
最近のデバイスでは、PCに接続した際にドライバがインストールされます。
そして、デバイスでChromeを起動し、デバッグするURLを開きます。
次に、PCでChromeを起動します。
そしてURLに、下記を入力します。
chrome://inspect/#devices
すると、PCのChromeはこんな画面になります。
またAndroid機の方では、こんな警告が出てきます。
Android機の警告を許可すると、PC側のChromeで、デバイスがリストされます。
※デバイスが出て来ない場合は、PCのChromeをリロードしてみてください。
※どうやってもデバイスが出て来ない場合は、ADBドライバが入っていないことが考えられます。
※その場合は、メーカーのドライバ配布ページからADBドライバを貰ってきてインストールしてください。
この中の「inspect」リンクをクリックすると...
はい!まいどお馴染みの、Chrome開発者ツールが出てきます!
FireTabletでは、AmazonアプリストアにChromeが出てきません。
仕方がないので、こちらから最新版のapkを貰ってきました。
https://androidapksfree.com/chrome/com-android-chrome/old/
本体に転送し、インストール。
FireTabletをPCに接続し、Chromeを起動してみると。
ばっちし、リモートデバッガが動きました。
XperiaVLはAndroid4.1と古いため、GooglePlayストアにChromeが出てきません。
やはり同じところから、今度は古めのChrome ver61.0のapkを貰ってきます。
またこちらはADBドライバが無かったため、PC側に発売当時のツールをインストール。
すると、Android4.1機でもデバッガを認識してくれました。
音楽サーバ"Mopidy"のフロントエンドを作る:12 Bootstrapテーマ導入とCSS軽量化
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第12回です。
今回は、Bootstrapテーマ導入とCSS最適化の流れを追って行きます。
デザインセンスはひとにお任せ
まず。
私のスキルセットは若干フロントエンド寄りとはいえ、デザイナではありません。
自分のデザインセンスには、一切の信用を置いていません。
職業デザイナさんと仕事をするたびに、毎回感心させられます。
あの人たち、どうやってかっこよさと機能を両立させてるんでしょうね?
こういう自分に無いものは、他人様に求めるのが一番です。
そしてBootstrapには、そんなデザイナさんたちの能力の粋が込められたテーマが、ときには無償で配布されていたりします。
ありがたや、ありがたや!
テーマをえらぶ
いろいろとテーマをぐぐった中で、こちらのページが目に付きました。
hackerthemes.com
殆どがMITライセンスで公開されていて、サンプルも見やすくていいですね!
色々検討したのですが、ユーザー層を考えてみると。
Mopidyを使うのは恐らく、若い男性でラズパイとかが好きな技術系のひとたち。
なので、黒ベースで明暗がクッキリしたクールなイメージの、Neon Glow
を導入することにしました。
読み込んでみる
こればっかりはnpmという訳にもいかないので、普通にダウンロードします。
貰ってきたzipを解凍してcssファイルを参照させます。
こちらがNeon Glow
適用直前コミットのUI。
いわゆる「Bootstrap臭い」雰囲気です。
そして、こちらがNeon Glow
適用後コミットのものです。
オレンジがいい感じなので、ボタンもタイトルも全部bg-warning
化しました。
おお。
印象変わるわ~...。
うーん、やっぱ、デザイナってすげえな。
描画が、重い
見栄えが一気に音楽プレイヤーらしくなった、のは、大変いいのですが。
元々若干モッサリしていた描画が、なんとなくさらに、重くなった気がします。
読み込んだCSSのサイズを見てみると。
- AdminLTE: 345KB(16,763行)
- Neon-Glowテーマ: 258KB(9,232行)
なるほど。
こりゃレンダリングに負荷が掛かっててもおかしくないですね...。
使っていない定義をピックアップ
こちらの記事にあるChromeのCoverageツールで、CSS上の未使用ルールがどのくらいあるのか、見てみました。
qiita.com
Chromeの開発者ツールから、左のメニュー→「More tools」→「Coverage」でカバレッジツールが出てきます。
ツールを出してからリロードし、UIを一通り触っていくと、使っているルールが緑色に、使っていないルールは赤にハイライトされます。
どれどれ...?
うへえ。
ほとんど使われていません。
そして、AdminLTEもNeon-Glowテーマも、Bootstrap4-CSSの修正版なので。
定義の重複が山のようにあります。
淡々と、手作業で
使ってないルールを一発で削除するような、便利なツールがあると良いのですが。
今のところ、そういうツールは、私は知りません。
仕方がないので、Chromeのツール画面とVisual Studioを行き来しながら、CSSのルールを一つずつ削除していきました。
無心に小一時間ほど、淡々と作業した結果。
- AdminLTE: 13KB(566行)
- Neon-Glowテーマ: 37KB(1,662行)
ロード早い!アニメーション軽い!
動画を撮るツールが手元に無いため、違いをお見せすることが出来ないのですが。
ブラウザロード時の初回描画も、アニメーションも、見るからに早くなりました!
「動いてるから、いいじゃない」と、ついサボりたくなるCSSのメンテですが。
体感には、明らかな差が出来るもんですね。
以上、デザイン適用とCSS軽量化のおはなし、でした!
音楽サーバ"Mopidy"のフロントエンドを作る:11 スワイプ操作を導入する
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第11回です。
今回は、スワイプ操作を検知して画面を移動する実装を追います。
使えることは使える、けども...
前回、デバイスごとに見栄えを調整しました。
そしてスマートフォン表示時は、ヘッダバーにボタンを置いて、リストの切替が出来るようにしてあります。
しかし、実際に操作してみると。
UIの切替が、思ったより不便でした...。
片手サイズのデバイスでヘッダをタッチすると、手でデバイス全体が隠れちゃうんです。
あんまり好きじゃないなー、ということで。
スワイプ操作によるUI切替を、導入することにしました。
Hammer.JSはスグレモノ
Hammer.jsは、ブラウザのタッチ操作をひと纏めに検出出来る、便利なライブラリです。
以前の案件で使っていたこともあり、特に迷いも無くこれを導入しました。
なんとなく、iOSのジェスチャー検知を思わせる作りです。
Vue用にラップされたライブラリ、vue2-hammerやvue-touchもあるんですが。
特に、ラップしてもらう必要も感じませんで...。
そのまんま使っています。
インストールはいつものnpm経由で。
# npm install hammerjs
@typesに型定義がありますので、こちらも入れておきます。
# npm install -D @types/hammerjs
スワイプ検知イベントを書く
Hammer.jsでは、スワイプの検出を下のように書きます。
import * as Hammer from 'hammerjs'; const elem = document.querySelector('.detector-element'); const detector = new Hammer(elem); detector.get('swipte').set({ direction: Hammer.DIRECTION_HORIZONTAL // 水平方向のスワイプを検出する。 }); detector.on('swipeleft', () => { // 左スワイプ検知時の処理 });
これで、タッチパネルはおろか、マウス操作でも反応してくれる、スグレモノです。
Android-Chromeで反応しない...?
さてさて、Viewの各パーツにHammer.jsを導入して、動作を確認してみたところ。
Android機のChromeで、反応がありません。
ぐぐってみると、not works
のワードがずらり。
どうも、頻出する現象のようですね。
ご本家GitHubのissueで、解決策が議論されていました。
こちらのコメントで、コード例があります。
var hammertime = new Hammer.Manager(document.querySelector('.l-main'), { touchAction: 'auto', inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput, recognizers: [ [Hammer.Swipe, { direction: Hammer.DIRECTION_HORIZONTAL }] ] });
ほうほう。イベントの入力方式を、対応状況に応じて切り替えてるのかな?
これをこのまま書いてみると、@types上の変数定義が無いため、ビルドが通りません。
この変数、ライブラリの実装にはあるんかいな?と調べてみると。
このへんに記述がありました。
var SUPPORT_TOUCH = 'ontouchstart' in window; var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);
こりゃ単に、@types上の定義が不足してるだけ、ですね。
例によって型定義ファイルをtypes
フォルダにコピーし、変数定義を追記します。
types/hammerjs/index.d.ts
interface HammerStatic { // -- 追記部分のみ抜粋 -- SUPPORT_TOUCH: boolean; SUPPORT_POINTER_EVENTS: boolean; SUPPORT_ONLY_TOUCH: boolean; ...
Android-Chromeで試すと、きちんと検知出来るようになっていました。
うむよしよし!、と思ってたら...。
この方法だと、PCのマウス操作に反応しなくなっちゃいまいした...。
うーん、PCブラウザのマウス操作なんで、別にオミットしちゃってもいいんですが。
気になるっちゃ、気になる。
悩んでいたら、例のissueの少し下の方で、CSSによる対応サンプルがありました。
touch-action: pan-y;
なるほど、スクロールの上下操作のみを許可するよ、と定義しちゃうわけですね。
試してみると、Hammer.js側の方はサンプル通りの記述のまま、Android-Chromeで動くようになりました!
教訓。
英語でも、きちんと全部読め、ということですね...。
アニメーションを導入する
検知イベントを取れるようになれば、あとはUIの切替機能を加えるだけです。
しかし、元々はタブパネルのように切り替えを実装していたもの。
左右にスワイプしたのに、UIが横移動せずにガチャン、と切り替わると妙な感じです。
ここは、横移動アニメーションを追加したいところ。
そこで、頼りになるCSSアニメーションライブラリAnimate.cssにご活躍いただきます。
なお、Animate.cssもnpmパッケージ化されており、インストールは至極簡単です。
# npm install animate.css
使い方も、至極簡単。
DOM要素のクラスに、animated
と、各アニメーションの種類を示すクラスを追加するだけです。
import 'animate.css/animate.css'; const elem = document.querySelector('.anim-elem'); elem.classList.add('animated', 'fadeOut');
たったこれだけで、CSSによるアニメーションが動いてくれます。
なんとまあ、簡単だこと!
これをなるべく多用できるように、こんなユーティリティクラスを用意していました。
src/ts/Utils/Animate.ts:
export default class Animate { // -- 一部抜粋 -- private _isHidingAnimation: boolean = false; private _resolver: (value: boolean) => void = null; private _elem: HTMLElement = null; private _classes: DOMTokenList = null; public constructor(elem: HTMLElement) { this._elem = elem; this._classes = this._elem.classList; this.OnAnimationEnd = this.OnAnimationEnd.bind(this); } public Execute(animation: Animation, speed: Speed = Speed.Normal): Promise<boolean> { return new Promise((resolve: (value: boolean) => void): void => { this._resolver = resolve; this._elem.addEventListener(Animate.AnimationEndEvent, this.OnAnimationEnd); // 同じ内容のアニメーションが既に設定済みか否か const needsDefer = ( this._classes.contains(Animate.ClassAnimated) && this._classes.contains(animation.toString()) ); Animate.ClearAnimation(this._elem); (needsDefer) //既にアニメーションセット済みのとき: 一度クリアしたあとで遅延実行 ? _.defer((): void => { this.InnerExecute(animation, speed) }) // プレーン状態のとき: 即時アニメーション実行 : this.InnerExecute(animation, speed); }); } private InnerExecute(animation: Animation, speed: Speed = Speed.Normal): void { this._isHidingAnimation = Animate.IsHideAnimation(animation); if (!this.GetIsVisible()) this.ShowNow(); this._classes.add(Animate.ClassAnimated); this._classes.add(animation.toString()); if (speed !== Speed.Normal) this._classes.add(speed.toString()); // animationendイベントタイムアウト: 100ms加算。 let endTime = -1; switch (speed) { case Speed.Slower: endTime = 3100; break; case Speed.Slow: endTime = 2100; break; case Speed.Normal: endTime = 1100; break; case Speed.Fast: endTime = 900; break; case Speed.Faster: endTime = 600; break; } setTimeout((): void => { if (this._resolver) this.Resolve(false); }, endTime); } // -- 以降、終了処理など -- }
アニメーションの終了をawaitで待てるようにしてあります。
備えは万全!
横移動アニメーションをいれる...と?
プロジェクトも終盤になると、実装が整理されて抽象クラスが増えていきますね。
Mopidy.Finderでは、カラム表示/フルスクリーン表示を切り替えるコンテンツ部分を、ContentDetailBase
というクラスに機能集約していました。
そこに、左右にスライド移動するアニメーションメソッドを加えます。
src/ts/Views/Bases/ContentDetailBase.ts:
private animate: Animate; // -- 中略 -- public async SlideInRight(): Promise<boolean> { this.ToVisible(); await this.animate.Execute(this.AnimationSlideInRight, this.AnimationSpeed); this.animate.Clear(); this.ToVisible(); return true; } public async SlideInLeft(): Promise<boolean> { this.ToVisible(); await this.animate.Execute(this.AnimationSlideInLeft, this.AnimationSpeed); this.animate.Clear(); this.ToVisible(); return true; } public async SlideOutRight(): Promise<boolean> { this.ToVisible(); await this.animate.Execute(this.AnimationSlideOutRight, this.AnimationSpeed); this.animate.Clear(); this.ToHide(); return true; } public async SlideOutLeft(): Promise<boolean> { this.ToVisible(); await this.animate.Execute(this.AnimationSlideOutLeft, this.AnimationSpeed); this.animate.Clear(); this.ToHide(); return true; }
さて、スワイプ操作時に移動してみましょう!
あれえ?
スライドアウトは動くんですが、移動先のUIがスライドインしてきません...。
UIのレイアウト修正
なぜこうなるか、というと。
Mopidy.Finderの画面構造では、Bootstrapのrow
に複数のcol-lg
を入れることで、各種モニタサイズに対応させていました。
スマートフォンサイズのディスプレイでは、下のような表示になっています。
Freepik from www.flaticon.com is licensed by CC 3.0 BY
この図の「コンテンツ1」がスワイプ操作を受けて左にスライドアウトしているとき、「コンテンツ2」は画面に見えない下の方で、スライドインアニメーションしていた、というわけなんですね...。
なるほどこりゃイカン、ということで、スマートフォン表示時はposition: absolute
になるように、CSSを調整します。
src/css/site.css:
.position-static { position: static !important; } .position-absolute { position: absolute !important; } .position-relative { position: relative !important; }
そしてこのCSSクラスを、モニタサイズに合わせてセットするメソッドを作ります。 src/ts/Views/Bases/ContentDetailBase.ts:
private static readonly PositionStatic: string = 'position-static'; private static readonly PositionAbsolute: string = 'position-absolute'; // -- 中略 -- public ToPositionStatic(): void { const classes = this.$el.classList; if (!classes.contains(ContentDetailBase.PositionStatic)) classes.add(ContentDetailBase.PositionStatic); if (classes.contains(ContentDetailBase.PositionAbsolute)) classes.remove(ContentDetailBase.PositionAbsolute); } public ToPositionAbsolute(): void { const classes = this.$el.classList; if (!classes.contains(ContentDetailBase.PositionAbsolute)) classes.add(ContentDetailBase.PositionAbsolute); if (classes.contains(ContentDetailBase.PositionStatic)) classes.remove(ContentDetailBase.PositionStatic); }
それらをモニタサイズ検知時に実行するように組み込み。
いざ動作確認!
ああ、よかった!
カレントUIがスライドアウトするのと一緒に、別のUIがスライドインしてきてます。
以上、スワイプ操作の導入のおはなし、でした!
音楽サーバ"Mopidy"のフロントエンドを作る:10 デバイス別の表示調整
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第10回です。
今回はBootstrap4をベースにした、デバイスごとに表示を最適化するフローを追います。
どんなふうにする?
まず、PCブラウザサイズ。
私が最も使うはずのサイズです。
なるべく全機能を表示しておき、マウスで即座に各機能にアクセスしたいです。
Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
次に、タブレットサイズ。
それなりのモニタサイズがありますが、常時メニューを表示しておくには小さすぎます。
サイドバーは、ハンバーガーメニューの開閉式にしておきましょう。
そして、タブレットを横にしたときは、カラムが並ぶように。
縦にしたときは、各カラムがフルスクリーン表示されるようにしたいです。
Icons made by Dave Gandy www.flaticon.com is licensed by CC 3.0 BY
そして、スマートフォンサイズ。
モニタサイズは小さいため、縦横ともにフルスクリーンで表示しておきたいです。
Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
モニタサイズを判定する
Bootstrapではマークアップ時に`lg'や'md'のようなサイズ閾値を織り交ぜることで、自動的にカラム表示/フルスクリーン表示を切り替えて貰えますよね。
しかし、「今現在、どのサイズ閾値を使っているのか」を取得する方法が、プレーンなBootstrapの機能の中では見つけることが出来ませんでした。
そこで、前回記事に挙げたResponsive Bootstrap Toolkit
を導入して、モニタサイズがBootstrap上のどれに当たるのか、を取得するようにしました。
しかし前回記事で少し触れたように、Responsive Bootstrap Toolkit
はまだBootstrap4には対応していません。
ぐぐってみると、ご本家GitHubのissueで対応方法が議論されていました。
github.com
ほうほう、useメソッドで閾値定義を渡せばよい、と。
ということで、このissueに記載があった定義を、そのままコピペで持ってきました。
src/ts/Libraries.ts:
Libraries.ResponsiveBootstrapToolkit.use('bs4', { 'xs': Libraries.$('<div class="d-xs-block d-sm-none d-md-none d-lg-none d-xl-none"></div>'), 'sm': Libraries.$('<div class="d-none d-sm-block d-md-none d-lg-none d-xl-none"></div>'), 'md': Libraries.$('<div class="d-none d-md-block d-sm-none d-lg-none d-xl-none"></div>'), 'lg': Libraries.$('<div class="d-none d-lg-block d-sm-none d-md-none d-xl-none"></div>'), 'xl': Libraries.$('<div class="d-none d-xl-block d-sm-none d-md-none d-lg-none"></div>') });
カラム/フルスクリーン切替の閾値をきめる
Bootstrap4のサイズは、公式ドキュメントでは下記のとおりです。
xs
: 幅576px未満sm
: 幅576px以上 - 768px未満md
: 幅768px以上 - 992px未満lg
: 幅992px以上 - 1200px未満xl
: 幅1200px以上
当初はmd
サイズを基準に切り替えていたのですが、これではタブレットを水平にしてもカラム表示になりませんでした。
htmlのmetaタグでviewportを指定しているため、デバイス本来の解像度よりも小さめに判定されるためです。
<meta name="viewport" content="width=device-width,initial-scale=1">
ということで、切替閾値をlg
に変更しました。
閾値に合わせた表示切替ロジック
Mopidy.Finderでは、ディスプレイサイズに合わせた表示を、NavigationController
で制御するようにしました。
コンストラクタから呼び出すInitialNavigationメソッドの中で、表示制御メソッドを実行しています。
src/ts/Controllers/NavigationController.ts: L63
private async InitialNavigation(): Promise<boolean> { await this._store.TryConnect(); const updateProgress = await this._store.GetDbUpdateProgress(); // どうも、ResponsiveToolkitの初期化後から反応が正しくなるまで // すこし時間がかかるっぽい。 // Settingsクエリが終わるまで待ってからAdjustする。 this.AdjustScreen(); // -- 中略 -- }
Responsive Bootstrap Toolkit
は、各閾値ごとに定義した要素の表示状態を見ることで現在のサイズを取得しているようです。
なので、初回描画が終わるまで、API呼び出しを挟むことで少し待たせています。
そして、表示制御メソッドの中身はこんなかんじ。 src/ts/Controllers/NavigationController.ts: L95
private AdjustScreen(): void { // コンテンツは、mdサイズを基点にカラム<-->フルスクリーンを切り替える。 if (this._viewport.is('<=md')) { this._content.ContentToFullscreen(); } else { this._content.ContentToColumn(); } // サイドバーは、lgサイズを基点に常時表示<-->操作終了で非表示化を切り替える。 if (this._viewport.is('<=lg')) { this._headerBar.SetSideBarClose(); } else { this._headerBar.SetSideBarOpen(); } // -- 中略 -- }
this._viewport
は、Responsive Bootstrap Toolkit
のインスタンスをセットしています。
isメソッドで比較演算子が使えるのは、便利ですね!
やれやれ一安心...と思いきや
閾値をlg
にセットして、手持ちのiPad mini4 と iPhone6+と で表示を確認しました。
まず、iPad mini4
よしよし、と。
次に、iPhone6+。
ボケボケでなんですが、これも表示は想定どおりです。
最後に、いま私が普段使いしている、MediaPad M5で試します。
あかーん!
横にしたとき、カラム表示になっていません...。
MediaPadのディスプレイサイズをみてみる
MediaPad M5のChromeにデバッガツールをアタッチしてみると。
スクリーンサイズの幅は、「963.137px」と出ています。
なるほど、lg
サイズの992pxより、小さいわけですね...。
ここで少し悩みました。
ある程度は汎用的に環境を作り込んでくれているであろうBootstrap様のサイズ規定に、はたして手を入れるべきか?
しかし。
このタブレットは、決して珍しい種類のものではありません。
他のAndroidタブレットでも、同じ現象が出るかもしれません。
なにより。
このアプリは、主に私が使うもの。
オレオレ環境に合わせて、何が悪い!
と、3秒くらい葛藤した上で、BootstrapのCSSに手を入れることに、決定しました。
閾値サイズに手を入れる
BootstrapのCSSは、それはそれは巨大な記述の塊です。
しかし、サイズを規定している箇所の記述は、だいたいこんな感じです。
@media (min-width: 768px) { ...
この、@media
で始まっているもののうち、lg
サイズに該当する 992px の値を変えてしまえば、終わりです。
992pxを示す@media (min-width: 992px)
が、11箇所。
そして992px未満を示す@media (max-width: 991.98px)
の記述が、2箇所ありました。
これをそれぞれ、下のように書き換えました。
@media (min-width: 992px)
→@media (min-width: 930px)
@media (max-width: 991.98px)
→@media (max-width: 929.98px)
すこしマージンを取り、閾値を930pxとしました。
そして、MediaPad M5で表示を見てみます。
おっしゃ!
狙いどおりです。
iPad mini4でも、表示は変わっていませんでした。
まあ、当然ちゃ当然です。
その他、iOS機と各種Android機で表示を試し、狙い通りかを確認します。
うんうん、ちゃんと見えてますね。
以上、デバイスに合わせた表示調整のおはなし、でした!
追記
ちょうどこの記事をかいた8月11日、こんなエントリがはてブに挙がってました。
hashimotosan.hatenablog.jp
なるほど、560px/960pxの二つ、か。
これはシンプルでいいですね。
音楽サーバ"Mopidy"のフロントエンドを作る:09 TypeScript用の型定義をつくる、その2
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第9回です。
今回も引き続き、TypeScriptでライブラリを使う際の型定義について、です。
@typesにない、だと...
Mopidy.FinderではUIの画面サイズの判定に、BootstrapのためのユーティリティResponsive Bootstrap Toolkit
を使っています。
それを@typeでsearchしてみると...
うへえ。見つかんねえ...
大正義@typesとはいえ、型定義が存在しないライブラリも、あるんですね。
ないなら書けばいいじゃない
そう、定義が無いなら、書けばいいのです。
ゴハンが無いならケーキを食えばいいのです。
めんどくせえよアントワネット!
あら、意外とそうでもありませんですのよ。オホホ。
使うとこだけ、書けばいい
この、Responsive Bootstrap Toolkit
。
欲しい機能は、
- isメソッド: Windowサイズ比較用メソッド
- changedコールバック: Windowサイズ変更時のコールバック
- currentメソッド: 現在のWindowサイズを取得するメソッド
くらいなもんです。
加えて、このライブラリはまだBootstrap4に対応していないため、
- useメソッド: Windowサイズ定義をセットするメソッド
も入れておきます。
別にDefinitelyTyped
リポジトリにプルリクする訳でもなく、自分が使うだけのこと。
それなら、使うところだけを定義しておけばいいのです!
定義の種類
まず定義するに当たって、読み込むライブラリが何者なのかを調べます。
型定義にはおおむね、下の4種類が多く見受けられます。
- newして使う、
クラス
の定義 - 既に生成済みの
インスタンス
の定義 - 各種クラスやインタフェース類、staticメソッドを集めた、
名前空間
としての定義 - staticなメソッドを持ち、かつnewでインスタンスを生成する
クラス
でもある定義。
このうち1.のクラス定義は、クラスベースのTypeScriptでは最も馴染みやすいものです。
declare class [クラス名]
としておき、中身はインタフェースを書く要領でpublicメンバーを書き連ねていきます。
4.の複合型定義は、見かける頻度は高いんですが、書くのはしんどいです。
クラスと名前空間を同じ名前で定義し、staticメソッドやインタフェース定義などを名前空間に集めておく書き方です。
前回記事に挙げたSortableJSの定義が、このように作られています。
そして、冒頭で挙げたResponsive Bootstrap Toolkit
は、どんなものでしょう?
ライブラリの使い方としては、下記のように書いてます。
import * as ResponsiveBootstrapToolkit from 'responsive-toolkit/dist/bootstrap-toolkit'; if (ResponsiveBootstrapToolkit.is('<=md')) { // md以下のときの処理 }
これは、2.のインスタンス
をimportしている、と考えてよいでしょう。
インスタンスの型定義
型定義ファイルで書くdeclare
構文。
これは、そのライブラリをimportしたときに存在するはずのもの、を示しています。
クラスが存在するはずならば、declare class [クラス名]
となります。
Responsive Bootstrap Toolkit
の場合はクラスでなくインスタンスになるため、存在するのは生成済みインスタンスの入れ物になる、変数名です。
この場合は、const/varで宣言された変数をdeclare
します。
declare const ResponsiveBootstrapToolkit;
そして、そのインスタンスが持つべきメンバーを、インタフェースとして定義します。
interface IResponsiveBootstrapToolkit { is(complareString: string): boolean; changed(callback: () => void, waitMsec?: number): void; current(): string; use(framewrokName: string, breadpoints: { [breadkpoint: string]: JQuery }): void; }
declareしたインスタンスは上述のインタフェースを持つため、型を指定してやります。
declare const ResponsiveBootstrapToolkit: IResponsiveBootstrapToolkit;
最後に、このライブラリはモジュールのため、存在宣言した変数をexportしてやります。
export = ResponsiveBootstrapToolkit;
全部合わせて、8行のコードで済みました!
types/responsive-toolkit/index.d.ts:
interface IResponsiveBootstrapToolkit { is(complareString: string): boolean; changed(callback: () => void, waitMsec?: number): void; current(): string; use(framewrokName: string, breadpoints: { [breadkpoint: string]: JQuery }): void; } declare const ResponsiveBootstrapToolkit: IResponsiveBootstrapToolkit; export = ResponsiveBootstrapToolkit;
これで、Responsive Bootstrap Toolkit
が、型安全に使えるようになります!
名前空間、と解釈してみる
このResponsive Bootstrap Toolkit
、生成済みインスタンス
として扱っていますが。
これは、staticなメソッドを持つ名前空間
、として見ることも出来ます。
そのように、書き換えてみたのがこちら。
declare namespace ResponsiveBootstrapToolkit { function is(complareString: string): boolean; function changed(callback: () => void, waitMsec?: number): void; function current(): string; function use(framewrokName: string, breadpoints: { [breadkpoint: string]: JQuery }): void; } export = ResponsiveBootstrapToolkit;
これでも、ビルドは通ります。
ただ、名前空間として考えるには、少し機能が小さすぎますかね?
そのへんは、お好みで。
以上、TypeScriptの型定義づくり、その2でした!
音楽サーバ"Mopidy"のフロントエンドを作る:08 TypeScript用の型定義をつくる
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第8回です。
今回は、TypeScriptでライブラリを使う際の、型定義について、です。
大正義@types
前回記事でちらっと触れましたが、最近はライブラリ配布時にTypeScript用の型定義も、同梱していることが散見されるようになりました。
しかし、たとえ同梱の定義が無いとしても。
有名なライブラリを網羅する、DefinitelyTypedというリポジトリ があります。
そして、それをnpmパッケージとして取り込んだものが、@typesです。
もはや、自分で型定義を書くことなんて無いのです!
...そう。
無いのです。定義済みの型定義で用事が済むのであれば...。
プロパティが足りない!
Mopidy.Finderでは、プレイリスト編集機能のためにSortableJSを導入しました。
ドラッグ&ドロップで要素を並べ替えできる、とても便利なライブラリです。
ここで、その@types上の定義を見てみましょう。
npmパッケージ名は、@types/sortablejs
です。
インストールして、中身を除いてみます。
# npm install -D @types/sortablejs
設定値をセットするための、Options定義がこちら。
node_modules/@types/sortablejs/index.d.ts:
declare namespace Sortable { export interface Options { /** * ms, animation speed moving items when sorting, `0` — without animation */ animation?: number; /** * Class name for the chosen item */ chosenClass?: string; dataIdAttr?: string; /** * time in milliseconds to define when the sorting should start */ delay?: number; /** * Disables the sortable if set to true. */ disabled?: boolean; /** * Class name for the dragging item */ dragClass?: string; /** * Specifies which items inside the element should be draggable */ draggable?: string; dragoverBubble?: boolean; dropBubble?: boolean; /** * Class name for the cloned DOM Element when using forceFallback */ fallbackClass?: string; /** * Appends the cloned DOM Element into the Document's Body */ fallbackOnBody?: boolean; /** * Specify in pixels how far the mouse should move before it's considered as a drag. */ fallbackTolerance?: number; fallbackOffset?: { x: number, y: number }; /** * Selectors that do not lead to dragging (String or Function) */ filter?: string | ((this: Sortable, event: Event | TouchEvent, target: HTMLElement, sortable: Sortable) => boolean); /** * ignore the HTML5 DnD behaviour and force the fallback to kick in */ forceFallback?: boolean; /** * Class name for the drop placeholder */ ghostClass?: string; /** * To drag elements from one list into another, both lists must have the same group value. * You can also define whether lists can give away, give and keep a copy (clone), and receive elements. */ group?: string | GroupOptions; /** * Drag handle selector within list items */ handle?: string; ignore?: string; /** * Call `event.preventDefault()` when triggered `filter` */ preventOnFilter?: boolean; scroll?: boolean; /** * if you have custom scrollbar scrollFn may be used for autoscrolling */ scrollFn?: ((this: Sortable, offsetX: number, offsetY: number, event: MouseEvent) => void); /** * px, how near the mouse must be to an edge to start scrolling. */ scrollSensitivity?: number; /** * px */ scrollSpeed?: number; /** * sorting inside list */ sort?: boolean; store?: { get: (sortable: Sortable) => string[]; set: (sortable: Sortable) => void; }; setData?: (dataTransfer: DataTransfer, draggedElement: HTMLElement) => void; /** * Element dragging started */ onStart?: (event: SortableEvent) => void; /** * Element dragging ended */ onEnd?: (event: SortableEvent) => void; /** * Element is dropped into the list from another list */ onAdd?: (event: SortableEvent) => void; /** * Created a clone of an element */ onClone?: (event: SortableEvent) => void; /** * Element is chosen */ onChoose?: (event: SortableEvent) => void; /** * Element is unchosen */ onUnchoose?: (event: SortableEvent) => void; /** * Changed sorting within list */ onUpdate?: (event: SortableEvent) => void; /** * Called by any change to the list (add / update / remove) */ onSort?: (event: SortableEvent) => void; /** * Element is removed from the list into another list */ onRemove?: (event: SortableEvent) => void; /** * Attempt to drag a filtered element */ onFilter?: (event: SortableEvent) => void; /** * Event when you move an item in the list or between lists */ onMove?: (event: MoveEvent) => boolean; } }
プレイリストの編集時は、複数のトラックを選択して並べ替えをしたい。
なので、MultiDragプラグインを使いたい。
こちらの公式サンプルコードでは、このように書かれています。
new Sortable(multiDragDemo, { multiDrag: true, // Enable multi-drag selectedClass: 'selected', // The class applied to the selected items animation: 150 });
しかし、上述のオプション定義には、multiDrag
項目が見当たらないのです...。
このままTypeScriptでサンプルのように書いてしまうと、ビルドエラーになります。
こんな場合は、慌てず騒がず。
型定義ファイルに、手を入れればいいのです!
型定義ファイルを配置する
node_modules下のファイルに直接手を入れるのは、ご法度です。
他の環境ではビルド出来なくなってしまいますね。
ではどうするか、というと。
プロジェクトに型定義ファイル用のフォルダを作り、型定義ファイルをコピーします。
Mopidy.Finderのプロジェクトでは、型定義配置用のフォルダtypes
を作り、そこにライブラリ別に配置しています。
SortableJSの型定義は、types/sortable/index.d.ts
に配置しました。
そして、元になった@typesのパッケージをアンインストールします。
# npm uninstall -D @types/sortablejs
その上で、TypeScriptに、コピーした型定義ファイルを参照させるように設定します。
tsconfig.json
のcompilerOptions
の中で、baseUrl
とpaths
を書きます。
"compilerOptions": { // -- 中略 -- "baseUrl": "./", "paths": { "sortablejs/modular/sortable.complete.esm": [ "types/sortable" ] } }
paths
を使用する際は、そのパスの基準となるbaseUrl
が必要になるようです。
ここでは、baseUrl
はプロジェクトルートを示す./
(=tsconfig.jsonがあるフォルダ)としています。
実際にSortableJSを使うTypeScriptモジュールでは、下記のようにimportして使います。
import Sortable from 'sortablejs/modular/sortable.complete.esm'; const elem = document.querySelector('.sortableWrapper'); Sortable.create(elem , { // 各種オプション定義 });
importするモジュールを、単にsortablejs
でなくsortablejs/modular/sortable.complete.esm
としてあるのは、SortableJSの全プラグインが同梱されたsortable.complete.esm.js
を使うためです。
公式ページのこちらに記載がありますね。
※Visual Studioでは、型定義の参照先を変えた際、認識されない場合があります。※
※そのときは、Visual Studioを再起動してみてください。※
型定義ファイルに手を入れる
さて、型定義を好き放題にいじるための準備が整いました!
そこで、公式ドキュメントに書いてある、MultiDragプラグインの定義を読んでみます。
まず、Option。
- multiDrag
- selectedClass
- multiDragKey
- onSelect
- onDeselect
の5つの定義が増えたようです。
multiDrag
は、複数選択可否の設定です。
これをtrue
とするとで、複数選択可能な状態にできる、と。
selectedClass
は、選択したDom要素に付与するクラス名ですね。
選択状態にした際にCSSによって背景色を変えたいので、これは使えそうです。
multiDragKey
は、複数選択時にキーボードで押すキーのようです。
常時複数選択にするつもりなので、これは使いませんでした。
また、onSelect
とonDeselect
のイベントハンドル2つ。
これは選択時と選択解除時に発火するイベントのようですね。
それぞれの時点でDomを操作するときに使えるかも。
でも、クラス名を自動で付与してくれるなら、使わないかな?
というわけで、Optionの定義に、multiDrag
とselectedClass
を追加しました。
declare namespace Sortable { export interface Options { // -- 中略 -- multiDrag?: boolean; selectedClass?: string; } }
そして、イベントオブジェクトのほうにも、値が増えています。
- items
- clones
- oldIndicies
- newIndicies
items
はDom要素の配列が入るようですね。
clone
は...なんだろう?分からないまま、使わずじまいでした。
そして、oldIndicies
とnewIndicies
。これは、Dom要素ごとの順序インデックスを、新旧で持っているようです。
イベント定義記述のすぐ下に、Indiciesの定義が書いてありますね。
それらを、Eventの定義に追記していきます。
declare namespace Sortable { export interface SortableEvent extends Event { // -- 中略 -- items: HTMLElement[]; newIndicies: Index[] | undefined; oldIndicies: Index[] | undefined; } export interface Index { index: number; multiDragElement: HTMLElement } }
なお、ドキュメント上のIndex
の定義ではHTMLElementはelement
、と書いてます。
しかし実際にイベントをダンプしてみると、名前はmultiDragElement
のようです。
こういうのは、実装に合わせて修正していきます。
修正後の型定義でビルドしてみる
型定義の修正が終わったら、実際にコードを書いて確かめます。
SortableJSの生成箇所は、こんなコードになっています。
src/ts/Views/Playlists/Lists/Tracks/TrackList.ts:
private async SetSortable(): Promise<boolean> { this.DisposeSortable(); await Delay.Wait(10); this.sortable = Sortable.create(this.TrackListUl, { animation: 500, multiDrag: true, selectedClass: 'selected', dataIdAttr: 'data-uri', onEnd: (): void => { this.OnOrderChanged(); } }); return true; }
TypeScriptをビルドしてみると...
はい、きちんと通ります!
プレイリスト画面で、実際に並べ替えを試してみます。
複数を選択して...
ドラッグすると...
順番が、変わりました!
以上、TypeScript型定義のメンテナンスのおはなし、でした!