드로그 앤 드롭 구현을 위한 인터페이스 활용하기
// 1
interface Draggable {
dragStartHandler(event: DragEvent): void;
dragEndHandler(event: DragEvent): void;
}
...
class ProjectItem
extends Component<HTMLUListElement, HTMLLIElement>
implements Draggable // 2
{
private project: Project;
get persons() {
if (this.project.people === 1) {
return "1 person";
} else {
return `${this.project.people} persons`;
}
}
constructor(hostId: string, project: Project) {
super("single-project", hostId, false, project.id);
this.project = project;
this.configure();
this.renderContent();
}
// 3
@autobind
dragStartHandler(event: DragEvent) {
console.log(event);
}
dragEndHandler(_: DragEvent) {
console.log("DragEnd");
}
// 4
configure() {
this.element.addEventListener("dragstart", this.dragStartHandler);
this.element.addEventListener("dragend", this.dragEndHandler);
}
renderContent() {
this.element.querySelector("h2")!.textContent = this.project.title;
this.element.querySelector("h3")!.textContent = this.persons + " assigned";
this.element.querySelector("p")!.textContent = this.project.description;
}
}
- 드래깅 가능한 요소를 렌더링 하는 클래스에 더하는 인터페이스를 만듭니다.
- 아이템 클래스와 인터페이스를 연결합니다.
- 이벤트리스너가 this가 되지 않기 위해 예전에도 만든 데코레이터를 이용합니다.
- 렌더링 요소에 접근해 이벤트를 추가합니다.
<template id="single-project">
<li draggable="true"> // 여기
<h2></h2>
<h3></h3>
<p></p>
</li>
</template>
html 파일에서 드래그할 대상의 속성을 설정해야 합니다.
그럼 다음과 같이 드래그할 대상을 잡고 내릴 수 있게 됩니다.
드래그 이벤트 및 UI의 현재 상태 반영하기
// 1
interface DragTarget {
dragOverHandler(event: DragEvent): void;
dropHandler(event: DragEvent): void;
dragLeaveHandler(event: DragEvent): void;
}
...
class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget // 2
{
assignedProjects: Project[];
constructor(private type: "active" | "finished") {
super("project-list", "app", false, `${type}-projects`);
this.assignedProjects = [];
this.configure();
this.renderContent();
}
// 3
@autobind
dragOverHandler(_: DragEvent) {
// 5
const listEl = this.element.querySelector('ul')!;
listEl.classList.add('droppable');
}
@autobind
dragLeaveHandler(_: DragEvent) {
// 6
const listEl = this.element.querySelector('ul')!;
listEl.classList.remove('droppable');
}
configure() {
this.element.addEventListener('dragover', this.dragOverHandler);// 4
this.element.addEventListener('dragleave', this.dragLeaveHandler);
projectState.addListener((projects: Project[]) => {
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
}
return prj.status === ProjectStatus.Finished;
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
}
renderContent() {
const listId = `${this.type}-projects-list`;
this.element.querySelector("ul")!.id = listId;
this.element.querySelector("h2")!.textContent =
this.type.toUpperCase() + " PROJECTS";
}
private renderProjects() {
const listEl = document.getElementById(
`${this.type}-projects-list`
)! as HTMLUListElement;
listEl.innerHTML = "";
for (const prjItem of this.assignedProjects) {
new ProjectItem(this.element.querySelector("ul")!.id, prjItem);
}
}
}
- 드래그와 드롭을 할 클래스에 인터페이스를 만듭니다.
- 리스트 클래스에 인터페이스를 연결합니다.
- 이벤트리스너가 this가 되지 않기 위해 예전에도 만든 데코레이터를 이용합니다.
- 렌더링 요소에 접근해 이벤트를 추가합니다.
- 드래그할 대상이 머무는 공간에 클래스 이름을 추가해 주는 코드입니다.
- 드래그할 대상이 떠난 공간에 클래스 이름을 삭제하는 코드입니다.
5-6번은 다음과 같이 css 파일이 들어있기 때문에 구성하였습니다.
.droppable {
background: #ffe3ee;
}
#finished-projects .droppable {
background: #d6e1ff;
}
다음과 같이 드롭이 가능한 장소에 색이 추가되어 더 알아보기 쉽게 합니다.
드롭할 수 있는 영역 추가하기
class ProjectItem
extends Component<HTMLUListElement, HTMLLIElement>
implements Draggable
{
...
@autobind
dragStartHandler(event: DragEvent) {
event.dataTransfer!.setData("text/plain", this.project.id); // 1
event.dataTransfer!.effectAllowed = "move"; // 2
}
dragEndHandler(_: DragEvent) {
console.log("DragEnd");
}
configure() {
this.element.addEventListener("dragstart", this.dragStartHandler);
this.element.addEventListener("dragend", this.dragEndHandler);
}
...
}
}
class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget
{
...
@autobind
dragOverHandler(event: DragEvent) {
// 3
if (event.dataTransfer && event.dataTransfer.types[0] === "text/plain") {
event.preventDefault(); // 4
const listEl = this.element.querySelector("ul")!;
listEl.classList.add("droppable");
}
}
@autobind
dragLeaveHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.remove("droppable");
}
// 5
dropHandler(event: DragEvent) {
console.log(event.dataTransfer!.getData('text/plain'));
}
configure() {
this.element.addEventListener("dragover", this.dragOverHandler);
this.element.addEventListener("dragleave", this.dragLeaveHandler);
this.element.addEventListener("drop", this.dropHandler);
projectState.addListener((projects: Project[]) => {
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
}
return prj.status === ProjectStatus.Finished;
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
}
...
}
- 드래그 이벤트의 데이터 전송 프로퍼티이고 setData의 첫 번째 인수는 데이터 포맷의 식별자, 두 번째 인수는 데이터입니다.
- 커서의 모양을 조절하고 브라우저에 의도를 더 잘 알려줄 수 있습니다.
- 드롭이 가능한 영역에만 데이터 전송을 하게 세운 데이터 포맷을 조건으로 연결합니다.
- 자바스크립트 드래그 앤 드롭 이벤트의 디폴트는 드롭을 허용하지 않기 때문에 그 디폴트를 막아줘야 합니다.
- 커서를 드롭했을 때 데이터를 전달하는 함수입니다.
위와 같이 드롭했을 때 콘솔창에 어떤 숫자가 찍히는 걸 볼 수 있습니다.
그건 바로 랜덤으로 생성한 id였고 잘 넘어왔습니다!
드로그 앤 드롭 마무리하기
class ProjectState extends State<Project> {
private projects: Project[] = [];
private static instance: ProjectState;
private constructor() {
super();
}
static getInstance() {
if (this.instance) {
return this.instance;
}
this.instance = new ProjectState();
return this.instance;
}
addProject(title: string, description: string, numOfPeople: number) {
const newProject = new Project(
Math.random().toString(),
title,
description,
numOfPeople,
ProjectStatus.Active
);
this.projects.push(newProject);
this.updateListeners(); // 2
}
// 1
moveProject(projectId: string, newStatus: ProjectStatus) {
const project = this.projects.find((prj) => prj.id === projectId);
if (project && project.status !== newStatus) { // 4
project.status = newStatus;
this.updateListeners(); // 2
}
}
// 2
private updateListeners() {
for (const listenerFn of this.listeners) {
listenerFn(this.projects.slice());
}
}
}
...
class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget
{
...
@autobind
dragOverHandler(event: DragEvent) {
if (event.dataTransfer && event.dataTransfer.types[0] === "text/plain") {
event.preventDefault();
const listEl = this.element.querySelector("ul")!;
listEl.classList.add("droppable");
}
}
@autobind
dragLeaveHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.remove("droppable");
}
@autobind
dropHandler(event: DragEvent) {
const prjId = event.dataTransfer!.getData("text/plain");
// 3
projectState.moveProject(
prjId,
this.type === "active" ? ProjectStatus.Active : ProjectStatus.Finished
);
}
configure() {
this.element.addEventListener("dragover", this.dragOverHandler);
this.element.addEventListener("dragleave", this.dragLeaveHandler);
this.element.addEventListener("drop", this.dropHandler);
projectState.addListener((projects: Project[]) => {
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
}
return prj.status === ProjectStatus.Finished;
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
}
...
}
...
- 프로젝트 상태 클래스에 변경 메서드를 추가합니다.
- 상태를 갱신하는 새로운 메서드가 생겼기에 기존에 생성자 함수에 있던 리스너 갱신 함수를 따로 빼고 생성자와 변경 메서드에 추가합니다.
- 추가한 메서드를 연결합니다.
- 조건을 추가하여 같은 곳에 드롭할 경우 리렌더링을 방지합니다.
짜잔...
드래그 앤 드롭 API 레퍼런스
https://developer.mozilla.org/ko/docs/Web/API/HTML_Drag_and_Drop_API
HTML 드래그 앤 드롭 API - Web API | MDN
HTML 드래그 앤 드롭 인터페이스는 파이어폭스와 다른 브라우저에서 어플리케이션이 드래그 앤 드롭 기능을 사용하게 해줍니다. 이 기능을 이용해 사용자는 draggable 요소를 마우스로 선택해 droppa
developer.mozilla.org
'TYPESCRIPT' 카테고리의 다른 글
웹팩(Webpack) 사용하기 (6) | 2023.04.07 |
---|---|
모듈과 네임스페이스 (6) | 2023.04.06 |
연습 프로젝트 3 (6) | 2023.04.03 |
연습 프로젝트 2 (13) | 2023.03.31 |
연습 프로젝트 1 (6) | 2023.03.30 |