Help us understand the problem. What is going on with this article?

commonmark.js + highlight.js + mathjax.jsでちょっとリッチなエディタを作ってみた(2/2)

More than 3 years have passed since last update.

おさらい

前回の投稿より、引き続きJavascriptでのMarkdownエディタの実装方法を投稿します。

前回の投稿では

  • Markdownの変換
  • Syntax Highlight
  • Mathjaxでの数式表示

について説明しましたが、今回は以下の機能を説明します。

  • drag & dropでの画像アップロード機能
  • リアルタイムプレビュー

サンプル(JS Bin)

JS Bin on jsbin.com

Qiitaでは何故か埋め込みJSが再生されない…

▶ drag & dropでの画像アップロード機能

[CODE] drag & drop event の付与

  // Add event of Drag & Drop of IMAGE
  // Change TEXTAREA Border 
  $('textarea#input_area').on('dragover', function(e){
    $('textarea#input_area').css('border', '4px green dashed');
  });

  $('textarea#input_area').on('dragleave', function(e){
    $('textarea#input_area').css('border', '4px gray solid');
  });

  $('textarea#input_area').on('drop', function(e){
    e.preventDefault();  // Invalid Browser Default Drag Action
    var file = e.originalEvent.dataTransfer.files[0];
    var imageMarkdown = "![sample-image]("+window.URL.createObjectURL(file)+")";
    GL.insertAtCaret('textarea#input_area', imageMarkdown);
    $('textarea#input_area').css('border', '4px gray solid');
    GL.convertMarkDownToHtml();
  });

概要

 HTML Drag and Drop API events を参考に、イベントを実装しております。

本コードではセレクタ活用のためjQueryを使用しておりますが、Plain Javascriptでも問題ありません。

また、本コードのようにjQueryを実装する際は、.onバインドはバージョン1.7以降で追加された実装(参考)ですので、ご注意下さい。

● dragoverイベント

  $('textarea#input_area').on('dragover', function(e){
    $('textarea#input_area').css('border', '4px green dashed');
  });

ファイルがtextarea上にdragされた祭に、textareaの囲みのborderの色と線の種類を変更します。

dragleaveイベントの詳細はこちら。ちなみに、dragenterというイベントもあるのですが、下記のような違いがあるようです(公式サイトより引用)。

  • dragover
    • The dragover event is fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds).
  • dragenter
    • The dragenter event is fired when a dragged element or text selection enters a valid drop target.

つまり、dragenterはオブジェクトがdrag対象に入った時点で1回発火。dragoverは数百ms毎に同じ状況で発火。

継続的にイベント状況を監視したい場合はdragoverのようなイメージですが、今回の場合はどちらでも良さそう。

● dragleaveイベント

  $('textarea#input_area').on('dragleave', function(e){
    $('textarea#input_area').css('border', '4px gray solid');
  });

こちらはオブジェクトがdrag対象から抜けた時点で発火するイベントで(参照)、上記で変えたtextarea周りのデザインを元に戻します。

● dropイベント

  $('textarea#input_area').on('drop', function(e){
    e.preventDefault();  // Invalid Browser Default Drag Action
    var file = e.originalEvent.dataTransfer.files[0];
    var imageMarkdown = "![sample-image]("+window.URL.createObjectURL(file)+")";
    GL.insertAtCaret('textarea#input_area', imageMarkdown);
    $('textarea#input_area').css('border', '4px gray solid');
    GL.convertMarkDownToHtml();
  });

こちらはdragしたオブジェクトをtextarea上でdropされたことで発火するイベントで(参照)

  • e.preventDefault(); でファイルのdropアクションのキャンセル
    • こちらのリンクでわかりやすく解説されていますが、本来、ファイルをブラウザにdropするとそのファイルをブラウザ上で開くという挙動をしますが、今回はそれをされるとエディタの挙動上問題なので、キャンセルします。
  • ②dropされたファイルの検出
  • ③画像表示用のmarkdown作成

    • これら2つはそのまんまですが、ここは本来ajaxなどでアップローダにファイルを送信し、受け取った画像URLをmarkdwonに反映させる形になるでしょう。
    • このサンプルではどこのアップローダにファイルをアップロードしている訳ではないですが、 HTML5 の File API機能のblob機能の仮想URL を作成して反映しています。(参照)
  • ④現在のキャラット位置にmarkdown挿入

    • 上記2行で作成したmarkdownを現在のtextarea上の現在のキャラット位置に挿入します。
    • GL.insertAtCaret('textarea#input_area', imageMarkdown); は以下の関数です。
// insert string at current Text Caret
GL.insertAtCaret = function(target, str){
    var obj = $(target);
    obj.focus();
    // Case of IE
    if(navigator.userAgent.match(/MSIE/)){
        var r = document.selection.createRange();
        r.text = str;
        r.select();
    // Case of Others
    }else{
        var s = obj.val();
        var p = obj.get(0).selectionStart;
        var np = p + str.length;
        obj.val(s.substr(0, p) + str + s.substr(p));
        obj.get(0).setSelectionRange(np, np);
    }
};
  • ⑤リアルタイムプレビュー更新
    • 前回の記事で解説した関数でpreviewを更新

▶ リアルタイムプレビュー

  // Add event of keypress & keyup
  $('#input_area').keypress(function() {
    if(GL.inputInterval) self.clearTimeout(GL.inputInterval);
    GL.inputInterval = setTimeout(GL.convertMarkDownToHtml, 500);
  });
  $('textarea#input_area').keyup(function(e) {
    if (e.keyCode == 46 || e.keyCode == 8){
      if(GL.inputInterval) clearTimeout(GL.inputInterval);
      GL.inputInterval = setTimeout(GL.convertMarkDownToHtml, 500);
    }
  });

● setTimeout, clearTimeout

文字の入力を検出してtextarea内のmarkdownをhtmlのプレビューに反映させます。

ただ単に文字の1文字1文字の入力を追って行くとイベント駆動が多くなりすぎるし、かといって定期実行というのも不自然なので、setTimeout, clearTimeout(なぜかsetTimeoutのページはなし)を使ってkeypress、keyupの後に500ms経過してからプレビューに反映させるという仕様にしています。

● keypress, keyup

基本的な英数字入力とEnterキーの入力は keypress で検出、delete・backspaceキーは keyup で検出。

keypressでdeleteキー等が検出できない理由は、ブラウザによってはdeleteキー等を文字入力以外の別の挙動(ページを戻るなど)を持っているため、だそうです(参照)。もしこれが本当であれば、前述のpreventoDefaultはここにも必要なのではないかと思いますが…

まとめ

前回の記事のライブラリの実装に加え、上記のコードでブラウザ上にエディタを手軽に作れることがわかりました。

ただ、今回の記事の内容ではファイルアップロード時のAjax通信等を省略していますので、実際に本番運用する場合はXSS対策などのセキュリティ面も考慮しなければなりません。

また何か良い機能を思い出したら改良して記事に起こしたいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした