Vanilla JS Sticker Project

DW J·2022년 9월 27일
0

project_sticker

목록 보기
1/2

스터디 과제 - 기능 구현을 목적으로 만든 프로젝트 입니다

Sticker 프로젝트 요구사항

  • 스티커 만들기 버튼을 누르면 스티커가 생성됩니다
  • 스티커가 겹쳐서 생기는 것을 방지하기 위해 계속 다른 위치에 생성합니다. 예를 들어, 항상 우, 하단 10px씩 이동하여 생성합니다
  • 생성된 스티커는 드래그로 원하는 위치로 이동할 수 있습니다
  • 스티커는 선택하면 항상 가장 위로 올라옵니다
  • 각 스티커에서 항목을 추가하는 버튼이 있습니다
  • 각 스티커에는 스티커를 삭제하는 버튼이 있습니다
  • 항목을 추가하면 스티커에 항목이 생성됩니다
  • 항목은 드래그하여 원하는 위치로 이동할 수 있습니다. 같은 스티커 내 뿐만 아니라 다른 스티커로도 이동이 가능합니다
  • 각 항목에는 항목을 삭제하는 버튼이 있습니다
    • 항목 삭제버튼에 대한 이벤트 핸들링을 해당 버튼에 등록하는 것이 아닌 항목에 등록하여 이벤트 위임방식으로 구현해 주세요
  • 생성되는 스티커의 배경색은 스티커 구분을 위해 랜덤으로 생성합니다. 다음의 임의의 색을 사용하면 보기 편한 색상이 지정됩니다
    • rgb(150 ~200, 150~200, 150~200)

1. HTML

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="./css/common.css" />
        <title>STICKER DRAGGABLE PROJECT</title>
    </head>
    <body>
        <button type="button" class="btn" id="sticker-add">스티커 추가</button>
        <script type="module" src="./js/app.js"></script>
    </body>
</html>
  • 스티커를 추가하는 버튼과 js파일만 존재
  • 디자인적인 요소는 최대한 배제하고 프로젝트를 진행한다

1-1. HTML Template

<div class="sticker-wrapper">
    <div class="sticker" style="top: 30px; left: 30px;">
        <div class="sticker-header">
            <h3>STICKER 1</h3>
            <div class="sticker-setting">
                <button type="button" class="btn" id="sticker-list-add">항목추가</button>
                <button type="button" class="btn" id="sticker-remove">스티커삭제</button>
            </div>
        </div>
        <div class="sticker-contents">
            <ul class="sticker-list">
                <li>
                    <div>목록1</div>
                    <button type="button" class="btn" id="sticker-list-remove">삭제</button>
                </li>
                <li>
                    <div>목록2</div>
                    <button type="button" class="btn" id="sticker-list-remove">삭제</button>
                </li>
            </ul>
        </div>
    </div>
</div>

2. CSS

* {
    margin: 0;
    padding: 0;
}

html, body {
    height: 100%;
    overflow: hidden;
    font-size: 12px;
}

.btn {
    padding: 3px;
    background-color: #fff;
    border: 1px solid #cecece;
}

.btn:hover {
    box-shadow: 0px 1px 2px -1px #999;
}

.btn:active {
    box-shadow: inset 0 1px 2px #999;
}

.sticker-wrapper {
    position: relative;
    height: 100%;
}

.sticker-wrapper .sticker {
    position: absolute;
    min-width: 150px;
    min-height: 100px;
    background-color: #bcf5e0;
    cursor: pointer;
}

.sticker-wrapper .sticker .sticker-header {
    padding: 5px;
}

.sticker-wrapper .sticker .sticker-header h3 {
    margin-bottom: 5px;
}

.sticker-wrapper .sticker .sticker-header .sticker-setting {
    display: flex;
    justify-content: center;
}

.sticker-wrapper .sticker .sticker-header .sticker-setting button + button {
    margin-left: 5px;
}

.sticker-wrapper .sticker .sticker-contents {
    padding: 0 5px 5px;
}

.sticker-wrapper .sticker .sticker-contents .sticker-list {
    list-style: none;
}

.sticker-wrapper .sticker .sticker-contents .sticker-list li {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    padding: 5px;
    background-color: #fff;
}

.sticker-wrapper .sticker .sticker-contents .sticker-list li + li {
    margin-top: 5px;
}

.sticker-wrapper .sticker .sticker-contents .sticker-list li > div {
    flex: auto;
}
  • 간단한 레이아웃 및 최소한의 디자인만을 적용한다

3. JS

3-1. app

import StickerLayout from "./stickerLayout.js";

window.addEventListener("DOMContentLoaded", (e) => {
    const stickerAdd = document.querySelector("#sticker-add");
    const stickerLayout = new StickerLayout({ parentEl: document.body });

    stickerAdd.addEventListener("click", (e) => {
        stickerLayout.addSticker();
    });
});
  • html에서 호출하는 기본 js
  • 스티커를 추가하기 위한 인스턴스 생성

3-2 stickerLayout

import Sticker from "./sticker.js";

export default class StickerLayout {
    constructor(options) {
        this.el = null;
        this.count = 1;
        this.stickerList = [];
        this.initialPositionX = 5;
        this.initialPositionY = 5;

        Object.assign(this, options);

        this.initElement();
        this.render();
    }

    initElement() {
        const element = document.createElement("div");
        element.classList.add("sticker-wrapper");
        element.addEventListener("removeSticker", this.handleClickRemoveSticker.bind(this));

        this.el = element;
    }

    render() {
        this.parentEl.appendChild(this.el);
    }

    addSticker() {
        const data = {
            id: `sticker_${crypto.randomUUID()}`,
            stickerCount: this.count++,
            initPosition: {
                initX: this.initialPositionX,
                initY: this.initialPositionY
            },
            parentEl: this.el,
            parentClientRect: this.el.getBoundingClientRect(),
            _self: this
        };
        const sticker = new Sticker(data);

        this.stickerList.push(sticker);

        // 아이템 추가 후 초기값에 10을 더해준다
        this.initialPositionX = this.initialPositionX + 10;
        this.initialPositionY = this.initialPositionY + 10;
    }

    removeSticker(id) {
        this.stickerList = this.stickerList.filter(sticker => sticker.id !== id);

        this.count--;
        this.initialPositionX = this.initialPositionX - 10;
        this.initialPositionY = this.initialPositionY - 10;
    }

    handleClickRemoveSticker(e) {
        const { id } = e.detail;

        this.removeSticker(id);
    }
}
  • 스티커들을 관리하기 위한 레이아웃 클래스
  • 스티커의 초기값(id, count, position 등) 설정

3-3 sticker

import StickerList from "./stickerList.js";

export default class Sticker {
    constructor(options) {
        this.el = null;
        this.itemList = [];
        this.listCount = 1;

        Object.assign(this, options);

        this.initElement();
        this.render(options.parentEl);
    }

    initElement() {
        const sticker = document.createElement("div");
        sticker.classList.add("sticker");
        sticker.id = this.id;

        const header = this.createHeader();
        const contents = this.createContents();

        sticker.appendChild(header);
        sticker.appendChild(contents);

        this.setStyle(sticker);

        // DRAG 이벤트 바인딩
        sticker.addEventListener("mousedown", this.dragStart.bind(this));
        sticker.addEventListener("mouseup", this.dragEnd.bind(this));

        // customEvent
        sticker.addEventListener("removeList", this.handleClickRemoveList.bind(this));

        this.el = sticker;
    }

    createHeader() {
        const header = document.createElement("div");
        header.classList.add("sticker-header");

        const headingTitle = document.createElement("h3");
        headingTitle.textContent = `STICKER ${this.stickerCount}`;

        const btnSettings = document.createElement("div");
        btnSettings.classList.add("sticker-setting");

        const addListBtn = document.createElement("button");
        addListBtn.classList.add("btn");
        addListBtn.id = "sticker-list-add";
        addListBtn.textContent = "항목추가";
        addListBtn.addEventListener("click", this.handleClickAdddList.bind(this));

        const removeSticker = document.createElement("button");
        removeSticker.classList.add("btn");
        removeSticker.id = "sticker-remove";
        removeSticker.textContent = "스티커삭제";
        removeSticker.addEventListener("click", this.handleClickRemoveSticker.bind(this, this.id));

        btnSettings.appendChild(addListBtn);
        btnSettings.appendChild(removeSticker);

        header.appendChild(headingTitle);
        header.appendChild(btnSettings);

        return header;
    }

    createContents() {
        const contents = document.createElement("div");
        contents.classList.add("sticker-contents");

        const list = document.createElement("ul");
        list.classList.add("sticker-list");

        contents.appendChild(list);

        return contents;
    }

    render(parent) {
        parent.append(this.el);
    }

    setStyle(target) {
        const bgColor = this.getBgColor();
        const { initX, initY } = this.initPosition;

        target.style.top = `${initY}px`;
        target.style.left = `${initX}px`;
        target.style.backgroundColor = `rgba(${bgColor}, 0.8)`;
    }

    getBgColor() {
        const result = [];

        for (let i = 0; i < 3; i++) {
            result.push(Math.floor(Math.random() * 255));
        }

        return result.join();
    }

    addList() {
        const data = {
            id: `sticker_${crypto.randomUUID()}`,
            stickerCount: this.stickerCount,
            listCount: this.listCount++,
            parentEl: this.el.querySelector(".sticker-list"),
            _self: this
        }

        const list = new StickerList(data);

        this.itemList.push(list);
    }

    removeList(id) {
        this.itemList = this.itemList.filter(item => item.id !== id);
    }

    // DRAG
    dragStart(e) {
        // 진행중

    dragMove(e) {
        // 진행중
    }

    dragEnd(e) {
        // 진행중
    }

    // HANDLER
    handleClickAdddList(e) {
        this.addList();
    }

    handleClickRemoveSticker(id) {
        const event = new CustomEvent("removeSticker", { bubbles: true, detail: { id } });

        this.parentEl.dispatchEvent(event);
        this.el.remove();
    }

    handleClickRemoveList(e) {
        const { id } = e.detail;

        this.removeList(id);
    }
}
  • 스티커 내부에 추가되는 목록들을 관리하기 위한 레이아웃 클래스
  • drag와 관련 된 로직 존재 - 스티커 이동
  • 상위 레이아웃에서 전달받은 위치값으로 스티커가 생성 될 때 겹치지 않도록 설정
  • 드래그 기능 구현중

3-4 stickerList

export default class StickerList {
    constructor(options) {
        this.el = null;

        Object.assign(this, options);

        this.initElement();
        this.render();
    }

    initElement() {
        const li = document.createElement("li");
        li.id = this.id;
        const div = document.createElement("div");
        div.textContent = `목록${this.stickerCount}-${this.listCount}`;

        const button = document.createElement("button");
        button.textContent = "삭제";
        button.addEventListener("click", this.handleClickRemove.bind(this, this.id));

        li.appendChild(div);
        li.appendChild(button);

        this.el = li;
    }

    render() {
        this.parentEl.appendChild(this.el);
    }

    handleClickRemove(id) {
        const event = new CustomEvent("removeList", { bubbles: true, detail: { id: id } });

        this.parentEl.dispatchEvent(event);
        this.el.remove();
    }
}
  • 스티커 내부에 들어가는 목록을 생성하는 클래스
  • drag와 관련 된 로직 추가예정 - 스티커간 목록 이동

view ex)

지속적으로 업데이트 예정

profile
잘하는것보다 꾸준히하는게 더 중요하다

0개의 댓글