Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user