初めてのJavaScript【メモ帳の作成】

公開日:2019-04-01
最終更新:2019-04-02

はじめに

最近暖かかったと思ったらまた寒くなってきましたね雪も降っています
私の住んでいる地域では、春のような暖かさが数日続いたのちにそれを裏切るかのようにドカ雪が降ります。
でも大抵その雪がその冬の最後となり、春を迎えます。きっとこの雪が止んだら桜が咲くのでしょう

さて、今回は3/28 からメモ帳を作成に取り組みました。
あと今回から #100DaysOfCode に参加したいと思います。JavaScriptの学習を始めたのが3月23日なので今日で9日目ですね(2019.3.31 現在)
今回はGithubにはまだあげていません。少しコードを見直して整理してから上げよう思っています。
それではやっていきます。

この記事で書くこと

この記事では機能の実装課題と問題解決にフォーカスして書きたいと思います。
また学習してわかったことを最後に書いて終わりたいと思います。

メモ帳の作成

メモ帳の機能と仕様・レイアウトの決定

まずはメモ帳の仕様と実装する機能について考えます。
どうせ作るのですからリッチテキストに対応してかっこ良くしたいですね。
実際に実装する機能は以下のようにします。

  • 太字、斜体などの装飾が可能
  • メモの保存、保存データの呼び出しが可能
  • 保存されたメモのデータをリスト表示
  • メモ帳が更新されたらリストの最上位に移動する(更新順に表示)

こんなところではないでしょうか。

次にメモ帳のレイアウトを考えます。
メモ帳では保存されているデータを一覧表示して選択が可能な機能を実装します。
したがって、保存メモの一覧画面メモの編集画面を表示する必要がありそうです。
単純に2カラムのデザインにすれば良さそうです。以下に実際のレイアウトを示します。


Fig 1. レイアウトの模式図. 画面右が保存データの一覧、左が編集画面

  • 右では : 保存されたメモの一覧の表示
  • 左では : 選択されたメモの表示・メモの編集
    が可能になっています。

また手抜き工事のために今回はbootstrapを使ってレスポンシブデザインを実現しました。
bootstrapでうまく設定できなかった部分は適宜CSSを書きました。

機能の実装

まず、 機能を実装する上で使用するidタグを以下の図に示します。


Fig 2 主要な機能を実装するイベント用idの模式図。id属性の値を#で示す。

  • #〇〇Note, #edit〇〇と書いてあるものはボタンになります。押すと関連した機能が実行されます。
  • #editNote, #noteTitleはテキストの入手力を行うために利用されます。

メモ帳の作成では以下の順番に機能を実装していきました。

  1. リッチテキストの編集画面
  2. 保存リストの表示
  3. データの保存・ロード
  4. その他諸々

長くなりそうなので、ここではリッチテキスト編集画面データの保存、ロード、リストへの出力と2つのパートに分けて説明したいと思います。

リッチテキストの編集画面

リッチテキストの編集画面の作成にはかなり手こずりまいした。というのも、最初は<textarea>で入力された文字を動的に受け取って<iframe>といった仕組みで動いているものだと思い、これをそのまま実装しようとしていたからです。
しかし調べていくうちに.contentEditable 属性という神便利な属性が存在することがわかりました。
.contentEditableでは指定した要素を直接変更できるようにする属性のようです。
以下にHTMLとJavascriptそれぞれの実装方法を示します。

HTML       : <div contentEditable="true">編集可能なdiv</div>  
Javascript : document.getElementById(id).contentEditable = true;  

私は以下のように実装しました。

<div id="editArea1" style="width: 400px; height: 300px; background-color: cadetblue;"></div>  
<script>  
    window.onload = function() {  
        var editArea = document.getElementById('editArea');  
        editArea.contentEditable = true;  
    };  
</script>  

これでページがロードされた後にeditAreaが編集可能となりました。
そしてこのcontentEditabletrueの時に、.contenteditable()を用いて選択範囲に対して新しい要素やスタイルを適用することが可能になります。
例えば太字やイタリック、下線を引きたい場合は以下のように実行します。
実行された時に選択されている範囲に対して適用されます。

// 太字  
document.execCommand('bold', false);  
// 下線  
document.execCommand('underline', false);  
// イタリック  
document.execCommand('italic', false);  

Fig 2で示したような#editbole, #edititalic, editunderlineidを持った<button>タグを作り、これをトリガーとすることでイベントを処理します。
実際の例はこちらです。ボタンを押すことで太字になったり太字を標準に戻したりできます。

    <button id="AAAAA">太字ボタン</button>  
    <div id="divBold">ここの領域が太字にできます。</div>  
    <script>  
        var $idaa = function(id) {  
            return document.getElementById(id);  
        };  
        window.onload = function() {  
            var aa = $idaa('divBold')  
            aa.contentEditable = true;  

            $idaa('AAAAA')  
                .addEventListener('click',  
                    () => document.execCommand('bold', false));  
        };  
    </script>  

ほんとこれ便利ですね。
他にも右寄せ中央揃え、hタグの設定、フォントサイズの変更なんかもできるため本格的なリッチテキストエディタも簡単に作れそうです。
というか、以下の文献にはシンプルですが完全体なリッチテキストエディタのソースがありました。
まじでこれに早く気付けばよかったです。

参考文献 :
https://developer.mozilla.org/ja/docs/Web/Guide/HTML/Editable_content#Differences_in_markup_generation

今後の展開

今回はdiv要素に対して変更を行うようにしていましたが、編集画面全体を<iframe>で構成させて.designMode = "On;"とすることでdocumentを直接編集可能にすることができるようです。
こちらの方がもしかしたらデータとして扱いやすいのかも知れません。おそらくはてなブログなどでの見たまま編集モードもこれを採用しているのかと思います。

参考文献 :
https://studio-key.com/1334.html

データの保存、ロード、リストへの出力

まずこのセクションを説明する上で、メモ帳のデータの移動について把握しておく必要がありそうです。
以下に保存データの流れを模式化した図を示します。


Fig 3. 保存データの流れ。init()関数を定義してlocalStrorageにアクセス、一覧リストへの表示を行なっている。

処置の流れはいたって単純そうです。

  1. localStorageに保存されているデータの数を習得
  2. その分だけforを回してインスタンスを生成
  3. 生成したHTMLを.innerHTMLを使って#noteDataAreaに出力
  4. クリックされたメモ帳に対応するデータをリッチテキスト編集画面に出力

34は簡単そうですね。問題は2です。
いかにしてインスタンを生成して更新順に並べ、一覧リストへと出力するかが難しかったです。
こうして整理してみると簡単そうに見えますけどね 笑
この流れが頭の中で整理されるまではかなり考えさせられました
あと、今回メモ帳として保存するデータはJSON形式にすることにしました。これでデータを1つにまとめることができ、今後author更新日時などのデータを加えるのも容易になるはずです。

local strageの仕様

そもそもの話ですがlocalStorageについてちゃんと把握しておく必要がありそうです。
いかにしてデータを保存し読み込むかを考えるために、まずはlocal storageの仕様を調べてみた。

メモ程度に書いているので知っていればlocal storageの注意点まで読み飛ばして構いまっせん。

以下の記述は次のページを参考にしました。
https://html.spec.whatwg.org/multipage/webstorage.html#the-localstorage-attribute

The Storage interface

Web Storageが提供しているインターフェイスStorage Interfaceについて
基本的な機能は以下の4つになる。

  • getter : getItem(key);
  • setter : setItem(key, value);
  • deleter : removeItem(key);
  • void : clear();

Each Storage object provides access to a list of key/value pairs, which are sometimes called items.
だそうな。あとどちらもString型らしい。

  • setItem()について

    If the given key does exist in the list, and its value is not equal to value, then it must have its value updated to value. If its previous value is equal to value, then the method must do nothing.

つまり、keyの重複はあり得ず、重複した場合は値が更新(上書き)される。

  • getItem()について

    The getItem(key) method must return the current value associated with the given key. If the given key does not exist in the list associated with the object then this method must return null.

つまり、getItem(key)で登録した値を返し、値がない場合はnullを返す。

  • removeItem()について

    The removeItem(key) method must cause the key/value pair with the given key to be removed from the list associated with the object, if it exists. If no item with that key exists, the method must do nothing.

つまり指定されたkeyが削除され、keyが存在しない場合はこのメソッドは何もしません。

さらに、For web developers (non-normative)にはこう書いてある。

// Itemの数を返す  
Storage.length  
// 何もないと`0`になり、何かあると`1`以上となる。  

// リスト内のn番目のkeyの名前を返す.Itemの数以上だと  
Storage.key(n)  
// これを使ってリストの番号の初期値を調べてみたら`0`からスタートするっぽい  

The localStorae attribute

メソッドのgetItemsetItemStorage Interfaceで提供されているメソッドのひとつで、これらのInterfaseにはlocalStoragesessionStorageの2つの属性がある様です。
今回はデータが永続的に保存されるlocalStorageを使用したいのでsessionStorageの説明はしません。

localStorageは、アドレス固有のorigenに関連づけられたStorageオブジェクトを返します。
origenというのは同一のアドレスの事を指すのだと思います。そして同じorigenでは保存したデータは、異なるWindwoやタブでも共有可能でorigenが存在する限り永続的に保存されるようです。
また、データ容量は5MBまでだそうです。つまりこのメモ帳は5MBまでしか使用出来なのかもしれません。

使う場合はStorage Interfaceで説明した4つのメソッドの頭にlocalStorage.をつければいい様です。
つまりlocalStorageは以下の様にして使用します。

localStorage.setItem(key, value);  
localStorage.getItem(key);  
localStorage.removeItem(key);  
localStorage.clear();  
localStorage.length;  
localStorage.key(n);  

ちなみにsessionStorageも同様です。

さて話を元に戻しましょう。

local storageの注意点と今回考えた保存方法

そしてlocalstorageを使用する上で注意しなければならない点があります。
それはlocal storageに保存されているデータは'key'を昇順に並べて保存されているということです。
例えば保存データのkeysave-1,save-2, save-3・・・というように割り当てるとします。これを順番に関係なく保存しても、localStrorage.key(n)を使って取り出すとsave-1,save-2, save-3・・・と並べられて出力されるのです。

今回、保存データをリストに更新順に表示しようと考えていました。初めは追加した順に並ぶものと思い、上書き保存などで更新する際は、一旦localStorageから削除して保存し直せば綺麗に順番に並ぶものと思っていました。
しかしこの方法は通用しないことになりますね。
そもそもこの固有の値をkeyの値に持たせるやり方は賢明ではないかもしれません。
save-1,save-2, save-3・・・とデータがあったとき、save-2が消された場合、
save-1,save-3, save-4・・・となりデータを新規に追加するにも不便です。

そこで考えたのは保存した時の年月日、時刻を連結してkeyを生成するやり方です。しかもミリ秒単位で刻めばいくら高速でメモを保存しても手動である限り重複することはなさそうです(localstorageのkeyは重複したら上書きされる)。
そこでこれらのkeyを生成する関数を作成しました。

    /**  
     * localStorageは値を昇順に並べるため、更新順に表示すためにkeyを更新日時にする  
     * 保存した日時をms単位で生成  
     * 0埋めが必要であることに気づいたため、.sliceでゼロ埋めする。  
     *   
     * @return {value} 保存した日時  
     */  
    function generateKey() {  
        const now = new Date();  
        const nYear = ('000' + now.getFullYear()).slice(-4);  
        const nMonth = ('0' + (now.getMonth() + 1)).slice(-2);  
        const nDate = ('0' + now.getDate()).slice(-2);  
        const nHour = ('0' + now.getHours()).slice(-2);  
        const nMin = ('0' + now.getMinutes()).slice(-2);  
        const nSec = ('0' + now.getSeconds()).slice(-2);  
        const nMs = ('00' + now.getMilliseconds()).slice(-3);  
        return TIMES = `${nYear}${nMonth}${nDate}${nHour}${nMin}${nSec}${nMs}`;  

    };  

コメントにあるように、ゼロうめをしています。
これによってミリ秒単位のkeyが生成できます。そしてlocal storage内で勝手に昇順に並べてくれるので取り出す時には更新順に並んでいるわけです。

あとはデータをイテレータでも使って順番に取り出してインスタンスを生成するだけです。
これはネットで探しても全く出てこなくて知った時には驚きました。Chromeだけの問題なのでしょうか???

とりあえず、データを更新順に並べる方法はこれで解決しました。

一覧リストから選択されたメモを編集画面に出力

肝心の処理に関する説明はあまりしていませんが、保存の仕方はある程度分かったかと思います。
次に一覧リストに出力されたデータをクリックして編集画面に出力する方法を考えます。

#noteDataAreaに出力されるHTMLは以下のようになっています。

<div id="noteDataArea">  
    <div class="memo-data">  
        <div class="data-area" id="20190401143114676">  
            <div class="title">リストはスクロール可能</div>  
            <div class="contents">メモが多い場合はスクロールしてみることができる。</div>  
        </div>  
    </div>  
    <div class="memo-data">  
        <div class="data-area" id="20190401143029279">  
            <div class="title">メモ帳のレイアウト</div>  
            <div class="contents">レイアウトはこんな感じです。</div>  
        </div>  
    </div>  
    <div class="memo-data">  
        <div class="data-area" id="20190401142954652">  
            <div class="title">How to keep localStorage values after re</div>  
            <div class="contents">Now, I believe it is clear what this doe</div>  
        </div>  
    </div>  
</div>  

noteDataArea > memo-data > 20190401143114676 > title, contents
といった階層にっなっています。
また3番目のmemo-dataではtitlecontentsが途中で途切れています。
これは保存されているデータの文章が長い場合は途中でそれ以降を切り捨てる処理を行なっているからです。

そしてここで重要なのが20190401143114676などの数値です。これはlocalStorageのところで説明した、生成されるkeyになています。
表示されているこれら一覧データの1つをクリックした場合、対応するidを取得しkeyと照らし合わせればいいと考えました。
要するに
一覧リストを表示 ⇨ クリック ⇨ クリックされた時のidを取得 ⇨ id=keyなので対応するデータを取得してそれを編集画面に表示
という流れになります。

しかし、これはどうやって取得すればいいか結局よくわかりませんでした。
無理やり感がありますが、以下のようにして取得しています。

document.getElementById("noteDataArea").onmousedown = function(e) {  
            selectId = e.path[1].id;  
        };  

一応これで選択されたidを取得することができます。あとはそれに対応するデータを取得して表示するだけです。

他にも細々とした処理があるのですが疲れてしまったのでここまでにしておきます。
また今度密かに更新しているかもしれません。

まとめ

今回は前回の電卓と比較するとかなりレベルが高かったように思えます。
HTML/CSSもJavascriptも、ただ闇雲に書いて挙動を確認していたため時間を多く取ってしまいました。
書いたコードがどのような振る舞いを示すのか、頭の中でイメージしながらコーディングができるように理解を深める必要がまだまだあります。
はじめのうちは.execCommand().contentEditableといった属性が存在することが分からず、これらの機能と自力で実装しようかと考えていました。
このような遠回りを避けるためにも主要なオブジェクト、メソッド、プロパティはしっかり把握しておく必要がありそうです。
次に何作るか考えながら並行してサンプルコードを書いて練習していきたいと思います。

おまけ

ここではこれ以外にメモしたものを忘れないように綺麗にまとめて随時更新していきたいと思います。
この3日間でメモした内容が4000字近くあってまとめるのにも苦労しそうです。

問題と解決方法

  • 装飾ボタンがChromeで効くのにSafariでは使えない。
    ボタンを実装していたタグが<div>タグだったが、<button>タグに変更したところ実行することができるようになった
    Bootstrapを使って、divタグbtnクラスを当てることでボタンを実装できていたのだと思っていた。
    しかし調べてみると大抵btnクラスbuttonタグで使われているようなのでやっぱりbuttonタグを使った方がいいのかもしれない。
    それにしてもChromeでは大丈夫でSafariではダメなのは不思議ですね。
    <button id="btnbold" class="btn" style="width: 150px !important;">btn太字ボタン</button>  
    <div id="divbold" class="btn" style="width: 150px !important;">div太字ボタン</div>  
    <div class="contents-area" style="width: 300px;height: 100px;background-color: bisque" id="editdiv"></div>  
    <script>  
        var $id = function(id) {  
            return document.getElementById(id);  
        };  
        var editArea = $id('editdiv');  
        editArea.contentEditable = true;  
        $id('btnbold')  
            .addEventListener('click', function() {  
                document.execCommand('bold', false);  
            });  
        $id('divbold')  
            .addEventListener('click', function() {  
                document.execCommand('bold', false);  
            });  
    </script>  
記事が少しでもいいなと思ったらクラップを送ってみよう!
0
+1
@okita_kamegoroの技術ブログ

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする