LoginSignup
1
1

More than 1 year has passed since last update.

Qiitaの記事投稿画面みたいなリアルタイムで編集状況わかるやつを作ってみる(第一回)

Posted at

目的

友達が記事投稿のサイト作りたいらしくて、折角ならQiita真似してやろうと思って、今回少しやってみました!

投稿画面

Qiitaの投稿画面をディベロッパーページで確認してみると…

<div spellcheck="false" autocorrect="off" autocapitalize="off" translate="no" contenteditable="true" class="cm-content cm-lineWrapping" style="tab-size: 4;" role="textbox" aria-multiline="true" data-test-editor-body="true" tabindex="30"><div class="cm-line">
</div>

で、textinputを使わない方法を使ってることがわかります…

そんなん出来んのかよ…

これを真似し始めたことが全ての始まりでした…

type=roleでcontenteditable="true"のdiv

こいつを使って何がいいのか?
正直わからなかったけど、とりあえずデザインが楽になりました笑

で、こいつを導入して、どうしていけばいいのかを考えていきます。

どんな挙動を考えるべきか?

想像した流れとしては

1 : テキストを打ち込む
2 : 打ち込んだテキストをjsで獲得
3 : 打ち込まれた文字で要素を検討(「#」なら「h1」とか)
4 : 要素をjsで作成して、右側確認画面に追加

の四段階でいける!と考えていたのですが…
実際は、間に要素を削除していく過程を入れています…

実装

テキストを打ち込む

インプットの場所
<div id='post_write_area_input' spellcheck="false"autocomplete='off'translate='no'role='textbox'area_multiline='true'contenteditable='true'data-test-editor-body="true">
    <div id='post_write_area_line'>
        <br>
    </div>
</div>

なんか知らないけど、ここのbrがあるとないとでかなり変わってきました笑

で、こいつ文字を打ち込むと、子要素のpost_write_area_lineを複製してくれます!!
で、とりあえず文字を打ち込むことに関してはこれでおkです…

ここからがほんと、課題でした…

テキストをjsで獲得する

ここが難しかった!!
今や何をしてきたか覚えてないのですが、とりあえず、contenteditable='true'で動くjsのコードはこんなんでした。

インプットの獲得
let text = document.getElementById('post_write_area_input');

text.addEventListener('input', inputChange);

そう、addEventListenerでinputを指定するのが正解だったんです…
最初はjQueryとかで色々試してたのですが、これでしか挙動しなかったです笑

で、要素を検討するフェーズです!

要素の検討

要素を検討するやつ
function makePutHtml(text, event, num){
    let result = document
    switch(text[0]){
        case '#':
            let j = 0
            for(let i=0; i<text.length; i++){
                if(text[i]=='#' && i < 4){
                    j+=1;
                }
            }
            text = text.slice(j)
            result = result.createElement('h'+j);
            result.setAttribute('id','post_confirm_area_pa'+num)
            result.innerText = text;
            return result
            break;
        default:
            result = result.createElement('p');
            result.setAttribute('id','post_confirm_area_pa'+num)
            result.innerText = text;
            return result
            break;
    }
}

とりあえず打ち込まれた要素を検討するコードはこんな感じで、switchに条件追加してけばいけました…
ただ、これ、問題があります。
実際これで要素を追加はできます。
ただ、追加される要素は永遠に増加して、ピラミットみたいな形の表示が起こります。

ここで、要素を追加するたびに既存の要素を削除していく必要が出てきました。

最初は、入力された内容をリストに行ごとに追加しておいて、変化が起きたらそこだけ削除して表示を直すってのと、一文字入力されるたびに要素を削除して作り直すみたいなことをしていました。

ただ、そんな方法だと、行を削除した後とか、途中の行に追加したときに変な挙動をすることに気づきました。
これ、単純に入力された順番で要素を追加していて、一行ずつで色々検討してるからこんなことになってんだと考えて…

めちゃくちゃ悩んだ結果、一番乱暴であろう、文字が入力されるたびに全要素を削除して、作り直すみたいな方法をとってみました!

function findDiff(event){
    result = []
    arr = [""]
    let delete_elem = document.getElementById('post_confirm_area_watch');
    while(delete_elem.firstChild){
        delete_elem.removeChild(delete_elem.firstChild)
    }
    for(let i = 0; i < event.currentTarget.children.length; i++){
        let child = event.currentTarget.children[i]
        arr[i] = child.innerText
    }
    i = 0
    arr.forEach((elem, idx)=>{
        elem = makePutHtml(elem,event,idx)
        result[idx] = [true,idx,elem]
    })
    console.log(arr)
    return result
}

で、シンプルにしたこれで、狙った通りに動きました!!

要素をjsで作成して、右側確認画面に追加

要素をjsで作成して、右側確認画面に追加
function inputChange(event){
    let arr = findDiff(event)
    for(let i=0; i<arr.length; i++){
        elem = arr[i]
        if(elem[0]){
            const getText = event.data
            confirmView.appendChild(elem[2])
    }
    }
}

はい、これで単純に追加はできます!!

実装したコード

最終的にはこんな感じのコードになりました!

<?php
    $title = 'First Step Topページ';
    $description = 'post';
    include './component/head.php';
?>
<style>
body.no_scroll{
   overflow: hidden;
}
body{
    margin: 0px;
    padding: 0px;
    background-color: #e7e7e7;
}
header{
    height: 8vh;
    padding: 0px;
    background-color: #9EF26A;
}
h1{
    padding: 0%;
    margin: 0;
    margin-left: 5px;
    padding: 3px;
}
#post_area{
    display: table;
    background-color: white;
    width: 100%;
    height: 75vh;
}
.page_make{
    border: solid 0.5px lightgray;
    display: table-cell;
    width: 50%;
    border-width: 0.5px;
}
.post_write_area{
    position: relative;
}
.post_confirm_area{
    position: relative;
}
#post_write_area_input{
    position: fixed;
    width: 50%;
    height: 75vh;
    overflow: scroll;
    border-left: 1px;
    border-top: 0px;
    border-bottom: 0px;
}
#post_write_area_input:focus{
    outline: none;
}
#post_confirm_area_watch{
    overflow-wrap: break-word;
    white-space: pre-wrap;
    position: fixed;
    width: 50%;
    height: 75vh;
    overflow: scroll;
    border-left: 1px;
    border-top: 0px;
    border-bottom: 0px;
}
p{
    padding: 0px;
    margin: 0px;
    margin-inline: 0px;
}
</style>
</head>
<body>
<header>
<div>
    <h1>Logo</h1>
</div>
</header>
    <div id='fill'>
        <div id='post'>
            <form action="">
            <div id='post_title_area'>
                <input placeholder="タイトル" tabindex="10" class="title_input" value="">
            </div>
            <div id='post_tag_area'>
                <input autocomplete="off" placeholder="タグ" tabindex="20" type="text" class="css-amcp4b ed5758y1" value="" >
            </div>
            <div id='post_area'>
                <div class='page_make post_write_area'>
                    <div id='post_write_area_input' spellcheck="false"autocomplete='off'translate='no'role='textbox'area_multiline='true'contenteditable='true'data-test-editor-body="true">
                        <div id='post_write_area_line'>
                            <br>
                        </div>
                    </div>
                </div>
                <div class='page_make post_confirm_area'><div id='post_confirm_area_watch'></div></div>
            </div>
            <div id='post_button_area'></div>
            </form>
        </div>
    </div>
</body>
<script type="text/javascript">
let i = 0
let col = 1
const confirmView = document.getElementById('post_confirm_area_watch')
let arr = [""]
function enterKey(event){
    if(event.key==='Enter'){
        i = 0
        col += 1
    }
}

function addFile(event){
    const newelem = document.createElement('p')
    newelem.setAttribute('id','post_confirm_area_pa'+col)
    confirmView.appendChild(newelem)
    i+=1
}

function makePutHtml(text, event, num){
    let result = document
    i+=1
    switch(text[0]){
        case '#':
            let j = 0
            for(let i=0; i<text.length; i++){
                if(text[i]=='#' && i < 4){
                    j+=1;
                }
            }
            text = text.slice(j)
            result = result.createElement('h'+j);
            result.setAttribute('id','post_confirm_area_pa'+num)
            result.innerText = text;
            return result
            break;
        default:
            result = result.createElement('p');
            result.setAttribute('id','post_confirm_area_pa'+num)
            result.innerText = text;
            return result
            break;
    }
}

//問題点
// 何故日本語入力だと画面に反映されるためにEnterでの改行が必要になっているのか
// 何故<h1>を利用すると上の要素が消去されてしまうのか
// 何故文字列の消去が反映されないのか?
// インデックスのずれ
//  -> 文字列の消去と同時に文字列の順序が逆転するのはなぜか?(appendChildで追加されてるから、そのものを修正してるのではなくて、修正されたものが追加される形になって、後ろにpushされている)
//      -> HTML要素そのものを入れ替える or colの順番で並ぶようにする?
// なんか知らんけど、一列丸々消去すると要素の複製が起こる
// ?一回の入力ごとにHTMLを丸々作り直すようにしたらどうだろうか?1

function findDiff(event){
    result = []
    arr = [""]
    let delete_elem = document.getElementById('post_confirm_area_watch');
    while(delete_elem.firstChild){
        delete_elem.removeChild(delete_elem.firstChild)
    }
    for(let i = 0; i < event.currentTarget.children.length; i++){
        let child = event.currentTarget.children[i]
        arr[i] = child.innerText
    }
    i = 0
    arr.forEach((elem, idx)=>{
        elem = makePutHtml(elem,event,idx)
        result[idx] = [true,idx,elem]
    })
    console.log(arr)
    return result
}

function inputChange(event){
    let arr = findDiff(event)
    for(let i=0; i<arr.length; i++){
        elem = arr[i]
        if(elem[0]){
            const getText = event.data
            confirmView.appendChild(elem[2])
    }
    }
}
let text = document.getElementById('post_write_area_input');

text.addEventListener('keypress',enterKey)
text.addEventListener('input', inputChange);
</script>
<script>
</script>

絶対もっとシンプルなの書けるんじゃねえかなあ、と思うんですけど、とりあえずドンマイです笑

まとめと今後

これで一応リアルタイムで反応する投稿ページは作れました!
文字で挙動宣言できるようにできたから、文字の入力でリンクの追加とか写真の追加、テーブルの表示等できるようにしていき、最終的にはphpでDBに追加していけるように動かしていこうと思います!!

1
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1