HTMLフォームでエンターキー押して入力要素のフォーカスを移動させたい、いや、しろよ!

2020年4月8日 間違い訂正を追記


HTMLでフォームをチマチマ組んでテストしていると、テキストボックス(input[type=text]要素)とかでついついエンターキー(改行キー)を押して、ムカッ!!!!!!とかしません? するよね、絶対するよね? 僕は自己嫌悪に陥って仕事を中断して帰りたくなる衝動に駆られます!

そこでググってjQueryのプラグイン探すなり、テック系の記事をコピペしたりしますよね。でも僕は探すより、自分で書く方が早いんですよ、結果的に。適当なjQueryプラグインなりを探してもそれの使い方を調べてたり、要らない機能があったりして、結局ドキュメントをよく読まないとわからないことが多い。

要は FORM要素内のINPUT要素に onkeyup  onkeydownイベントハンドラを仕込んでエンターキーをトラップすれば済む話。
たかだか数十行のスクリプトです。え?自分で組むとバグが!!!って? バグが出たらその都度直せばいいんです。ラクしちゃいけません。ラクするのはチラシの印刷だけでよろしい。

僕は jQuery 好き好き人間なので、適当にプラグインを書きました。

で、これを以下のようなフォームで使うと、このままでは失敗して、エンターキー押すとフォームが送信されてしまいます。

失敗の原因は、FORM要素内に input[type=submit]要素が存在するのが原因。
chromiumしか確認していないけど、どうやら input[type=submit]要素が存在すると、エンターキー押下で問答無用でFORMのアクションが走るようです。このinput[type=submit]要素を消すと送信されない、という挙動になってます。違ったらごめん。

追記:2020年4月8日
※ FORM要素内にinput[type=”text”]要素が一つしかない場合は、やっぱり問答無用でFORMのアクションが走るみたいです。嘘情報申し訳ない。上記はinput[type=”text”]が二つ以上ある場合のようです。一つしかないFORMの場合はダミーのinput[type=”text”]をどこかに配置して display: none; とかしとけばいいみたい。よく分からん仕様だなぁ。。。

じゃぁ、FORM要素にinput[type=button]要素なりa要素なりを書いて、そのonclickイベントハンドラ内で FORM要素のsubmitメソッドをコールすればいいんじゃない?ということになるんですが、残念ながらそうは問屋は降ろさない。
input[type=submit]要素がないと、FORM要素のvalidityチェックが走らないんだな、これが。せっかくinput要素に requireだのpatternだのを書いても無視されてしまう。

解決方法は2つ。

(1)
input[type=button]要素のonclickハンドラ内で、FORM要素のsubmitメソッドをコールする前に、reportValidityというメソッドをコールしてその返り値を判断して submitメソッドをコールするかどうかを決定する。

    <script type="text/javascript"><!--
      (function($) {
        $('form').enterNext();
        $('form [type=button]').click(function(ev) {
          if(this.form.reportValidity())
            this.form.submit();
        });

      })(jQuery);

    //--></script>

(2)
input[type=submit]要素を隠すか非表示(display: none;とか)にしておき、input[type=button]要素のonclickハンドラとFORM要素のonsubmitイベントハンドラを仕込んでおいてエンターキー押下かではなくクリックされたかをチェックさせる。

    <form>
    ....
    <input type="submit" style="display:none;" />
    <input type="button" value="送信" />
    </form>
    <script type="text/javascript"><!--
      (function($) {
        $('form').enterNext();

        $('form').submit(function(ev) {
          if(!'isClick' in this || this.isClick !== true)
          {
            ev.preventDefault();
            return false;
          }
        });
        $('form [type=button]').click(function(ev) {
          $('form').prop('isClick',true).find('[type=submit]').click();
        });

      })(jQuery);

    //--></script>

最初(2)の方を使ってたんだけど、(1)の方がカンタンなので、そっちにした。単に reportValidityというメソッドを後から知ったんだけどね💦

ちなみに、FORMのvalidityチェックの際、エラーメッセージをカスタマイズするプラグインもついで書いた。プラグインにするほどのものでもないけど、一回一回書くのはめんどっちーのでプラグインにした方がラク、というだけの理由ですけどね。

テーブルヘッダの固定

これまで、tableタグ内で thead要素を固定してtbody要素をスクロール可能にするために、
table要素の外側の要素の高さを固定してposition:relative、overflow:autoと指定して、下記のような自作の簡易jQueryプラグインを使用していました。

実際のデモ

HTMLとCSS

jQueryのプラグインは適当に作成。汎用性はなし。

で、最近 display: sticky というのがあって便利だよ、というのを今更ながら知りまして、chrome,firefox用の画面にはこれを使うようしました。
MDNサイトで見ると「粘着位置指定要素 (stickily positioned element)」と言うそうで、要はイケてるサイトでよく見る、
下方スクロールしたら勝手にメニューなんかがページ上部に張り付く(固定される)挙動を簡単に実現できる CSSプロパティみたいです。

stickily positioned elementとは、 position の計算値が sticky である要素です。これは包含ブロックがフロールート (又はその中でスクロールするコンテナー) 内の指定されたしきい値 (例えば top に設定された auto 以外の値など) を達するまでは相対的な配置として扱われ、包含ブロックの反対の端が来るまでその位置に「粘着」するものとして扱われます。

これ読んでも、正直よく分かりません💦 すみません。

で、これをテーブルヘッダに利用しよう、というわけです。
ググると結構記事になっていて、要するに thead要素内の th要素に display:sticky を指定して固定するようです。

実際のデモ

theadをstickyにするのが真っ当な感じですが、現在のブラウザのバージョンでは thead を固定することはできないみたい(無視される)。
それと、th要素をstickyで固定しても、th要素に適用されている border は固定されないみたいな挙動になる感じです(若干ずれてしまう?)

/* 実際のCSS */
.table-container {
  position: relative;
  overflow: auto;
  height: 100px;
  width: 80%;
  border: 1px solid #ccc;
}
.table-container table {
  width: 100%;
  border-collapse: collapse;
}
.table-container table > thead {
  background-color: #eee;
}
.table-container table > thead th {
  border-bottom: 2px solid #ccc !important;
  position: -webkit-sticky !important;
  position: sticky !important;
  z-index: 2 !important;
  top:0 !important;
}
.table-container table > tbody {
  background-color: white;
}
.table-container table tr > * {
  text-align: left;
  border-bottom: 1px solid #ccc;
}
.table-container table > tbody > tr:last-child > * {
  border-bottom: none !important;
}

HTMLはほとんど同じ 。最後のscript要素が要らないだけ。

<div class="table-container">
  <table>
    <thead>
      <tr>
        <th>カラム1</th>
        <th>カラム2</th>
        <th>カラム3</th>
        <th>カラム4</th>
        <th>カラム5</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>カラムデータ1</td>
        <td>カラムデータ2</td>
        <td>カラムデータ3</td>
        <td>カラムデータ4</td>
        <td>カラムデータ5</td>
      </tr>
       ...
       ...
    </tbody>
  </table>
</div>

ただし、Internet Explorerには全バージョンを通して非対応なので(display:stickyは単に無視される)、
かっこ悪いけどブラウザで判別し(navigator.userAgent)、IEの時のみjQueryプラグインを使用するように分岐するようにした。

たとえば、ウェブシステムで行をjavascriptで動的に増やしたり減らしたりするときは、行を追加したり削除したりする関数を一個作っておき、もしブラウザがIEなら 複製テーブル、複製元テーブル両方に行を追加したり、削除したり。なんにしてもメンドクサイが。
IEユーザーがいなくなれば、これも必要なくなるかなと。

ウェブブラウザ上のJavaScriptでExcelファイルをゴニョゴニョしたい

特に需要はないと思いますが、極たま~~~にウェブでエクセルファイルを扱うことがあります。
なんつっても、世の中の文書フォーマットは、マイクロソフトのエクセル(Excel)がデファクトスタンダードです💦
これはもうどうにもなりません。CSVで、つっても、エクセルファイルをよこしやがります。

まぁ、やっぱり業務システムにはエクセルは欠かせません、というか、これなしには、日本は動きません。日本の企業とか国、地方公共団体は、エクセルで動いているんです。これはもう動かしようのない事実であり当分の間は変わりませんし、変化の兆しも見えません。

しょうがない。

と、開発者の方が思ったかどうかわかりませんが、ブラウザのJavaScriptでエクセルファイルをパースして編集して、出力してくれる ライブラリがあるんですね。サーバーサイドのNode.jsでも使えます、というかそっちがメインのライブラリなのかも。

一般的にMS-Officeをインストールしたパソコンにはおまけ?として、EXCELのオートメーションが使えるようになってます。あくまでOfficeを買えば!の話ですが。
Windows Scripting HostなどからVBScriptやJScriptを使ってカンタンにエクセルファイルをアーダコーダできます。

・・・が、今回はこういう噺ではありません。
一般的なブラウザ上で、エクセルファイルをアーダコーダできるライブラリがあったんです。需要がなかったので今まで知らなかった!

SheetJS Spreadsheets simplified

で、ググったりしていろいろ調べると、意外にカンタンに使えちゃいますね。ただし、javascriptの blobとかFileReaderとかArrayBufferとかUint8Arrayとか・・・そういうちょっとややこしめの知識が必要です。

昔と違ってブラウザでローカルファイルとか普通に扱えます。ですが、その時は必ず、上記のFile/Blob/FileReader/ArrayBuffer/TypedArrayとかが絡んできます。
FileとBlobの関係、ArrayBufferとTypedArray(Uint8Arrayとか)の関係、さらにはFileReaderとFile/Blobの関係。このあたりは鬼門です。なんでこんなめんどくさいんだよ!っていつも思います。

MDNとかのリファレンスを読むのが手っ取り早いのですが・・・とりあえず、MDNのサイトでは、こういう場合は、こうする、という「お約束」の手順が書かれているので、まずそれを丸覚えするのがいいと思います。公文式です(笑) 理屈は後から学べばいいんです。ただ、ググってブログ記事を参考にするのは結局は理解するのが遅くなってしまうので、リファレンスとサンプルを読んで、書いて、試してみたほうがいいと思ってます。

※ たぶんIEでは動かないと思う。試してないけど。IEは既にMSも認めたオワコンなんで、どーでもいい。

下のデモ(テストコード)

さて、HTMLファイルをサクッと書きます。(Ryzenの価格表コピペしました。)

JSライブラリは適当にCDNから引っ張ってきましょう。僕は jQuery好き好き人間なので、jQueryを使用します。すみません。
INPUT[type=file]タグでローカルファイルの口としましょう。ドラッグ&ドロップを仕込んでもいいのですが、コードをカンタンにするため、普通のファイル選択にしました。ここでローカルファイルを選んでjavascriptコードに放り込みます。

で、次に実際の処理を書いていきます。まず、基本。INPUT[type=file]のonchangeイベントハンドラを起点にしています。(13行目付近)
ローカルファイルを読み込んでゴニョゴニョするときは、必ずFileReaderのインスタンスを作って、onloadイベントで処理を行います。(18行目付近)
下記例では、FileReader.readAsArrayBuffer() していますが、単純に Data URIが必要であれば FileReader.readAsDataUrl() を使用します。
ローカルの画像ファイルを読み込んで表示するときは、readAsDataUrlメソッドを使いますよねぇ。

ちなみにreadAsArrayBufferメソッドを使うと、onloadイベントハンドラ内で ev.target.result によってArrayBufferオブジェクトを得ることができますが、直接このArrayBufferオブジェクトにアクセスすることができません。必ず、TypedArray・・・たとえば、Uint8Arrayなどのオブジェクトのインスタンスからアクセスします。めんどくさいですねぇ。

FileReader.onload内で、ev.target.result を そのまま Uint8Arrayコンストラクタに渡して、ArrayBufferへアクセスするための Uint8Arrayオブジェクトを作り(21行目付近)、それを XLSX.readメソッドに渡します。ただそれだけで、エクセルファイルをパースしてくれます。あとは、XLSX.utils オブジェクトのメソッドをコールしていけば、だいたいのことはできると思います。
CSVデータが欲しければ、XLSX.utils.sheet_to_csv, JSONデータが欲しければ XLSX.utils.sheet_to_json、シートを増やしたければ、XLSX.utils.append_sheet、他にも javascript配列をシートに、DOM TABLE要素をシートに・・・とか対応するメソッドがあるようです。この辺のドキュメントはgithubをたよりに試行錯誤するしかないのかな。。。

エクセルファイルにシートを追加して、そのエクセルファイルをダウンロードする、これだけのことをクライアント内(ブラウザ内)で完結することができてしまいます。
ローカル内でサーバーを立てる必要もありません。上記二つのHTMLとJSを任意のフォルダに適当な名前で保存して、file://スキームで試すことも可能です。

(1) HTMLファイル ローカルで開いたところ。

(2) エクセルを選択すると、ダウンロードリンクが追加されます。

(3) 元のエクセルはいつもダミーで使わせてもらってる疑似会社情報のエクセルファイル
( http://hogehoge.tk/personal/ )

(4) HTMLのテーブルデータをエクセルシートにぶっこんでくれます。
ただしセル書式は全部吹っ飛びます。

今回は、エクセルシートを追加してみましたけど、セルの属性とかは全部ぶっとんでしまいました💦
が、単にJSONやCSVが取れればいい、というのが大半の需要かと思いますので問題はないでしょう。

僕は エクセルファイルをサーバー側で変換するのではなく、クライアントで一旦CSVに変換してから、サーバーにアップロードして、処理を行う用途に使用しました。サーバーでエクセルファイルを処理すると重くなるんで・・・。
エクセルを読むだけなら、カンタンにできるので、本当にありがたいライブラリです。

ただ一点、ちゃんとしたリファレンスドキュメントがありません。。。これはPro版を買えってことなのかなぁ。。。Community版はApache License 2.0なんで、「おまえら、ソース読んで、自分でなんとかしろよ」ってことなんでしょうね。

Creating screenshot with Firefox + selenium + Node.js

これのつづきです。

前回はヘッドレスモードでブラウザからスクリーンショットを取りました。これで特に問題はなかったのですが、欲が出てきてしまい、実現するにはブラウザからオプションを指定するだけではできなくなりました。
ということで、node.js から selenium-webdriver を使ってのスクリーンショット生成の自動化のコードを書くことにしました。

実現するには、下記が必要です。

試用環境は、Windows10 Pro(1809) + WSL(ubuntu 1604) + Node.js + FireFox(ubuntu) です。
WSLでのFireFoxのインストールは、apt install firefox で普通にできます。また日本語フォントは、一つ前にも書きましたが、/mnt/c/Windows/Fonts ディレクトリのシンボリックリンクを/usr/share/fontsへ作って フォントキャッシュを更新。

また、FireFoxのwebdriverのインストールです。が、これはダウンロードしてきたものをパスの通ったディレクトリ(例えば /usr/local/binとか)に配置すればOK。

$ wget -nd https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz
$ tar xvzf geckodriver-v0.24.0-linux64.tar.gz
$ sudo cp geckodriver /usr/local/bin/ && sudo chmod 0755 /usr/local/bin/geckodriver

適当にディレクトリを作り、まずはこの中で作ります。
selenium-webdriver のインストールです。

$ npm install selenium-webdriver

ググると、簡単なサンプルコードが出てきますが、ページ全体のスクリーンショットを得るためには、ウィンドウの幅・高さを設定しないといけないみたいで・・・下記コードでは、document.bodyのページコンテンツを保存するために、ページロードが終わった後、document.documentElement.scrollHeightで得られた高さを Window.setRect するようにしました。(29行目付近)
幅を1024ピクセルにハードコードしてますが・・・手抜きです💦 コマンドラインのパース一切手抜きです。すみません。

また、余計なお世話的な機能なんですが、CSSセレクタを使用して、指定要素だけのスクリーンショットも取れるようにしました。これは適当に findElement()してその要素に対して takeScreenshot()メソッドをコールしてやるだけ。(35行目付近)

selenium-webdriverのAPIリファレンスを読めば大概のことはできると思います。ブラウザの各種設定を行うabout:configと同じことをしたい場合は、firefoxドライバのインスタンス生成時に、firefox.Options.setPreferenceインスタンスメソッドで変更もしくは追加したOptionsをsetFirefoxOptionsで設定してあげればいいですし(66行目付近)、いつも使っているプロファイルをコピペして、setProfileメソッドでコピペしたプロファイルのディレクトリパスを指定してあげればいい。(試してはいないけど。。。)

下記コードでは ユーザーエージェントを変えられるようにもしてます(ハードコードですけど。。。)

(75行目付近)上記コメントにも残しましたが、得られたPNGデータを標準出力に書き込むとき、fs.writeSyncを使うとエラーになってしまいます。

Node.js固有の問題なのか、WSLが悪さをしているのか分かりません。パイプじゃなくて単にファイルにリダイレクトさせてあげるとエラーは出ません。僕には原因がわからないので、とりあえず、process.stdout のStreamに書き込むとうまく動きました。
一応エラーでググって見たのですが、よくわかんなかったです。いまいち非同期処理が理解てきていないのかも。

selenium-webdriverの takeScreenshotメソッドでは、PNGファイルが取得できますが、「jpgファイルが欲しい!」「リサイズしたもが欲しい!」とかだと、ImageMagickのconvertコマンドに頼る方がよりUNIXライクな方法ではないでしょうか。。。node.jsでも画像処理のモジュールを組みこめばワンストップでできそうですけど。。。

# リサイズしてJPGファイルに
$ node screenshot.js https://www.instagram.com/xxxx/ | convert - -resize 600x insta.jpg

# 特定のセレクタの画像を取得
$ node screenshot.js https://www.yahoo.co.jp/ "#navi" > yahoo-navi.png

エラーハンドリングしてないので、エラーが起こったら適当に 例外処理入れてね。

google-chromeを使う場合もwebdriverのインスタンスを作成するところ以外(具体的には上記コードの async function takeScreenshot()のところ)はほとんど同じ手順ではないかと思います。

ブラウザのヘッドレスモードでスクショ

ホームページのスクリーンショットを撮る作業を何とか自動化したい・・・ということで、ずっと前は phantomjs 一択だったような・・・ちょっと前に開発終了してて、今はGoogle Chrome、Mozilla FireFox自体が既にヘッドレスモードをサポートしている、との事。

Window版のChrome/FireFox 及びWSL(ubuntu 1604)上のLinux版での導入手順の備忘録です。

まずは、Windows(x64)版で試してみます。
Windows10を使用していますが、すでに Chrome/FireFoxともインストール済みです。
コマンドラインから手軽に使えるように環境変数にChrome/Firefoxのインストール先のディレクトリパスを登録しときます。
僕はあまり環境を汚したくないので 下記のようなコマンドファイル(chrome.cmd/firefox.cmd)を作って現在パスが通ってるディレクトリに放り込んでます(C:\Windows ディレクトリとか(^^;。

@echo off
"Chromeのexeファイルのフルパス" 

FireFoxも同様

さて、ヘッドレスモードは、–headless オプションつけて起動します。
このヘッドレスモードは node.jsやその他のスクリプト言語から、制御するのですが、スクショ撮りだけなら、chrome/firefoxとも -screenshotオプションが用意されていますので、簡単です。

# FireFoxの場合
>> firefox.cmd -headless -screenshot スクショ.jpg https://www.yahoo.co.jp/ --window-size=1024

# Chromeの場合
>> chrome.cmd --headless --disable-gpu --screenshot https://www.yahoo.co.jp/ --window-size=1024,768

微妙にオプションの付け方違いますので本家サイトで要調査です。
FireFoxの場合、カレントディレクトリに画像が作られます。ウィンドウサイズも横幅だけ指定しておけば、高さは自動的に決めてくれますし、ラクです。
chromeの場合は、ちょっとクセがあって、まず、ヘッドレスモードは管理者モードが必要みたいです。コマンドプロンプトを管理者モードで立ち上げないとおそらく失敗します。また、画像の名前は screenshot.png と固定みたい?で、保存されるディレクトリも chromeの実行ファイルと同じディレクトリで固定みたいです。ちょっと使い勝手が悪いです。後述する Linux(ubuntu)版ではカレントディレクトリに作られるのでこの辺は直して欲しいなぁ。。。

実際の業務では、撮ったスクショを ImageMagick でリサイズしたりして加工する、一連のスクリプトを組んで運用します。
注意点が一つ。間違ったURLを指定すると、制御が戻ってこないのでタスクマネージャーもしくは taskkillコマンドで殺すしかありません(^^;

次にLinux版(WSL)です。
具体的な導入手順です。ヘッドレスモードでの使用なので、Xサーバーは必要ありません。

一点、ご注意を。chrome/firefoxとも、日本語フォントが無い場合スクショに豆腐フォントになります。
WSLの場合は、/usr/share/fonts に Windowsのフォントディレクトリのシンボリックを作ればいいみたいですね。

$ sudo ln -s /mnt/c/Windows/Fonts /usr/share/fonts/windows
$ fc-cache -fv 

FireFox編
普通に sudo apt install firefox でインストールします。
使用するときのオプション指定は、上記 Windows版と同じです。

google-chrome編

$ wget -nd https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
$ dpkg -i ./google-chrome-stable_current_amd64.deb

※このパッケージをインストールすると googleのリポジトリが追加されます。追加されたくない場合は、

$ sudo touch /etc/default/google-chrome

4/6現在のバージョン 73.0.3683.xxx は問題(バグ?)があるらしく、ヘッドレスモードで立ち上げるとエラーで落ちます。そのため、以前のバージョンにダウングレードしないと使えません。(WSLの固有の問題なのかも?)

$ google-chrome --headless --no-sandbox --disable-gpu --screenshot https://www.yahoo.co.jp/ --window-size=1024,768
[0406/230503.514352:FATAL:gpu_data_manager_impl_private.cc(892)] The display compositor is frequently crashing. Goodbye.
Failed to generate minidump.Illegal instruction (コアダンプ)

古いバージョンは、ググれば見つけられると思います。僕は71.0.3578.80をダウンロードし、上書きインストールしました。

また、WSLで chromeを使用する場合、WSL自体を管理者モードで立ち上げないと使えないみたいです。普通にWSLを使用すると、–no-sandboxが必要です。–no-sandboxをつけるとセキュリティ低下を伴うので、特にこだわりがない場合は FireFoxを常用するのがいいのかも。

#普通にWSLを立ち上げた場合、--no-sandoboxを付けないと以下のエラーがでます。
$ google-chrome --headless  --disable-gpu --screenshot https://www.yahoo.co.jp/ --window-size=1024,768
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Permission denied
Failed to generate minidump.Illegal instruction (コアダンプ)

#管理者モードで立ち上げると
$ google-chrome --headless --disable-gpu --screenshot https://www.yahoo.co.jp/ --window-size=1024,768
[0406/230947.581201:ERROR:gpu_process_transport_factory.cc(967)] Lost UI shared context.
[0406/230948.477496:INFO:headless_shell.cc(546)] Written to file screenshot.png.