티스토리 뷰

 when2Meet이라는 그룹 일정 조율 사이트를 리뉴얼하는 프로젝트에 참여하게 되었다. 해당 웹 사이트에서의 주요 기능 중 하나가 드래그 해서 되는 일정을 선택할 수 있도록 하는 건데, 그 부분이 핵심이라 팀장이 일주일 동안 공부하고, 간단하게 구현까지 해보라는 과제를 내주셨다! 그래서 이것저것 알아보니, 우리 프로젝트에 딱 알맞는 react-table-drag-select 라는 모듈이 있었으나 현재는 deprecated 되어 사용할 수 없었다. dragselect 라는 개인 개발자가 만든 모듈이나, 생명주기를 이용하여 구현을 하신 분도 있었다. 하지만, 리액트에서 생명주기 함수까지 사용하고 싶지는 않았고, 더 대중적인 모듈을 찾고자 했다. 그 와중에 발견한 한줄기 빛이 바로 react-selecto...! 

미리 보는 공부 결과 화면

 

 

🎯 사용법

개발자 공식 github에 있는 내용을 가져왔다.

바로 이해가 되는 부분은 번역만 해두었고, 잘 이해가 안가는 부분은 두번째 주석으로 남겨두었다.

// 개발자 공식 github에서 사용방법 번역 및 이해를 위한 추가 설명
import Selecto from "selecto";

const selecto = new Selecto({
    // selection element를 추가할 container
    container: document.body,
    // Selecto's root container (No transformed container. (default: null)
    rootContainer: null,
    // selection element를 드래그할 영역(default: container)
    dragContainer: Element,
    // 선택할 타깃. queryselector나 Element로 등록할 수 있다.
    selectableTargets: [".target", document.querySelector(".target2")],
    // 클릭에 따라 고를지 말지, (default: true)
		// 드래그가 아닌 클릭에도 선택될 수 있도록 할 것인가?
    selectByClick: true,
    // 타깃 안에서부터 고를 수 있는지 없는지, (default: true)
		// 타깃 안에서부터 드래그 할 수 있게 할 것인가?
    selectFromInside: true,
    // 선택 이후 선택된 타깃과 함께 다음 타깃을 고를지 말지, (타깃이 재선택 된다면 선택 취소)
		// 두번째 드래그 시작시 이전의 모든 선택이 초기화시키지 않고 유지할 것인가?
    continueSelect: false,
    // keydown과 keyup을 통해 다음 타깃 선택을 계속할 키 결정
		// 특정 키를 누르면서 드래그하면 선택이 초기화됨, 어떤 키로?
    toggleContinueSelect: "shift",
    // keydown keyup 이벤트를 위한 container
    keyContainer: window,
    // 얼마큼 객체가 드래그 영역에 포함되어야 선택된걸로 할건지(default: 100)
    hitRate: 100,
});

selecto.on("select", e => {
    e.added.forEach(el => {
        el.classList.add("selected");
    });
    e.removed.forEach(el => {
        el.classList.remove("selected");
    });
});

 

이번 프로젝트에서 쓰일 속성 값들을 미리 뽑아내기도 하고, 

dragContainer: {element}, // 이 속성을 타깃 자체만으로 해야 타깃 위에서만 드래그 가능.
selectByClick: {true},
selectFromInside: {true},
continueSelect: {true}
hitRate:{0},

 

 

💻 적용

한줄짜리 timeline과 일주일짜리 timeline을 만들어보았다...!

1) 한줄 짜리 timeline

// App.js

import * as React from "react";
import Selecto from "react-selecto";

export default function App() {
    const slots = [];

    for (let i = 0; i < 96; ++i) {
        slots.push(i);
    }
    return <div className="app">
        <div className="container">
            <Selecto
                dragContainer={".slot"}
                selectableTargets={[".selecto-area .slot"]}
                hitRate={0}
                selectByClick={true}
                selectFromInside={true}
                continueSelect={true}   
                ratio={0}
                onSelect={e => {
                    e.added.forEach(el => {
                        el.classList.add("selected");
                    });
                    e.removed.forEach(el => {
                        el.classList.remove("selected");
                    });
                }}
            />

            <div className="select-container">
                <div className="elements selecto-area" id="selecto1">
                    {slots.map(i => {
                        let sep = "";
                        if (i % 4 === 3) {
                            sep = " solid";
                        } else if (i % 4===1) {
                            sep = " dotted";
                        }
                            return <div className={"slot" + sep} key={i}></div>
                        
                }
                    )}
                    
                </div>
            </div>
            <div className="empty elements"></div>
        </div>
    </div>;
}

 

/* css */
html, body, #root {
    position: relative;
    margin: 0;
    padding: 0;
    height: 100%;
    color: #333;
    background: #fdfdfd;
}

.app {
    position: relative;
    min-height: 100%;
    padding: 10px 20px;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
}

.container {
    max-width: 800px;
}

body {
    background: #fff;
}


.slot {
    display: block;
    width: 45px;
    height: 10px;
    background: #eee;
    --color: #4af;
    border-right: 0.5px solid black;
    border-left: 0.5px solid black;
    transition: all ease 0.2s;

}

.solid {
    border-bottom: 0.5px solid black;
}

.dotted {
    border-bottom: 0.5px dotted black;
}

.elements {
    margin-top: 40px;
    border: 0.5px solid black;
    
}

.selecto-area .selected {
    color: #fff;
    background: var(--color);
}

.scroll {
    overflow: auto;
    padding-top: 10px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.infinite-viewer, .scroll {
    width: 100%;
    height: 300px;
    box-sizing: border-box;
}

.infinite-viewer .viewport {
    padding-top: 10px;
}

.empty.elements {
    border: none;
}

 

결과화면

 

 

2) 일주일 timeline

// App.js

import * as React from "react";
import Selecto from "react-selecto";

export default function App() {
    const slots = [];

    for (let i = 0; i < 672; ++i) {
        slots.push(i);
    }
    return <div className="app">
        <div className="container">
            <Selecto
                dragContainer={".slot"}
                selectableTargets={[".selecto-area .slot"]}
                hitRate={0}
                selectByClick={true}
                selectFromInside={true}
                continueSelect={true}   
                ratio={0}
                onSelect={e => {
                    e.added.forEach(el => {
                        el.classList.add("selected");
                    });
                    e.removed.forEach(el => {
                        el.classList.remove("selected");
                    });
                }}
            />

            <div className="select-container">
                <div className="elements selecto-area" id="selecto1">
                    {slots.map(i => {
                        let sep = ""
                            if (parseInt(i / 7) % 4 === 1) {
                                sep = "dotted";
                            } else if (parseInt(i / 7) % 4 === 3) {
                                sep = "solid";
                            }
                        return <div className={"slot "+sep} key={i}></div>
                    })}           
                </div>
            </div>
            <div className="empty elements"></div>
        </div>
    </div>;
}
/* CSS */
html, body, #root {
    position: relative;
    margin: 0;
    padding: 0;
    height: 100%;
    color: #333;
    background: #fdfdfd;
}

.app {
    position: relative;
    min-height: 100%;
    padding: 10px 20px;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    line-height: 0px;
}


body {
    background: #fff;
}


.slot {
    display: inline-block;
    width: 45px;
    height: 10px;
    background: #eee;
    --color: #4af;
    box-sizing: border-box;
    border-right: 0.5px solid black;
    border-left: 0.5px solid black;
    transition: all ease 0.2s;

}

.solid {
    border-bottom: 0.5px solid black;
}

.dotted {
    border-bottom: 0.5px dotted black;
}

.elements {
    width: 315px;
    margin-top: 40px;
    border: 0.5px solid black;
    
}

.selecto-area .selected {
    color: #fff;
    background: var(--color);
}

.scroll {
    overflow: auto;
    padding-top: 10px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.infinite-viewer, .scroll {
    width: 100%;
    height: 300px;
    box-sizing: border-box;
}

.infinite-viewer .viewport {
    padding-top: 10px;
}

.empty.elements {
    border: none;
}

 

결과화면

 

 

🎈 수정한 속성

 예제 코드를 기반으로 수정한 속성들은 다음과 같다.

  1. dragContainer => slot 바깥에서는 드래그가 먹히지 않도록 slot 자체를 드래그 영역으로 잡았다.
  2. hitrate => 영역이 조금만 드래그 영역에 들어가도 선택되도록 0으로 설정하였다.(낮을수록 민감해짐)
  3. 그 외 CSS 속성들.. => 가로에 7개의 slot만이 들어갈 수 있도록, slot과 전체 div width속성을 고정.

 

 

 현재 일주일짜리 timeline은 CSS를 위해 가로 너비가 7일이라는 가정 하에 관계식을 세웠지만, 실제 프로젝트에 적용하기 위해서라도 더 일반적으로 나타낼 수 있는 방법을 생각해봐야할 것 같다!

 

Comments