Edited at

オンラインふせんアプリを作りたい人のためのファーストガイド


この記事でわかること

jQueryを用いたオンラインふせんアプリの作り方


どんなことが書かれているか


  • 画面領域にふせんを貼る方法

  • ふせんをドラッグ・アンド・ドロップで移動させる方法

  • ふせんの削除

  • ふせんの色を変える

  • 画像を貼る

  • テキストの入力に合わせて入力領域を広げる

  • ふせんを手動で広げる

  • データ保存のタイミング


書かれていないこと


  • サーバーサイドのコード

  • デプロイの方法


対象者


  • ふせんアプリを作ってみたいと思ったことがあるけど、何をすればいいのかわからない人


背景

昨年末から今年にかけて、個人で開発しているWorldTypeというWebアプリの一機能として「アイデアボード」という付箋アプリ的機能を開発しました。

今回、開発の際に得た知見を共有します。

「作りたいけどどうしていいかわからん」という人に対して最初のステップを踏み出してもらうきっかけになれば嬉しいです。


先駆者

@Hironsan さんが公開されている「Brain Hacker」のコードの多くを参考にしました。これがなければ、ふせん機能を作ろうとは思わなかったと思います。この場を借りて御礼を申し上げます。

https://github.com/tech-sketch/Brain_Hacker


準備

実験環境として、ふせんを操作するdiv領域(.controller)と

ふせんを配置するdiv領域(.sticky-note-container)だけを持った

HTMLを用意します。


sticky-note.html

<div id="controller">

<input type="button" id="add-button" value="追加">
</div>
<div id="sticky-note-container">

</div>



ふせんを作る

追加ボタンを押したら、ふせんを模したdivをsticky-note-containerに追加します。


sticky-note.js

function getNewNote() {

return '<div class="note">' +
'<input type="text">' +
'</div>';
}

$('#add-button').on('click', function(){
var note = getNewNote();
$('#sticky-note-container').append(note);
});



sticky-note.css

.note {

background-color: aliceblue;
padding: 10px;
}
.note > input[type="text"] {
width: 90%;
height: 5rem;
}

こんな感じになります。

add_notes.gif

https://jsfiddle.net/m846tcgg/

これでふせんを作る作業は終わりです。


ふせんをドラッグアンドドロップできるようにする

さっきの追加したDivをドラッグアンドドロップできるようにしましょう。

jQuery UIを使えば拍子抜けするほど簡単です。

jQuery UIを読み込んだ後、追加した要素のdraggable()を呼び出します。


sticky-note.js

function getNewNote() {

return '<div class="note">' +
'<input type="text">' +
'</div>';
}

$('#add-button').on('click', function(){
var $note = $(getNewNote());
$note.draggable();
$('#sticky-note-container').append($note);
});



sticky-note.css

#sticky-note-container {

height: 400px;
}
.note {
background-color: aliceblue;
padding: 10px;
cursor: pointer;
}
.note > input[type="text"] {
width: 90%;
height: 5rem;
}

これでふせんをドラッグ・アンド・ドロップできるようになりました。

move_notes.gif

https://jsfiddle.net/m846tcgg/1/


ふせんの削除

追加して動かせるようになったら削除機能を付けましょう。

挙動を試すだけなので、削除するためのボタンはふせんの上に置きます。

それ以上に説明することは何もありません。


sticky-note.js

function getNewNote() {

return '<div class="note">' +
'<input type="text">' +
'<input class="delete-button" type="button" value="削除">' +
'</div>';
}

$('#add-button').on('click', function(){
var $note = $(getNewNote());
$note.draggable();
$note.children(".delete-button")
.on('click', function() {
$(this).parents('.note').remove();
});
$('#sticky-note-container').append($note);
});


remove_notes.gif.gif

https://jsfiddle.net/m846tcgg/2/


ふせんの色を変える

次に、ふせんの色を変えます。

ふせんの上に色変更ボタンを置く形で実装しました。

色変更ボタンにcolorというカスタムデータ属性を設定して、それをふせんのbackground-colorに反映するようにしています。


sticky-note.js

function getNewNote() {

return '<div class="note">' +
'<input type="text">' +
'<input class="delete-button" type="button" value="削除">' +
'<input data-color="#e00020" class="color-button" type="button" value="赤">' +
'<input data-color="#e0ff20" class="color-button" type="button" value="黄">' +
'</div>';
}

function appendFunctions($note) {
$note.draggable();

$note.children(".delete-button").on('click', function() {
$(this).parents('.note').remove();
});

$note.children(".color-button").on('click', function() {
const color = $(this).data('color');
$(this).parents('.note').css('background-color', color);
});
}

$('#add-button').on('click', function(){
var $note = $(getNewNote());
appendFunctions($note);

$('#sticky-note-container').append($note);
});


color_notes.gif

https://jsfiddle.net/m846tcgg/9/


画像を貼れるようにする

画像はサーバーサイドを用意する必要があるのですこし面倒ですが、以下のような順番で実装すれば実現できます。


  1. テキストと同じように画像用のふせんを追加するロジックを作る

  2. 画像データを受けるためのサーバーサイドのAPIを実装する

  3. 画像をS3などに配置できたら、そのURLをフロントエンドに返す

  4. imgタグにそのurlを埋め込む

今回は画像のアップロード処理をサポートしてくれるライブラリとしてDropzone.jsを使いました。


  • ドラッグ・アンド・ドロップによるアップロードの実現

  • サーバーへの画像の送信処理の簡素化

をするためだけでも十分に役に立ってくれます。

今回の記事ではサーバーサイドを作ってないので、フロントエンドのコードだけを公開してもそのまま使えるわけではないのですが、処理フローを知るときの参考にはなりますのでご紹介します。


sticky-note.js

function getNewImageNote() {

return '<div class="note">' +
'<img class="note-image hide" crossorigin="anonymous" >' +
'<div class="fallback dropzone"></div>' +
'<input class="delete-button" type="button" value="削除">' +
'<input data-color="#e00020" class="color-button" type="button" value="赤">' +
'<input data-color="#e0ff20" class="color-button" type="button" value="黄">' +
'</div>';
}

function loadImage(id, image_url) {
var content_id = 0/*ふせんのID*/;

var img_elem = $('#' + content_id).children('.file-render').find('.note-image');
var dropzone_elem = $('#' + content_id).children('.file-render').find('.dropzone');

if (image_url) {
img_elem.attr('src', image_url);
img_elem.removeClass('hide');
dropzone_elem.addClass('hide');
}
else {
img_elem.attr('src', '');
img_elem.addClass('hide');
dropzone_elem.removeClass('hide');
}
}

function appendImageNoteFunctions($note) {
$note.find('.dropzone').dropzone({
url: 'http://localhost/api/image/id'/*画像のPOST先URL*/,
maxFilesize: 10,
acceptedFiles: 'image/*',
dictDefaultMessage: 'ファイルを選択するかドロップしてください',
dictFileTooBig: 'アップロードできるファイルは10MBまでです',
headers: {
/*サーバーサイドの要件に従って設定*/
},
success: function(file, response) {
this.removeAllFiles();
loadImage(response.id, response.image);
}
});
}

$('#add-image-button').on('click', function(){
var $note = $(getNewImageNote());
appendImageNoteFunctions($note);
addNewNote($note);//jsfiddleを参考にしてください
});



テキストの入力にあわせて領域を広げられるようにする

テキストの行数にあわせてふせんが縦に伸縮する機能を作ります。

ふせんの中身をTextAreaにして、Autosizeというライブラリを使うと簡単にできます。

Autosize


sticky-note.js

function getNewNote() {

return '<div class="note">' +
'<textarea class="expanding"></textarea>' +
'<input class="delete-button" type="button" value="削除">' +
'<input data-color="#e00020" class="color-button" type="button" value="赤">' +
'<input data-color="#e0ff20" class="color-button" type="button" value="黄">' +
'</div>';
}

function appendFunctions($note) {
$note.draggable();

autosize($note.find('.expanding'));

$note.children(".delete-button")
.on('click', function() {
$(this).parents('.note').remove();
});
$note.children(".color-button")
.on('click', function() {
const color = $(this).data('color');
$(this).parents('.note').css('background-color', color);
});
}


スクリーンショット 2018-06-07 0.12.44.png


ふせんをドラッグで広げられるようにする

ユーザーが自分でふせんを広げられるようにする機能を作ります。

jQuery UIで簡単に実現できますが、

jsのほか、jquery-ui.min.cssを読み込んでいないと動きませんので注意してください。


sticky-note.js

function appendFunctions($note) {

$note.resizable();
// ...
}

https://jsfiddle.net/m846tcgg/32/


データ保存のタイミング

ふせんに入力があったり、ふせんの場所が移動したとき、その変化をサーバーに伝える必要があります。変化の都度保存する方法、ページを移動するときに初めて保存する方法など、やり方はいくつか考えられますが、ここではWorldTypeで行っているやりかたを紹介します。

WorldTypeでは、30秒ごとに変化があったふせんをまとめて保存するという方法をとっています。

関連する箇所を何カ所か抜粋します。


sticky-note.js


$(function(){
setInterval(function(){
saveNotes()
},30000);
});
function saveNotes() {
var unsavedNotes = getUnsavedNotes();

save2DB(unsavedNotes.texts, unsavedNotes.images, unsavedNotes.tables);
}


intervalの間に変化があったふせんにはクラス属性として「unsaved」をつけています。保存したらunsavedをremoveするという単純な流れです。


sticky-note.js

    note.find('.title, .description').change(function() {

setUnsaved(card);
});
note.find('table').colResizable({
onResize: function() {
setUnsaved(card);
}
});
note.resizable({
handles: 'e, w',
stop: function(e, ui) {
setUnsaved(card);
}
});

function setUnsaved(elem) {
$('#save-status').html('未保存あり');

$('.save-note').addClass('unsaved-exists');
$(elem).addClass('unsaved');
}



DBに保存している内容(おまけ)

ふせんのデータは以下のような構造で保存しています。

DB設計の参考にしてください。

(Python3 + djangoです)


models.py


class NoteBase(models.Model):
#親のふせんのID(WorldTypeではふせんの上にふせんをドッキングできる)
parent_id = models.CharField(blank=True, max_length=100)

width = models.IntegerField(default=250)
left = models.FloatField(default=25)
top = models.FloatField(default=25)
color = models.CharField(blank=True, max_length=10)

class Meta:
abstract = True

class TextNote(NoteBase):
title = models.TextField(blank=True)
description = models.TextField(blank=True)

def __str__(self):
return

def __unicode__(self):
return

class ImageNote(NoteBase):
image = ProcessedImageField(upload_to='storybox',
format='JPEG',
options={'quality': 60},
blank=True)



終わりに

今回の内容が皆さんの何かのお役に立ちましたら嬉しいです。

もし書いている内容に誤謬などがありましたらお知らせいただけるととても助かります。

@tRiaez1

ありがとうございました。