忘れないように記録しとこ

カバの樹

Vue.jsを使って、もっとWordPressっぽいTinyMCE用メディアアップロードを実装する

2017年11月14日

環境

vue.js : 2.5.6
jQuery: ?
bootstrap : 3.3.7
Dropzone.js:4.3.0
TinyMCE:?

はじめに

 

以前に以下の記事を書きました。

https://www.kabanoki.net/1233

今回は、Wordpressぽいメディアアップロードモーダルをちょっとバージョンアップをして、Vue.jsを導入しました。

選択したファイルの「拡張子」とか「パス」とか「ファイル名」とかの情報を管理するのに、Vue.jsのリスト機能は理想的でした。

JSON形式で情報を保持して、選択時に自動切り替え等々ができて、ちょー便利です!

 

完成版イメージ

http://aqueous-beyond-18288.herokuapp.com/upload_modal/full.html

 

TinyMCEを導入する

まず手始めにtinyMCEを導入します。
ライブラリのダウンロードは、こちらから
「Download TinyMCE Community」版をダウンロードしてもらえば問題ないはずです。


<div class="textarea">
  <textarea class="mytextarea"></textarea>
</div>

 $(function(){
        /**
         *
         * tinymce
         */
        tinymce.init({
            selector: '.mytextarea',
            body_id: 'mce-blog',
            plugins: [
                'advlist autolink lists link image hr anchor',
                'searchreplace wordcount visualblocks visualchars',
                'nonbreaking table contextmenu',
                'paste textcolor colorpicker textpattern'
            ],
            toolbar1: 'undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | link image | forecolor backcolor',
            image_advtab: true,
            relative_urls : false,
            document_base_url :  '/',
            content_style: "#mce-blog {width: 950px}"
        });
 });

 

Bootstrapのモーダルを導入する

次にファイルアップロードフォームをするための枠を用意します。

今回は、Bootstrapのモーダル機能を導入します。

Bootstrapは、こちらからダウンロードしてください。

モーダル周りのマニュアルはこちら

<div id="main-content">
  <button class="btn btn-default" type="button" v-on:click="showModal()">ファイルアップロード</button>			
  <div class="textarea">
    <textarea class="mytextarea"></textarea>
  </div>  
   <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
      <div class="modal-dialog modal-lg" role="document">
         <div class="modal-content">
            <div class="modal-header">
               <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
               <h4 class="modal-title" id="myModalLabel">ファイルアップロード</h4>
            </div>
            <div class="modal-body"></div>
            <div class="modal-footer"></div>
         </div>
      </div>
   </div>   
</div>
 $(function(){
        /**
         *
         * tinymce
         */
        tinymce.init({
            selector: '.mytextarea',
            body_id: 'mce-blog',
            plugins: [
                'advlist autolink lists link image hr anchor',
                'searchreplace wordcount visualblocks visualchars',
                'nonbreaking table contextmenu',
                'paste textcolor colorpicker textpattern'
            ],
            toolbar1: 'undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | link image | forecolor backcolor',
            image_advtab: true,
            relative_urls : false,
            document_base_url :  '/',
            content_style: "#mce-blog {width: 950px}"
        });
 });

 

モーダルにタブを導入する

次に先程のモーダルに、アップロード用とファイル管理用のタブを導入します。

タブは、Bootstrapのタブを使用します。
マニュアルはこちら

しれっとVue.jsを組み込みました。
Vue.jsのダウンロードはこちら
本番バージョンをダウンロードして貰えれば問題ないかと。

Vue.jsですが、この段階ではまだ気にしなくて大丈夫です。

<div id="main-content">
  <button class="btn btn-default" type="button" v-on:click="showModal()">ファイルアップロード</button>			
  <div class="textarea">
    <textarea class="mytextarea"></textarea>
  </div>  
  <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
   <div class="modal-dialog modal-lg" role="document">
      <div class="modal-content">
         <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
            <h4 class="modal-title" id="myModalLabel">ファイルアップロード</h4>
         </div>
         <div class="modal-body">
           <!-- Nav tabs -->
           <ul class="nav nav-tabs" role="tablist">
               <li role="presentation"class="active"><a href="#uplode" aria-controls="profile" role="tab" data-toggle="tab">アップロード</a></li>
               <li role="presentation" >
                  <a href="#home" aria-controls="home" role="tab" data-toggle="tab">ファイル一覧</a>
               </li>
          </ul>
          <!-- Tab panes -->
          <div class="tab-content">
            <div role="tabpanel" class="tab-pane active" id="uplode">A</div>
            <div role="tabpanel" class="tab-pane" id="home">B</div>
          </div>
         </div>
         <div class="modal-footer"></div>
       </div>
     </div>
  </div>    
</div>   
var MEDIA;

 $(function(){
        /**
         *
         * tinymce
         */
        tinymce.init({
            selector: '.mytextarea',
            body_id: 'mce-blog',
            plugins: [
                'advlist autolink lists link image hr anchor',
                'searchreplace wordcount visualblocks visualchars',
                'nonbreaking table contextmenu',
                'paste textcolor colorpicker textpattern'
            ],
            toolbar1: 'undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | link image | forecolor backcolor',
            image_advtab: true,
            relative_urls : false,
            document_base_url :  '/',
            content_style: "#mce-blog {width: 950px}"
        });
 });

MEDIA = new Vue({
  el: '#main-content',
  data: {

  },
  methods:{
    showModal: function(){
      $('#myModal').modal('show');
    },
  }
});

下記がその部分です。

MEDIA = new Vue({
  el: '#main-content',
  data: {

  },
  methods:{
    showModal: function(){
      $('#myModal').modal('show');
    },
  }
});

 

モーダルのアップロードタブにDropzoneを導入する

次に、ファイルをドラッグアンドドロップでアップロードするためにDropzon.jsをダウンロードします。
ライブラリのダウンロードはこちら
マニュアルはこちら

<div id="main-content">
  <button class="btn btn-default" type="button" v-on:click="showModal()">ファイルアップロード</button>			
  <div class="textarea">
    <textarea class="mytextarea"></textarea>
  </div>  
  <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
   <div class="modal-dialog modal-lg" role="document">
      <div class="modal-content">
         <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
            <h4 class="modal-title" id="myModalLabel">ファイルアップロード</h4>
         </div>
         <div class="modal-body">
           <!-- Nav tabs -->
           <ul class="nav nav-tabs" role="tablist">
               <li role="presentation"class="active"><a href="#uplode" aria-controls="profile" role="tab" data-toggle="tab">アップロード</a></li>
               <li role="presentation" >
                  <a href="#home" aria-controls="home" role="tab" data-toggle="tab">ファイル一覧</a>
               </li>
          </ul>
          <!-- Tab panes -->
          <div class="tab-content">
            <div role="tabpanel" class="tab-pane active" id="uplode">
            <form id="my-dropzone" action="/" class="dropzone" >
                <div  class="fallback">
                    <input name="file" type="file" multiple />
                </div>
            </form>
      </div>
            <div role="tabpanel" class="tab-pane" id="home">B</div>
          </div>
         </div>
         <div class="modal-footer"></div>
       </div>
     </div>
  </div>    
</div>  
var mediaNo = 0;
var MEDIA;
Dropzone.options.myAwesomeDropzone = false;
Dropzone.autoDiscover = false;

$(function(){
        /**
         *
         * tinymce
         */
        tinymce.init({
            selector: '.mytextarea',
            body_id: 'mce-blog',
            plugins: [
                'advlist autolink lists link image hr anchor',
                'searchreplace wordcount visualblocks visualchars',
                'nonbreaking table contextmenu',
                'paste textcolor colorpicker textpattern'
            ],
            toolbar1: 'undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | link image | forecolor backcolor',
            image_advtab: true,
            relative_urls : false,
            document_base_url :  '/',
            content_style: "#mce-blog {width: 950px}"
        });
   
      /**
         * dropzone
         */
        var myDropzone = new Dropzone("#my-dropzone",{
            dictDefaultMessage : 'ファイルをドロップでアップロードします。',
            renameFilename: function(file_name){
                return file_name;
            },
            acceptedFiles : 'image/*' +
            ', application/pdf' +
            ', application/excel' +
            ', application/vnd.ms-excel' +
            ', application/msexcel' +
            ', application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' +
            ', application/msword' +
            ', application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            dictInvalidFileType : '選択されたファイルは許可されていない形式です。'
        });
});

MEDIA = new Vue({
  el: '#main-content',
  data: {
    mediaItems:[]
  },
  methods:{
    addMedia: function(item){
      this.mediaItems.unshift(item);
    },
    showModal: function(){
      $('#myModal').modal('show');
    },
  }
});

See the Pen dVaZbp by カバの樹 (@kabanoki) on CodePen.

 

アップロードした画像をモーダルのファイル一覧タブに表示する

さて、ここからVue.jsを本格的に使用します。
アップロードしたファイルをJSONに格納していきます。
そうすれば、自動的に一覧に追加されます。

<div id="main-content">
  <button class="btn btn-default" type="button" v-on:click="showModal()">ファイルアップロード</button>			
  <div class="textarea">
    <textarea class="mytextarea"></textarea>
  </div>  
  <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
   <div class="modal-dialog modal-lg" role="document">
      <div class="modal-content">
         <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
            <h4 class="modal-title" id="myModalLabel">ファイルアップロード</h4>
         </div>
         <div class="modal-body">
           <!-- Nav tabs -->
           <ul class="nav nav-tabs" role="tablist">
               <li role="presentation"class="active"><a href="#uplode" aria-controls="profile" role="tab" data-toggle="tab">アップロード</a></li>
               <li role="presentation" >
                  <a href="#home" aria-controls="home" role="tab" data-toggle="tab">ファイル一覧</a>
               </li>
          </ul>
          <!-- Tab panes -->
          <div class="tab-content">
            <div role="tabpanel" class="tab-pane active" id="uplode">
            <form id="my-dropzone" action="/" class="dropzone" >
                <div  class="fallback">
                    <input name="file" type="file" multiple />
                </div>
            </form>
      </div>
            <div role="tabpanel" class="tab-pane" id="home">
        <div class="media-box col-lg-12 col-md-12 col-sm-12 col-xs-12">
           <div  v-for="item, index in getMediaItems"
                 v-bind:class="[selectMediaItem.no == item.no ? 'media-list col-lg-2 col-md-2 col-sm-2 col-xs-2 media-checked':'media-list col-lg-2 col-md-2 col-sm-2 col-xs-2']"
                 v-on:click="checked(item)" >
              <div class="img-box" v-bind:data-media_id="item.no" >
                  <img
                       v-bind:src="item.path"
                       v-bind:alt="item.client_name"
                       v-bind:data-path="item.path"/>
              </div>
          </div>
        </div>
      </div>
          </div>
         </div>
         <div class="modal-footer"></div>
       </div>
     </div>
  </div>    
</div>   
var mediaNo = 0;
var MEDIA;
Dropzone.options.myAwesomeDropzone = false;
Dropzone.autoDiscover = false;

    $(function(){
        /**
         *
         * tinymce
         */
        tinymce.init({
            selector: '.mytextarea',
            body_id: 'mce-blog',
            plugins: [
                'advlist autolink lists link image hr anchor',
                'searchreplace wordcount visualblocks visualchars',
                'nonbreaking table contextmenu',
                'paste textcolor colorpicker textpattern'
            ],
            toolbar1: 'undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | link image | forecolor backcolor',
            image_advtab: true,
            relative_urls : false,
            document_base_url :  '/',
            content_style: "#mce-blog {width: 950px}"
        });

        /**
         * dropzone
         */
        var myDropzone = new Dropzone("#my-dropzone",{
            dictDefaultMessage : 'ファイルをドロップでアップロードします。',
            renameFilename: function(file_name){
                return file_name;
            },
            acceptedFiles : 'image/*' +
            ', application/pdf' +
            ', application/excel' +
            ', application/vnd.ms-excel' +
            ', application/msexcel' +
            ', application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' +
            ', application/msword' +
            ', application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            dictInvalidFileType : '選択されたファイルは許可されていない形式です。'
        });

        myDropzone.on("success", function(file, res) {
            // 本来は、res にはサーバーからのレスポンスが反映される
            var res = {
                "no": mediaNo,
                "name": file.name,
                "client_name": file.name,
                "path": file.dataURL,
            };

            MEDIA.addMedia(res);

            mediaNo = mediaNo +1;
        });
    });

    var MEDIA = new Vue({
        el: '#main-content',
        data: {
            mediaItems: [],
            selectMediaItem: '',
            selectMediaItemTitle:'',
            selectMediaItemUrl:'',
        },
        methods:{
            showModal: function(){
                $('#myModal').modal('show');
            },
            addMedia: function(item){
                this.mediaItems.unshift(item);
            },
        },
        computed:{
            getMediaItems: {
                get: function () {
                    return this.mediaItems;
                },
                set: function (v) {
                    this.items = v;
                }
            }
        }
    });

下記記述が、今回のメディアアップロードの肝となるデータになります。

mediaItemsが全てのファイルのデータです。
既にアップロードしてあるデータは、mediaItemsにJSONで渡しあげると良いです。

その他は、のち程ご説明します。

        data: {
            mediaItems: [],
            selectMediaItem: '',
            selectMediaItemTitle:'',
            selectMediaItemUrl:'',
        },

下記の記述で、ファイルのアップロードが成功したときに、mediaItemsJSONにデータを追加しています。
サンプルは、アップロードされた画像の生データを反映させていますが、実際に使用する時には、サーバーからのレスポンス値を反映させた方が良いと思います。

        myDropzone.on("success", function(file, res) {
            // 本来は、res にはサーバーからのレスポンスが反映される
            var res = {
                "no": mediaNo,
                "name": file.name,
                "client_name": file.name,
                "path": file.dataURL,
            };

            MEDIA.addMedia(res);

            mediaNo = mediaNo +1;
        });

 

【デモページ】


 

http://aqueous-beyond-18288.herokuapp.com/upload_modal/index4.html

 

アップロードした画像をエディタに反映させる

さて、これが最後になります。
一覧に表示されたファイルを選択し、エディタへ反映させます。
その際に、ファイルのパスだとかファイル名だとかを確認できる機能を追加しました。

<div id="main-content">
  <button class="btn btn-default" type="button" v-on:click="showModal()">ファイルアップロード</button>			
  <div class="textarea">
    <textarea class="mytextarea"></textarea>
  </div>  


    <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
        <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content">
                <div class="modal-header">

                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                    <h4 class="modal-title" id="myModalLabel">ファイルアップロード</h4>
                </div>
                <div class="modal-body">

                    <!-- Nav tabs -->
                    <ul class="nav nav-tabs" role="tablist">
                        <li role="presentation"class="active"><a href="#uplode" aria-controls="profile" role="tab" data-toggle="tab">アップロード</a></li>
                        <li role="presentation" >
                            <a href="#home" aria-controls="home" role="tab" data-toggle="tab">ファイル一覧</a>
                        </li>
                    </ul>
                    <!-- Tab panes -->
                    <div class="tab-content">
                        <div role="tabpanel" class="tab-pane active" id="uplode">
                            <form id="my-dropzone" action="./upload.json" class="dropzone" >
                                <div  class="fallback">
                                    <input name="file" type="file" multiple />
                                </div>
                            </form>
                        </div>
                        <div role="tabpanel" class="tab-pane" id="home">
                            <div class="media-box col-lg-12 col-md-12 col-sm-12 col-xs-12">
                                <div  v-for="item, index in getMediaItems"
                                      v-bind:class="[selectMediaItem.no == item.no ? 'media-list col-lg-2 col-md-2 col-sm-2 col-xs-2 media-checked':'media-list col-lg-2 col-md-2 col-sm-2 col-xs-2']"
                                      v-on:click="checked(item)"
                                >
                                    <div class="img-box" v-bind:data-media_id="item.no" >
                                        <img
                                                v-bind:src="item.path"
                                                v-bind:alt="item.client_name"
                                                v-bind:data-path="item.path"
                                        />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <div class="text-left col-lg-7 col-md-7 col-sm-7 col-xs-7">
                        <p>
                            <strong>ファイル名: </strong>
                            <span class="media-title-checked">{{this.selectMediaItemTitle}}</span>
                        </p>
                        <p>
                            <strong>URL</strong>(下記のURLをコピーしてリンク先に貼り付けてください)
                            <input class="form-control media-path-checked" type="text" v-bind:value="selectMediaItemUrl" style="cursor: pointer" disabled="">
                        </p>
                    </div>
    				<div class=" col-lg-5 col-md-5 col-sm-5 col-xs-5">
    					<button type="button" class="media-insert btn btn-primary " disabled="disabled" v-on:click="insertEditorMedia">記事に挿入</button>
    					<button type="button" class="media-delete btn btn-danger" disabled="disabled" v-on:click="removeMedia">画像を削除</button>
    				</div>
                </div>
            </div>
        </div>
    </div>
    
</div>   
var mediaNo = 0;
var MEDIA;
Dropzone.options.myAwesomeDropzone = false;
Dropzone.autoDiscover = false;
    var base_url = '/aqueous-beyond-18288/web/upload_modal/';

    $(function(){
        /**
         *
         * tinymce
         */
        tinymce.init({
            selector: '.mytextarea',
            body_id: 'mce-blog',
            plugins: [
                'advlist autolink lists link image hr anchor',
                'searchreplace wordcount visualblocks visualchars',
                'nonbreaking table contextmenu',
                'paste textcolor colorpicker textpattern'
            ],
            toolbar1: 'undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | link image | forecolor backcolor',
            image_advtab: true,
            relative_urls : false,
            document_base_url :  '/',
            content_style: "#mce-blog {width: 950px}"
        });

        /**
         * dropzone
         */
        var myDropzone = new Dropzone("#my-dropzone",{
            dictDefaultMessage : 'ファイルをドロップでアップロードします。',
            renameFilename: function(file_name){
                return file_name;
            },
            acceptedFiles : 'image/*' +
            ', application/pdf' +
            ', application/excel' +
            ', application/vnd.ms-excel' +
            ', application/msexcel' +
            ', application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' +
            ', application/msword' +
            ', application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            dictInvalidFileType : '選択されたファイルは許可されていない形式です。'
        });

        myDropzone.on("success", function(file, res) {
            // 本来は、res にはサーバーからのレスポンスが反映される
            var res = {
                "no": mediaNo,
                "name": file.name,
                "client_name": file.name,
                "path": file.dataURL,
                "mime_type": "image",
            };

            MEDIA.addMedia(res);

            mediaNo = mediaNo +1;
        });
        
         /**
         * model
         */
        $('#myModal').on('hidden.bs.modal', function () {
            myDropzone.removeAllFiles();
        });
    });

    var MEDIA = new Vue({
        el: '#main-content',
        data: {
            mediaItems: [],
            selectMediaItem: '',
            selectMediaItemTitle:'',
            selectMediaItemUrl:'',
        },
        methods:{
            showModal: function(){
                $('#myModal').modal('show');
            },
            addMedia: function(item){
                this.mediaItems.unshift(item);
            },
            removeMedia: function () {
                var self = this;

                if(confirm('本当に削除しますか?')){
                     $(".media-insert, .media-delete").prop("disabled", true);

                        self.mediaItems.filter(function(item, index){
                            if(self.selectMediaItem.no == item.no){
                                self.mediaItems.splice(index, 1);
                                self.selectMediaItem = '';
                                self.selectMediaItemTitle = '';
                                self.selectMediaItemUrl = '';
                            }
                        });
                }
            },
            checked:function(item){
                this.selectMediaItem = item;
                this.selectMediaItemTitle = item.client_name;
                this.selectMediaItemUrl = item.path;

                $(".media-insert, .media-delete").prop("disabled", false);
            },
            insertEditorMedia: function(){
                var self = this;

                if(/^image/.test(self.selectMediaItem.mime_type))
                    var media = tinymce.activeEditor.dom.createHTML('img', {src: self.selectMediaItem.path, style:'max-width:100%;', class:'mce-img'}, '');
                else
                    var media = tinymce.activeEditor.dom.createHTML('a', {href: self.selectMediaItem.path, class:'mce-media'}, '<img src="./common/images/document.png">');

                $(".media-insert, .media-delete").prop("disabled", true);

                setTimeout(function(){
                    self.selectMediaItem = '';
                    self.selectMediaItemTitle = '';
                    self.selectMediaItemUrl = '';

                    tinymce.activeEditor.selection.setContent(media);

                    $('#myModal').modal('hide');
                },0);
            },
        },
        computed:{
            getMediaItems: {
                get: function () {
                    return this.mediaItems;
                },
                set: function (v) {
                    this.items = v;
                }
            }
        

Vueにmethodsを追加しました。

下記が、一覧にあるファイルを選択するのに必要な関数です。

checked:function(item){
   this.selectMediaItem = item;
   this.selectMediaItemTitle = item.client_name;
   this.selectMediaItemUrl = item.path;
  
   $(".media-insert, .media-delete").prop("disabled", false);
},

次に、選択されたファイルをエディタに反映する関数です。

<button type="button" class="media-insert btn btn-primary " disabled="disabled" v-on:click="insertEditorMedia">記事に挿入</button>
insertEditorMedia: function(){
   var self = this;

 // エディタに反映するファイルをオブジェクト化する
   if(/^image/.test(self.selectMediaItem.mime_type))
       var media = tinymce.activeEditor.dom.createHTML('img', {src: self.selectMediaItem.path, style:'max-width:100%;', class:'mce-img'}, '');
    else
       var media = tinymce.activeEditor.dom.createHTML('a', {href: self.selectMediaItem.path, class:'mce-media'}, '<img src="./common/images/document.png">');

   $(".media-insert, .media-delete").prop("disabled", true);

  // setTimeoutを使って、処理のタイミングを最後にさせる
    setTimeout(function(){
        self.selectMediaItem = '';
        self.selectMediaItemTitle = '';
        self.selectMediaItemUrl = '';
    
    // ファイルをエディタに反映させる。
        tinymce.activeEditor.selection.setContent(media);

         $('#myModal').modal('hide');
    },0);
},

 

いかがでしょうか?

今日はこの辺でー

  • B!