LoginSignup
2
5

More than 5 years have passed since last update.

サクッとそれっぽいパーソナルカンバン

Last updated at Posted at 2017-06-10

カンバンがタスク管理に良さそうだったから、やっつけで作ってみた。

jquery
Shadow DOM
FlexBox
D&D API
自作コンテキストメニュー
Save、Load、Del、カンバン追加用テキストエリア表示

バグは気分で修正する。


<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>パーソナルカンバン</title>
    <link rel="stylesheet" href="./kanbanStyle.css">
    <script src="./jquery.min.js"></script>
    <script src="./kanban.js"></script>
</head>
<body>
    <div id="flexWrap">
        <div class="kanban" id="todoTask" dropzone="move">
            <div class="kanban-title"><h2>TODO</h2></div>
        </div>
        <div class="kanban" id="doingTask" dropzone="move">
            <div class="kanban-title"><h2>doing</h2></div>
        </div>
        <div class="kanban" id="backlogTask" dropzone="move">
            <div class="kanban-title"><h2>backlog</h2></div>
        </div>
    </div>
    <div id="kannbann-addItem" style="display:none">
        <div id="kanban-add-wrap">
            <button id="kanban-add-clz">x</button>
            <button id="kanban-add-todo">TODO</button>
            <button id="kanban-add-doing">doing</button>
            <button id="kanban-add-backlog">backlog</button>
        </div>
        <textarea id="kannbann-addItem-inpuut" placeholder="1行目タイトル&#13;&#10;2行目以降内容"></textarea>
    </div>
    <template id="tmpKanbanItem">
        <div class="kanban-item" contextmenu="delKanban" draggable="true"><h3 class="kanban-item-title">title</h3><p class="kanban-text">text</p></div>
    </template>
    <template id="tmpContextMenu">
        <li class="context-item" id="del" >Delete</li>
        <li class="context-item desable">Move item<ul class="contextMenu-item">
            <li class="context-item desable" id="moveup">moveup</li>
            <li class="context-item desable" id="movedown">movedown</li>
        </ul></li>
    </template>
    <template id="tmpContextMenuSaveLoad">
        <li class="context-item" id="addItem">AddItem</li>
        <li class="context-item" id="save">Save</li>
        <li class="context-item" id="load">Load</li>
    </template>
</body>
</html>

kanbanStyle.css

html,body{width:100%;height:100%;padding:0;margin:0;}
h1,h2,h3,h4,h5,h6{margin:0.5rem 0;padding:0;}
#flexWrap {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: 100%;
    height: 100%;
}
#todoTask{background: lightgray}
#doingTask{background: white}
#backlogTask
.kanban {
    display: grid;
    position:relative;
    height: 100%;
    grid-template-rows:2rem;
    grid-auto-rows: 200px;
    grid-gap: 1rem;
    overflow-y:scroll;
}

.kanban-title {
    display:flex;
    height: 2rem;
}

.kanban-title > h2 {
    margin:0;
    padding:0;
}
.kanban-item {
    margin: 0 1rem;
    border: solid 2px #666;
    border-radius: 5px;
}
.kanban-item-title {font-size: 1.2rem;}
.kanban-text {font-size: 0.8rem;}

#kannbann-addItem {
    height: 200px;
    position:fixed;
    bottom:0;
    width:calc(100% - 2rem);
    margin: 0;
    padding: 0 1rem 1rem;
    background: gray;
}
#kanban-add-wrap{}
#kannbann-addItem-inpuut{
    width: calc(100% - 4px - 2rem);
    height: 170px;
    resize:none;
    outline: none;
    margin:0;
    padding: 0.5rem;
    border: 2px solid #999;
}

.contextMenu {
    position: absolute;
    background: lightgray;
    margin: 0;
    padding: 5px;
}

#contextMenu {
    position:absolute;
    min-width: 120px;
    margin:0;
}

.contextMenu-item {
    list-style:none;
    padding: 0;
    border: 0.1px solid #ccc;
    box-shadow:3px 3px 5px 0px rgba(0,0,0,0.5);
}

.context-item {
    position: relative;
    font-size: 0.8rem;
    height: 1.0rem;
    padding: 0.2rem;
    background: #fff;
}
.context-item:hover {background: #c6d7ff;}
.contextMenu-item > hr {
    border-top: 0.2px solid #ccc;
    border-left:none;
    border-right:none;
    border-bottom:none;
    margin:0;
    padding: 1px;
    background: #fff;
}

.context-item > .contextMenu-item {display: none;position: absolute;left: 100%;top: 0;}
.context-item:hover > .contextMenu-item {display: block;}

.desable{background: lightgray !important;}

kanban.js

$(function(){
    loadKanban();
    setEvents();
});

function setEvents(){
    $('.kanban-item').each(function(idx, elem) {
        elem.id = `kanban-item-${idx}`;
    })

    $('.kanban').on('dragover dragenter', function(e) {
        e.preventDefault();
    })
    .off('contextmenu').on('contextmenu', kanbanContextMenuSaveLoad)
    .on('drop', kanbanDropEv);

    $('.kanban-title').on('click', showKanbanAdd);

    $('#kanban-add-clz').on('click', hideKanbanAdd);
    $('#kanban-add-todo').on('click', function(){addKanbanItem('#todoTask')});
    $('#kanban-add-doing').on('click', function(){addKanbanItem('#doingTask')});
    $('#kanban-add-backlog').on('click', function(){addKanbanItem('#backlogTask')});

    addKanbanItemEvent();
}

function addKanbanItemEvent(){
    $('.kanban-item')
    .off('dragstart').on('dragstart', kanbanDragstartEv)
    .off('contextmenu').on('contextmenu', kanbanContextMenu)
    ;
}

function kanbanDragstartEv(e){
    var elem = $(this);
    var kanbanID = elem.closest('.kanban').attr('id');
    var itemID = `#${elem.attr('id')}`;
    e.originalEvent.dataTransfer.setData('kanbanID', kanbanID);
    e.originalEvent.dataTransfer.setData('itemID', itemID);
}
function evCanselAndBlockBubble(e){
    e.preventDefault();
    e.stopPropagation();
}
function kanbanContextMenuSaveLoad(e){
    evCanselAndBlockBubble(e);

    let elem = getContextMenuBase(e);
    elem.append($(getKanbanContextMenuSaveLoad()));
    $(this).closest('.kanban').append(elem);

    elem.children().data('targetID', `#${e.currentTarget.id}`);
    setContextMenuEvent(elem);
}

function kanbanContextMenu(e) {
    evCanselAndBlockBubble(e);

    let elem = getContextMenuBase(e);
    let item = `${getKanbanContextMenuSaveLoad()}<hr>${getKanbanContextMenu()}`;

    elem.append($(item));
    $(this).closest('.kanban').append(elem);

    elem.children().data('targetID', `#${e.currentTarget.id}`);
    setContextMenuEvent(elem);
}

function getContextMenuBase(e){return $(`<ul id="contextMenu" class="contextMenu-item" style="top:${e.pageY - 5}px;left:${e.offsetX - 5}px;"></ul>`)}
function getKanbanContextMenuSaveLoad() {
    let menuItem = $('#tmpContextMenuSaveLoad')[0].content.cloneNode(true);
    return getKanbanContextMenuCreator(menuItem);
}
function getKanbanContextMenu(){
    let menuItem = $('#tmpContextMenu')[0].content.cloneNode(true);
    return getKanbanContextMenuCreator(menuItem);
}
function getKanbanContextMenuCreator(menuItem) {
    let item = '';
    for (let li of menuItem.children) item+= li.outerHTML;
    return item;
}

function setContextMenuEvent(menuElem){
    menuElem
    .off('click')
    .on('click', '#addItem', showKanbanAdd)
    .on('click', '#save', saveKanban)
    .on('click', '#load', loadKanban)
    .on('click', '#del', function(e){kanbanDeltarget($(this).data('targetID'));})
    .on('click', '#moveup', function(e){kanbanMoveUp($(this).data('targetID'));})
    .on('click', '#movedown', function(e){kanbanMoveDown($(this).data('targetID'));})
    .on('click', '.context-item', kanbanContextMenuRemove)
    .off('mouseleave').on('mouseleave', kanbanContextMenuRemove)
    ;
}

function kanbanDeltarget(id){$(id).remove();}
function kanbanContextMenuRemove(){$('#contextMenu').remove();}
function kanbanMoveUp(id){
    console.log(id);
}
function kanbanMoveDown(id){
    console.log(id);
}

function kanbanDropEv(e) {
    var kanbanID = e.originalEvent.dataTransfer.getData('kanbanID');
    var itemID = e.originalEvent.dataTransfer.getData('itemID');
    elem = $(itemID).clone();
    $(itemID).remove();
    $(this).append(elem);
    addKanbanItemEvent();
}

function getNewItemID(){
    return $('.kanban-item').length;
}

function saveKanban(){
    var todoItem = [];
    var doingItem = [];
    var backlogItem = [];
    $('#todoTask > .kanban-item').each(function(idx,elem){todoItem.push(retObj(idx, elem));})
    $('#doingTask > .kanban-item').each(function(idx,elem){doingItem.push(retObj(idx, elem));})
    $('#backlogTask > .kanban-item').each(function(idx,elem){backlogItem.push(retObj(idx, elem));})

    let data = {
        todoTask: todoItem,
        doingTask: doingItem,
        backlogTask: backlogItem,
    };
    localStorage.setItem('kanban', JSON.stringify(data));
}

function retObj(idx,elem) {
    return {
        id:elem.id,
        title: elem.querySelector('.kanban-item-title').html(),
        text: elem.querySelector('.kanban-text').html(),
    }
}

function loadKanban() {
    var data = JSON.parse(localStorage.getItem('kanban'));
    for (var d in data) {
        let tmp = '';
        for (var f of data[d]) tmp += `<div class="kanban-item" contextmenu="delKanban" draggable="true" id="${f.id}"><h3 class="kanban-item-title">${f.title}</h3><p class="kanban-text">${f.text}</p></div>`;
        $(`#${d} > .kanban-item`).remove();
        $(`#${d}`).append(tmp);
    }
    addKanbanItemEvent();
}

function showKanbanAdd() {$('#kannbann-addItem').show();}
function hideKanbanAdd() {$('#kannbann-addItem').hide();$('#kannbann-addItem-inpuut').val('');}

function addKanbanItem(id) {
    var data = $('#kannbann-addItem-inpuut').val().split(/\r|\n/);
    var title = data.shift(), text = data.join('<br>');
    let itemId = getNewItemID();
    let elem = $(`<div class="kanban-item" contextmenu="delKanban" draggable="true" id="kanban-item-${itemId}"><h3 class="kanban-item-title">${title}</h3><p class="kanban-text">${text}</p></div>`);
    $(id).append(elem);
    addKanbanItemEvent();
}

function checkMe(me){
    console.log(me);
}

実装予定

  • D&Dの並び替え
  • 登録済みカンバン編集機能
  • 開始終了日付
  • 任意のカンバン種類追加削除
  • stylesheetのless化(less.js使用予定)
  • ショートカットキーによる操作(内容未定)
  • オートセーブ(ON/OFF切り替え付き)

既知のバグ

  • カンバンの内容の長さに合わせてy軸方向に伸縮しない(後ほど更新)
  • カンバンを削除→追加でカンバンに振っているIDが重複する(後ほど更新)
  • カンバン追加後テキストエリアがクリアされない
  • カンバン追加テキストエリアを閉じた時、テキストエリアがクリアされない

修正済み

2
5
1

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
2
5