JavaScript
HTML5
CSS3
Web

[ページ遷移しないWebシステム] JavaScriptによるリッチテキストエディタ

 CMSを自作しようと思うと、ついて回るのはテキスト編集機能です。HTMLタグをできる限り意識せずに、ある程度の体裁を表現できるものが必要となります。

 そこで必要になるのがリッチテキストエディタですが、有名どころの欠点はエディタを固定することを前提に作られていることです。そうなるとテキスト編集専用ページに遷移するような、悲しい実装になってしまいます。

 理想はページ遷移せず、その場でテキストを編集し、その場で結果を確認できることです。今回は、そんな機能のサンプルを作りました。

関連リンク

サンプル      https://mofon001.github.io/teditor/
ソースコード    https://github.com/mofon001/teditor
これで実装したBlog https://croud.jp/

以前の内容

[ページ遷移しないWebシステム] JavaScriptによるWindow Framework
[ページ遷移しないWebシステム]Qiitaの投稿をページ遷移無しで表示してみる
[ページ遷移しないWebシステム] JavaScriptによるURLの操作と、ページ遷移しないプログラム

リッチテキストエディタを作ろう

動作画面

image.png

 編集したい場所をクリックすると、エディタが起動します
 エディタは可動式のウインドウになっているので、ページ遷移の必要はありません
 保存ボタンを押すと、その場で内容が書き換わります
 画像ファイルのコピペやドラッグドロップにも対応しています
 オリジナルカラーピッカーもあります(RGBって三色だから三角形だよね?)

利用するために必要なソースコード

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <link rel="stylesheet" href="css/Gui.css"/>
    <link rel="stylesheet" href="css/Editor.css"/>
    <style type="text/css">
        div.target{margin: 8px;background-color: antiquewhite;min-height: 300px;}
    </style>
    <script type="text/javascript" src="script/Gui.js"></script>
    <script type="text/javascript" src="script/TextEditor.js"></script>
    <script type="text/javascript" src="Main.js"></script>
    <title>JavaScriptによるWindowフレームワーク</title>
</head>
<body>
    <div class="target">テスト用のエリア1</div>
    <div class="target">テスト用のエリア2</div>
    <div class="target">テスト用のエリア3</div>
</body>
</html>
Main.js
(function(){
//エディタカスタマイズ用
function createCustomEditor(node){
    var editor = createTextEditor();    //テキストエディタの作成
    editor.setSize(1200,800);           //サイズの指定
    editor.setPos();                    //位置を中央に設定
    editor.setHtml(node.innerHTML);     //nodeの内容をエディタに設定

    //コントロールパネル作成(エディタのカスタマイズ)
    var panel = editor.addPanel();
    panel.getClient().innerHTML = "<button>保存</button>";
    panel.setSortZ(1);                  //パネルを一番上に
    //保存ボタンを押したらノードに内容を書き込む
    panel.getClient().querySelector("button").addEventListener("click",function(){
        node.innerHTML = editor.getHtml();
    });
}

//ページが読み込まれたらonLoadを実行
document.addEventListener("DOMContentLoaded",onLoad);
function onLoad(){
    //DIVタグをクリックしたらテキストエディタを作成する
    var nodes = document.querySelectorAll("div.target");
    for(var i=0;i<nodes.length;i++){
        nodes[i].addEventListener("click",function(){
            createCustomEditor(this);
        });
    }
}

})();

 必要になるのは以下のファイルです。

  • Gui.css
  • Editor.css
  • Gui.js (ウインドウ表示用ライブラリ)
  • TextEditor.js (エディタ機能)

 createTextEditor()でテキスト編集の基本機能が使えます。追加でcreateCustomEditor()を作っていますが、ここで保存機能などを追加します。
 いつもはBODYタグの中は一切なにも書かないのですが、今回はサンプルが分かりやすくなるように涙をのんで書きました。

プログラムの解説

 ページ遷移をしないシステムを作るために私が最初にやったのは、JavaScriptによるウインドウシステムの作成です。これを作っておけば、あとはウインドウ上に好きな機能をのせて、好きな時に好きな場所へ表示が可能になります。

 この辺りの肝になるGui.jsですが、これ自体を全て解説するのはかなり困難です。ウインドウをウインドウとして動作させるため、あらゆる挙動を自前で計算しています。フレーム部分を引っ張ってサイズを変更する処理一つでも、かなり手間がかかっています。親ウインドウのサイズが変わった場合に、それを子ウインドウへ伝播させ、レイアウトを再計算するなんてこともしなければなりません。特定のウインドウがフォアグラウンドに回った時に、別のウインドウとの重ね合わせ順序の再計算なども処理しています。

 Editor.jsの方はウインドウ機能を利用していることを除けば、一般的なリッチテキストエディタと変わりません。追加機能があるとすると、せいぜい画像をUrlDataに変換して貼り付けられるぐらいです。ちなみに私のBlogシステムの方は、Qiitaと同じように、ファイルをアップロードしてリンクを張る形式にしてあります。UrlData形式は今回のサンプル用にカスタマイズしたものです。

これを実現するのに書いたソースコード

 利用するだけなら、この内容は理解しなくても大丈夫です。
 Gui.jsに関しては今回では使っていない機能も含まれているので、かなり長くなっています。
 それに比べればTextEditor.jsのテキスト編集部分の実装はまだマシな量です。
 

Gui.js
(function(){

    //ページ読み出し後の初期化処理    
    document.addEventListener("DOMContentLoaded",onLoad);
    function onLoad(){

        //GUI描画エリアの作成
        GUI.root = document.createElement("div");
        GUI.root.className = "GUIArea";
        document.body.appendChild(GUI.root);

        GUI.rootWindow = GUI.createWindow();
        GUI.rootWindow.id = "root";
        GUI.rootWindow.style.width = 0;
        GUI.rootWindow.style.height = 0;
        GUI.rootWindow.setSize(getClientWidth(), getClientHeight());
        GUI.layout(true);
    }

    //RGB変換
    function getARGB(color) {
        var a = (color >>> 24) / 255;
        var r = (color >> 16) & 0xff;
        var g = (color >> 8) & 0xff;
        var b = color & 0xff;
        return "rgba(" + r + "," + g + "," + b + "," + a + ")";
    }



    //GUIがらみの初期設定
    GUI = {};
    GUI.getARGB = getARGB;
    GUI.getRGB = function(color){
        var a = (color >>> 24) / 255;
        var r = (color >> 16) & 0xff;
        var g = (color >> 8) & 0xff;
        var b = color & 0xff;
        return "rgb(" + r + "," + g + "," + b+")";
    }
    GUI.foreground = [];
    GUI.focus = null;
    GUI.setFocus = function(node){
        GUI.focus = node;
    }

    //イベント処理全般
    function onTouchEnd(e){
        onMouseUp(e);   //タッチイベントをイベントを転送
    }
    function onMouseUp(e){
        e.etype = "mouseup";
        GUI.selectWindow = null;
        if(GUI.overParts == "GUIClient")
            GUI.overWindow.callEvent(e);    
    }
    function onTouchStart(e){
        if(e.targetTouches.length > 0){
            var touch = e.targetTouches[0];
            e.clientX = touch.pageX;
            e.clientY = touch.pageY;
            e.etype = "mousedown";
            onMouseDown(e);
        }
    }

    function onMouseDown(e){
        e.etype = "mousedown";
        GUI.mouseDownX = parseInt(e.clientX);
        GUI.mouseDownY = parseInt(e.clientY);

        var win = GUI.getWindow(GUI.mouseDownX,GUI.mouseDownY);
        if(win){
            win.setForeground();
            var x = win.GUI.x;
            var y = win.GUI.y;
            var widht = win.GUI.widht;
            var height = win.GUI.height;

            var partsName = win.getHit(GUI.mouseDownX,GUI.mouseDownY);
            if(partsName){

                GUI.selectWindow = win;
                GUI.selectStartX = win.GUI.x;
                GUI.selectStartY = win.GUI.y;
                GUI.selectStartWidth = win.GUI.width;
                GUI.selectStartHeight = win.GUI.height;
                GUI.selectParts = partsName;
            }
            switch(partsName){
                case "GUITitleMin":
                    win.setMinimize(!win.GUI.minimize);
                    break;
                case "GUITitleMax":
                    win.setMaximize(!win.GUI.maximize);     
                    break;
                case "GUITitleClose":
                    win.close();
                    break;
                case "GUIClient":
                    var p = win.screenToClient(GUI.mouseX, GUI.mouseY);
                    e.cx = p.x;
                    e.cy = p.y;
                    win.callEvent(e);
                    break;
            }
        }
    }
    function onMouseClick(e){
        e.etype = "mouseclick";
        GUI.mouseDownX = parseInt(e.clientX);
        GUI.mouseDownY = parseInt(e.clientY);
        var win = GUI.getWindow(GUI.mouseDownX,GUI.mouseDownY);
        var p = win.screenToClient(GUI.mouseX, GUI.mouseY);
        e.cx = p.x;
        e.cy = p.y;
        win.callEvent(e);
    }
    function onMouseDblClick(e){
        e.etype = "mousedblclick";
        if(GUI.overParts == "GUIClient"){
            GUI.mouseDownX = parseInt(e.clientX);
            GUI.mouseDownY = parseInt(e.clientY);
            var win = GUI.getWindow(GUI.mouseDownX,GUI.mouseDownY);
            var p = win.screenToClient(GUI.mouseX, GUI.mouseY);
            e.<