2019年2月2日土曜日

一つの正規表現でほしい結果を得るのが難しかったので、複数回に分けて正規表現を使えばいいと考えて作ったツールの制作メモ

HTMLとJavascriptで動作するツールを作ったので、その際に実装に困った部分とかをメモしていく。

contenteditable属性とdocument.execCommand関数


当初はtextareaタグを使おうとしたのだが、このままだと正規表現とマッチした箇所が色付けできないようで、別の方法を使わないといけない。

そんな時はcontenteditable属性を好きなタグに付けてあげると、HTML上から好きに編集できるようになる。Web上でテキストエディタを作る時に利用されているようだ。

それに加え、document.execCommand関数を使うと太字にしたり、背景色を変えたり、いろいろな操作ができる。

実際にWeb上で動作が確認できるサイトを以下に書いておく。

https://codepen.io/chrisdavidmills/full/gzYjag/
https://codepen.io/fukaminmin/pen/JyzOLz


document.execCommand関数は一部ブラウザでは使用できない機能がある。

そのため、実際に使えるか確認する必要があるのだが、そのときはdocument.queryCommandSupported関数を使うといい。

var cmd = "bold";
if(document.queryCommandSupported(cmd)){
   document.execCommand(cmd);
}
これで、ブラウザ上で正規表現とマッチした箇所を表示する準備ができた。

Javascript上からテキストを選択する (Seletion型とRange型)


document.execCommand関数は選択されているテキストに対して処理を行う関数なので、どうにかしてマッチした部分を選択する必要が出てきた。

その選択もマウスを使うわけにも行かず、Javascriptから行う必要がある。

そんな時はSelection型とRange型を利用することができる。

Range: https://developer.mozilla.org/en-US/docs/Web/API/Range
Selection: https://developer.mozilla.org/en-US/docs/Web/API/Selection

var range = document.createRange(); // Range型の作成
range.setStart(node, index);
range.setEnd(node, index + regex_pattern.value.length);
//Selection型を取得して、rangeで指定した範囲を選択部分として登録する
window.getSelection().addRange(range);
//選択を解除した時は以下の関数を呼ぶ。
window.getSelection().removeAllRanges();

注意点として、Range.setStart()とRange.setEnd()に渡すnodeの型によって、indexの意味合いが変わってくる。


TextComment、CDATASectionタイプのNodeならその中の文字列の添字と同じ意味合いになるが、それ以外のタイプのノードだと、子要素への添字になる。
今回はテキストの一部を選択したかったので、分岐でうまいこと処理を実装する必要があった。

Node.nodeTypeの種類はNode型の変数からアクセスできる


Javascript初心者丸出しだが、最初はグローバル領域にあるものだと勘違いしていた。


一度、document.execCommand関数で分割されたテキストをもとに戻すと複数の子ノードに分割されたままになる。


(なんだが、ブラウザ依存な気もするが...)

正規表現で一致した箇所をdocument.execCommand関数を使ってマークした後、別の正規表現を使って他の部分をマークしたいケースが当然出てくる。

そのときは以前マークした部分をクリアーしてあげる必要が出てくるが、そのクリアーした後のテキストノードは分割されたままであって、それ起因のエラーに出くわしてしまった。

Node.textContentを使っているとそれに気が付かずに、しばらく原因がわからず困ってしまった。

一度原因がわかれば、一つの子ノードにまとめれば解決できるので、まとめようとしたところ・・・添字アクセス違反のエラーが起きてしまった。なぜ?

子要素の削除にはChildNode.remove関数を使ったほうがいい。…?


子要素へのアクセスはNode.childNodesを利用している。

はじめは、Node.removeChild関数を使ったが、なぜかundefinedになっていた。

ここらへんはHTMLDivElement型からremoveChild関数を呼び出そうとしてのが間違いだった。

子ノードの削除はChildNode型のremove関数を使うとできるようだ。

終わりに


以上のことで、やりたいことを形に大体できた。
ここに書いていないが正規表現のパイプライン化の処理を追加するとツールはできる。

ただ一つ問題点があって、置換する時に正規表現のキャプチャー機能の扱いをどうするか決めなければいけない。

実際、この機能が欲しい。というゆうより無いととても不便である。

単純に各正規表現でキャプチャーしたものを指定できるようにするのか、オレオレ仕様の謎仕様に仕立て上げるのか、正規表現の仕様を頑なに守ろうとするのか。

どのようにすれば自然な形にキャプチャーできるのかはまた別に考えることにする。

けど、$1.2みたいにドットで区切って指定する形が無難でいいと思う。

2019年2月1日金曜日

FirebaseのonAuthStateChanged関数でページを開いたときだけ、違う処理をしたい!

firebaseを使って漢字パズル的なサイトを作りたいなって思ったので、少しずつ作業を進めていたところ、あるささいな問題に出くわした。

認証時に表示するスナックバーがページを開いたときにも出てしまう。  

書いたままなんだけど、結構鬱陶しかったので、なんとか解消できないか試してみた。

// ページを開いたときにもスナックバーが表示されてしまうコード。
firebase.auth().onAuthStateChanged(function(user){
  if(user) {
    showSnacbar("認証されているよ");
  } else {
    showSnacbar("認証されていないよ");
  }
});

onAuthStateChanged関数の戻り値を使ってみたが…

まずは、 firebase.auth().onAuthStateChanged関数の戻り値を使うと登録した関数を取り除くことができるようなので、初回限定の関数を登録して、そこからはじめの処理を行う関数を登録するようにしてみた。が、ダメだった。

// ダメだったコード。
var unsubscribe = firebase.auth().onAuthStateChanged(function(user){
  unsubscribe();
  //ページを開いたときにはスナックバーを表示しないようにしたつもり
  firebase.auth().onAuthStateChanged(function(user){
    if(user) {
      showSnacbar("認証されているよ");
    } else {
      showSnacbar("認証されていないよ");
    }
  });
});

関数自体を取り除くことはできたが、onAuthStateChangedオブサーバーの発行中にfirebase.auth().onAuthStateChangedで関数を登録すると、その発行中に即座に呼び出されてしまうみたいだ。

そういえば、クロージャってあったよね(解決)


どうしようかと悩んでいたら、Javascriptにはクロージャがあるじゃないかと気づき、以下のコードを書いてみた。
// クロージャを使った成功例
var smartShowSnacbar = function() {
  var counter = 0;
  return function(user) {
    counter++;
    if (counter <= 1) { return ; }

    if(user) {
      showSnacbar("認証されているよ");
    } else {
      showSnacbar("認証されていないよ");
    }
  };
};
firebase.auth().onAuthStateChanged(smartShowSnacbar());
これで無事、目標を達成できた。クロージャを初めて習う人が見ることになるコードだけど、上手に活用できたのではないかと思う。