415 lines
11 KiB
JavaScript
415 lines
11 KiB
JavaScript
/**
|
|
* DOM 조작 유틸리티 모듈 (DOMUtils.js)
|
|
* ========================================
|
|
* document.querySelector 대신 쓰는 간편 함수들입니다.
|
|
*
|
|
* [초보자용 사용 예]
|
|
* DOMUtils.$('.my-class') → 첫 번째 요소 (querySelector)
|
|
* DOMUtils.$$('.my-class') → 모든 요소 (querySelectorAll)
|
|
* DOMUtils.fadeIn(el, 300) → 페이드 인 (300ms)
|
|
* DOMUtils.fadeOut(el, 300) → 페이드 아웃
|
|
* DOMUtils.delegate(parent, 'click', '.btn', handler) → 동적 요소에 이벤트 위임
|
|
* DOMUtils.htmlToElement('<div>') → HTML 문자열 → Element
|
|
*
|
|
* @module DOMUtils
|
|
*/
|
|
class DOMUtils {
|
|
/**
|
|
* 요소 선택 (단일)
|
|
* @param {string} selector - CSS 선택자
|
|
* @param {Element} parent - 부모 요소
|
|
* @returns {Element|null}
|
|
*/
|
|
static $(selector, parent = document) {
|
|
return parent.querySelector(selector);
|
|
}
|
|
|
|
/**
|
|
* 요소 선택 (다중)
|
|
* @param {string} selector - CSS 선택자
|
|
* @param {Element} parent - 부모 요소
|
|
* @returns {NodeList}
|
|
*/
|
|
static $$(selector, parent = document) {
|
|
return parent.querySelectorAll(selector);
|
|
}
|
|
|
|
/**
|
|
* 요소 생성
|
|
* @param {string} tag - 태그명
|
|
* @param {Object} attrs - 속성 객체
|
|
* @param {string} content - 내용
|
|
* @returns {Element}
|
|
*/
|
|
static createElement(tag, attrs = {}, content = "") {
|
|
const element = document.createElement(tag);
|
|
|
|
Object.entries(attrs).forEach(([key, value]) => {
|
|
if (key === "class" || key === "className") {
|
|
element.className = value;
|
|
} else if (key === "style" && typeof value === "object") {
|
|
Object.assign(element.style, value);
|
|
} else if (key.startsWith("data-")) {
|
|
element.setAttribute(key, value);
|
|
} else {
|
|
element[key] = value;
|
|
}
|
|
});
|
|
|
|
if (content) {
|
|
if (typeof content === "string") {
|
|
element.innerHTML = content;
|
|
} else if (content instanceof Node) {
|
|
element.appendChild(content);
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* 클래스 토글
|
|
* @param {Element} element - 대상 요소
|
|
* @param {string} className - 클래스명
|
|
* @param {boolean} force - 강제 적용 여부
|
|
*/
|
|
static toggleClass(element, className, force) {
|
|
if (!element) return;
|
|
if (force !== undefined) {
|
|
element.classList.toggle(className, force);
|
|
} else {
|
|
element.classList.toggle(className);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 여러 클래스 추가
|
|
* @param {Element} element - 대상 요소
|
|
* @param {...string} classNames - 클래스명들
|
|
*/
|
|
static addClasses(element, ...classNames) {
|
|
if (!element) return;
|
|
element.classList.add(...classNames);
|
|
}
|
|
|
|
/**
|
|
* 여러 클래스 제거
|
|
* @param {Element} element - 대상 요소
|
|
* @param {...string} classNames - 클래스명들
|
|
*/
|
|
static removeClasses(element, ...classNames) {
|
|
if (!element) return;
|
|
element.classList.remove(...classNames);
|
|
}
|
|
|
|
/**
|
|
* 요소의 위치 정보 가져오기
|
|
* @param {Element} element - 대상 요소
|
|
* @returns {Object}
|
|
*/
|
|
static getPosition(element) {
|
|
if (!element) return null;
|
|
const rect = element.getBoundingClientRect();
|
|
return {
|
|
top: rect.top,
|
|
left: rect.left,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
x: rect.x,
|
|
y: rect.y,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 요소를 퍼센트 위치로 설정
|
|
* @param {Element} element - 대상 요소
|
|
* @param {number} x - X 위치 (%)
|
|
* @param {number} y - Y 위치 (%)
|
|
* @param {string} transform - 추가 transform
|
|
*/
|
|
static setPercentPosition(element, x, y, transform = "translate(-50%, -50%)") {
|
|
if (!element) return;
|
|
element.style.position = "absolute";
|
|
element.style.left = `${x}%`;
|
|
element.style.top = `${y}%`;
|
|
if (transform) {
|
|
element.style.transform = transform;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 요소 페이드 인
|
|
* @param {Element} element - 대상 요소
|
|
* @param {number} duration - 지속 시간 (ms)
|
|
* @returns {Promise}
|
|
*/
|
|
static fadeIn(element, duration = 300) {
|
|
if (!element) return Promise.resolve();
|
|
|
|
return new Promise((resolve) => {
|
|
// 기존 display 값 저장 (grid, flex 등 유지)
|
|
const originalDisplay = element.style.display || window.getComputedStyle(element).display;
|
|
const isGridOrFlex = originalDisplay === "grid" || originalDisplay === "flex" ||
|
|
originalDisplay.includes("grid") || originalDisplay.includes("flex");
|
|
|
|
element.style.opacity = "0";
|
|
// grid/flex인 경우 display를 설정하지 않음
|
|
if (!isGridOrFlex) {
|
|
element.style.display = "block";
|
|
}
|
|
element.style.transition = `opacity ${duration}ms ease-in-out`;
|
|
|
|
setTimeout(() => {
|
|
element.style.opacity = "1";
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
element.style.transition = "";
|
|
// grid/flex인 경우 display 스타일 제거
|
|
if (isGridOrFlex) {
|
|
element.style.display = "";
|
|
}
|
|
resolve();
|
|
}, duration);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 요소 페이드 아웃
|
|
* @param {Element} element - 대상 요소
|
|
* @param {number} duration - 지속 시간 (ms)
|
|
* @returns {Promise}
|
|
*/
|
|
static fadeOut(element, duration = 300) {
|
|
if (!element) return Promise.resolve();
|
|
|
|
return new Promise((resolve) => {
|
|
element.style.opacity = "1";
|
|
element.style.transition = `opacity ${duration}ms ease-in-out`;
|
|
|
|
setTimeout(() => {
|
|
element.style.opacity = "0";
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
element.style.display = "none";
|
|
element.style.transition = "";
|
|
element.style.opacity = ""; // 재오픈 시 opacity 초기화
|
|
resolve();
|
|
}, duration);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 요소 슬라이드 다운
|
|
* @param {Element} element - 대상 요소
|
|
* @param {number} duration - 지속 시간 (ms)
|
|
* @returns {Promise}
|
|
*/
|
|
static slideDown(element, duration = 300) {
|
|
if (!element) return Promise.resolve();
|
|
|
|
return new Promise((resolve) => {
|
|
element.style.display = "block";
|
|
const height = element.scrollHeight;
|
|
element.style.height = "0";
|
|
element.style.overflow = "hidden";
|
|
element.style.transition = `height ${duration}ms ease-in-out`;
|
|
|
|
setTimeout(() => {
|
|
element.style.height = `${height}px`;
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
element.style.height = "";
|
|
element.style.overflow = "";
|
|
element.style.transition = "";
|
|
resolve();
|
|
}, duration);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 요소 슬라이드 업
|
|
* @param {Element} element - 대상 요소
|
|
* @param {number} duration - 지속 시간 (ms)
|
|
* @returns {Promise}
|
|
*/
|
|
static slideUp(element, duration = 300) {
|
|
if (!element) return Promise.resolve();
|
|
|
|
return new Promise((resolve) => {
|
|
const height = element.scrollHeight;
|
|
element.style.height = `${height}px`;
|
|
element.style.overflow = "hidden";
|
|
element.style.transition = `height ${duration}ms ease-in-out`;
|
|
|
|
setTimeout(() => {
|
|
element.style.height = "0";
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
element.style.display = "none";
|
|
element.style.height = "";
|
|
element.style.overflow = "";
|
|
element.style.transition = "";
|
|
resolve();
|
|
}, duration);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 이벤트 위임
|
|
* @param {Element} parent - 부모 요소
|
|
* @param {string} eventType - 이벤트 타입
|
|
* @param {string} selector - 자식 선택자
|
|
* @param {Function} handler - 핸들러 함수
|
|
*/
|
|
static delegate(parent, eventType, selector, handler) {
|
|
if (!parent) return;
|
|
|
|
parent.addEventListener(eventType, (event) => {
|
|
const target = event.target.closest(selector);
|
|
if (target && parent.contains(target)) {
|
|
handler.call(target, event);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* HTML 문자열을 요소로 변환
|
|
* @param {string} html - HTML 문자열
|
|
* @returns {Element}
|
|
*/
|
|
static htmlToElement(html) {
|
|
const template = document.createElement("template");
|
|
template.innerHTML = html.trim();
|
|
return template.content.firstElementChild;
|
|
}
|
|
|
|
/**
|
|
* HTML 문자열을 요소 배열로 변환
|
|
* @param {string} html - HTML 문자열
|
|
* @returns {Array}
|
|
*/
|
|
static htmlToElements(html) {
|
|
const template = document.createElement("template");
|
|
template.innerHTML = html.trim();
|
|
return Array.from(template.content.children);
|
|
}
|
|
|
|
/**
|
|
* 요소가 뷰포트에 있는지 확인
|
|
* @param {Element} element - 대상 요소
|
|
* @returns {boolean}
|
|
*/
|
|
static isInViewport(element) {
|
|
if (!element) return false;
|
|
const rect = element.getBoundingClientRect();
|
|
return (
|
|
rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 부드러운 스크롤
|
|
* @param {Element|string} target - 대상 요소 또는 선택자
|
|
* @param {Object} options - 옵션
|
|
*/
|
|
static smoothScroll(target, options = {}) {
|
|
const element = typeof target === "string" ? this.$(target) : target;
|
|
if (!element) return;
|
|
|
|
const defaultOptions = {
|
|
behavior: "smooth",
|
|
block: "start",
|
|
inline: "nearest",
|
|
};
|
|
|
|
element.scrollIntoView({ ...defaultOptions, ...options });
|
|
}
|
|
|
|
/**
|
|
* 전체 화면 토글
|
|
* @param {Element} element - 대상 요소
|
|
*/
|
|
static toggleFullscreen(element = document.documentElement) {
|
|
if (!document.fullscreenElement) {
|
|
element.requestFullscreen?.() ||
|
|
element.webkitRequestFullscreen?.() ||
|
|
element.msRequestFullscreen?.();
|
|
} else {
|
|
document.exitFullscreen?.() ||
|
|
document.webkitExitFullscreen?.() ||
|
|
document.msExitFullscreen?.();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 클립보드에 복사
|
|
* @param {string} text - 복사할 텍스트
|
|
* @returns {Promise}
|
|
*/
|
|
static async copyToClipboard(text) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} catch (err) {
|
|
console.error("Failed to copy:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 요소의 스타일 가져오기
|
|
* @param {Element} element - 대상 요소
|
|
* @param {string} property - CSS 속성
|
|
* @returns {string}
|
|
*/
|
|
static getStyle(element, property) {
|
|
if (!element) return null;
|
|
return window.getComputedStyle(element).getPropertyValue(property);
|
|
}
|
|
|
|
/**
|
|
* 여러 스타일 설정
|
|
* @param {Element} element - 대상 요소
|
|
* @param {Object} styles - 스타일 객체
|
|
*/
|
|
static setStyles(element, styles) {
|
|
if (!element || !styles) return;
|
|
Object.assign(element.style, styles);
|
|
}
|
|
|
|
/**
|
|
* 요소 제거
|
|
* @param {Element} element - 대상 요소
|
|
*/
|
|
static remove(element) {
|
|
if (element && element.parentNode) {
|
|
element.parentNode.removeChild(element);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 요소 내용 비우기
|
|
* @param {Element} element - 대상 요소
|
|
*/
|
|
static empty(element) {
|
|
if (!element) return;
|
|
while (element.firstChild) {
|
|
element.removeChild(element.firstChild);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ES6 모듈 내보내기
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = DOMUtils;
|
|
}
|