UNPKG

gojongwon-circular-progress-bar

Version:

A lightweight, customizable circular progress bar component for web applications

380 lines (379 loc) 13.8 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); class CircularProgressBar { // 현재 SVG 크기 /** * CircularProgressBar 생성자 * @param container - 프로그레스 바를 렌더링할 HTML 요소 * @param options - 프로그레스 바 설정 옵션 */ constructor(container, options) { __publicField(this, "container"); // 프로그레스 바가 렌더링될 컨테이너 __publicField(this, "options"); // 설정 옵션 __publicField(this, "svg"); // SVG 요소 __publicField(this, "gauge"); // 게이지 원 (진행률 표시) __publicField(this, "trail"); // 트레일 원 (배경) __publicField(this, "text"); // 텍스트 요소 __publicField(this, "resizeObserver"); // 컨테이너 크기 변화 감지용 __publicField(this, "currentSize", 0); this.container = container; this.options = { responsive: false, gaugeWidth: 12, gaugeColor: "#2196f3", gaugeType: "round", trailWidth: 12, trailColor: "#e0e0e0", textColor: "#333", textSize: 24, textFont: "Arial", animate: true, duration: 800, disableInlineStyles: false, ...options }; if (this.options.responsive && !this.options.size) { this.options.size = this.getContainerSize(); } this.initialize(); this.render(); if (this.options.responsive) { this.setupResizeObserver(); } } /** * 색상 값을 해석합니다. * 문자열이면 그대로 반환하고, 함수면 현재 진행률을 기반으로 색상을 계산합니다. * @param colorValue - 색상 값 (문자열 또는 함수) * @param progress - 현재 진행률 (0~1) * @returns 해석된 색상 문자열 */ resolveColor(colorValue, progress) { if (typeof colorValue === "function") { return colorValue(progress); } return colorValue; } /** * SVG 요소와 원형 프로그레스 바를 초기화합니다. * 중심은 같고 너비의 중앙만 같도록 반지름을 조정합니다. */ initialize() { if (!this.options.size) { this.options.size = this.getContainerSize(); } this.currentSize = this.options.size; this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.svg.setAttribute("width", this.options.size.toString()); this.svg.setAttribute("height", this.options.size.toString()); this.svg.setAttribute("viewBox", `0 0 ${this.options.size} ${this.options.size}`); if (this.options.className) { this.svg.setAttribute("class", this.options.className); } this.svg.style.transform = "rotate(-90deg)"; const centerX = this.options.size / 2; const centerY = this.options.size / 2; const maxStrokeWidth = Math.max(this.options.gaugeWidth, this.options.trailWidth); const baseRadius = (this.options.size - maxStrokeWidth) / 2; const trailRadius = baseRadius; const gaugeRadius = baseRadius; this.trail = document.createElementNS("http://www.w3.org/2000/svg", "circle"); this.trail.setAttribute("cx", centerX.toString()); this.trail.setAttribute("cy", centerY.toString()); this.trail.setAttribute("r", trailRadius.toString()); this.trail.setAttribute("fill", "none"); if (this.options.trailClassName) { this.trail.setAttribute("class", this.options.trailClassName); } if (!this.options.disableInlineStyles) { this.trail.setAttribute("stroke", this.resolveColor(this.options.trailColor, 0)); this.trail.setAttribute("stroke-width", this.options.trailWidth.toString()); } this.gauge = document.createElementNS("http://www.w3.org/2000/svg", "circle"); this.gauge.setAttribute("cx", centerX.toString()); this.gauge.setAttribute("cy", centerY.toString()); this.gauge.setAttribute("r", gaugeRadius.toString()); this.gauge.setAttribute("fill", "none"); if (this.options.gaugeClassName) { this.gauge.setAttribute("class", this.options.gaugeClassName); } if (!this.options.disableInlineStyles) { this.gauge.setAttribute("stroke", this.resolveColor(this.options.gaugeColor, 0)); this.gauge.setAttribute("stroke-width", this.options.gaugeWidth.toString()); this.gauge.setAttribute( "stroke-linecap", this.options.gaugeType === "round" ? "round" : "butt" ); } const circumference = 2 * Math.PI * gaugeRadius; this.gauge.style.strokeDasharray = circumference.toString(); this.gauge.style.strokeDashoffset = circumference.toString(); const textGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); textGroup.setAttribute( "transform", `translate(${this.options.size / 2}, ${this.options.size / 2}) rotate(90)` ); this.text = document.createElementNS("http://www.w3.org/2000/svg", "text"); if (this.options.textClassName) { this.text.setAttribute("class", this.options.textClassName); } if (!this.options.disableInlineStyles) { this.text.setAttribute("fill", this.options.textColor); this.text.setAttribute("font-size", this.options.textSize.toString()); this.text.setAttribute("font-family", this.options.textFont); this.text.setAttribute("text-anchor", "middle"); this.text.setAttribute("dominant-baseline", "middle"); } this.text.setAttribute("x", "0"); this.text.setAttribute("y", "0"); textGroup.appendChild(this.text); this.svg.appendChild(this.trail); this.svg.appendChild(this.gauge); this.svg.appendChild(textGroup); this.container.innerHTML = ""; this.container.appendChild(this.svg); } /** * 현재 값에 따라 프로그레스 바를 렌더링합니다. * stroke-dashoffset을 조정하여 진행률을 표시합니다. */ render() { const maxStrokeWidth = Math.max(this.options.gaugeWidth, this.options.trailWidth); const baseRadius = (this.currentSize - maxStrokeWidth) / 2; const gaugeRadius = baseRadius; const circumference = 2 * Math.PI * gaugeRadius; const progress = Math.min(Math.max(this.options.value / this.options.maxValue, 0), 1); const dashLength = circumference * progress; if (!this.options.disableInlineStyles) { if (typeof this.options.gaugeColor === "function") { this.gauge.setAttribute("stroke", this.resolveColor(this.options.gaugeColor, progress)); } if (typeof this.options.trailColor === "function") { this.trail.setAttribute("stroke", this.resolveColor(this.options.trailColor, progress)); } } this.gauge.style.strokeDasharray = circumference.toString(); this.gauge.style.strokeDashoffset = (circumference - dashLength).toString(); if (this.options.animate && this.options.duration > 0) { this.gauge.style.transition = `stroke-dashoffset ${this.options.duration}ms ease-in-out`; } else { this.gauge.style.transition = "none"; } const displayText = this.options.text ?? `${Math.round(progress * 100)}%`; this.text.textContent = displayText; } /** * 프로그레스 바의 값을 설정합니다. * @param value - 새로운 값 (0 ~ maxValue 사이) */ setValue(value) { this.options.value = Math.min(Math.max(value, 0), this.options.maxValue); this.render(); } /** * 현재 값을 반환합니다. * @returns 현재 값 */ getValue() { return this.options.value; } /** * 컨테이너의 크기를 가져옵니다. * @returns 컨테이너의 최소 크기 (가로, 세로 중 작은 값) */ getContainerSize() { const rect = this.container.getBoundingClientRect(); return Math.min(rect.width, rect.height); } /** * ResizeObserver를 설정하여 컨테이너 크기 변화를 감지합니다. */ setupResizeObserver() { if (typeof ResizeObserver !== "undefined") { this.resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const newSize = Math.min(entry.contentRect.width, entry.contentRect.height); if (newSize !== this.currentSize && newSize > 0) { this.resize(newSize); } } }); this.resizeObserver.observe(this.container); } } /** * SVG 크기를 조정합니다. * @param newSize - 새로운 크기 */ resize(newSize) { this.currentSize = newSize; this.options.size = newSize; this.svg.setAttribute("width", newSize.toString()); this.svg.setAttribute("height", newSize.toString()); this.svg.setAttribute("viewBox", `0 0 ${newSize} ${newSize}`); const maxStrokeWidth = Math.max(this.options.gaugeWidth, this.options.trailWidth); const baseRadius = (newSize - maxStrokeWidth) / 2; const centerX = newSize / 2; const centerY = newSize / 2; const trailRadius = baseRadius; const gaugeRadius = baseRadius; this.trail.setAttribute("cx", centerX.toString()); this.trail.setAttribute("cy", centerY.toString()); this.trail.setAttribute("r", trailRadius.toString()); this.gauge.setAttribute("cx", centerX.toString()); this.gauge.setAttribute("cy", centerY.toString()); this.gauge.setAttribute("r", gaugeRadius.toString()); const circumference = 2 * Math.PI * gaugeRadius; const progress = Math.min(Math.max(this.options.value / this.options.maxValue, 0), 1); const dashLength = circumference * progress; this.gauge.style.strokeDasharray = circumference.toString(); this.gauge.style.strokeDashoffset = (circumference - dashLength).toString(); this.gauge.style.transition = "none"; const textGroup = this.svg.querySelector("g"); if (textGroup) { textGroup.setAttribute("transform", `translate(${centerX}, ${centerY}) rotate(90)`); } setTimeout(() => { if (this.options.animate && this.options.duration > 0) { this.gauge.style.transition = `stroke-dashoffset ${this.options.duration}ms ease-in-out`; } }, 0); } /** * 인스턴스를 정리합니다. * ResizeObserver를 해제합니다. */ destroy() { if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = void 0; } } /** * 게이지 색상을 동적으로 업데이트합니다. * @param color - 새로운 색상 (문자열 또는 함수) */ setGaugeColor(color) { this.options.gaugeColor = color; const progress = Math.min(Math.max(this.options.value / this.options.maxValue, 0), 1); this.gauge.setAttribute("stroke", this.resolveColor(color, progress)); } /** * 트레일 색상을 동적으로 업데이트합니다. * @param color - 새로운 색상 (문자열 또는 함수) */ setTrailColor(color) { this.options.trailColor = color; const progress = Math.min(Math.max(this.options.value / this.options.maxValue, 0), 1); this.trail.setAttribute("stroke", this.resolveColor(color, progress)); } /** * 현재 게이지 색상을 반환합니다. * @returns 현재 게이지 색상 설정 */ getGaugeColor() { return this.options.gaugeColor; } /** * 현재 트레일 색상을 반환합니다. * @returns 현재 트레일 색상 설정 */ getTrailColor() { return this.options.trailColor; } /** * SVG 요소에 CSS 클래스를 설정합니다. * @param className - CSS 클래스명 */ setClassName(className) { this.options.className = className; this.svg.setAttribute("class", className); } /** * 게이지 원에 CSS 클래스를 설정합니다. * @param className - CSS 클래스명 */ setGaugeClassName(className) { this.options.gaugeClassName = className; this.gauge.setAttribute("class", className); } /** * 트레일 원에 CSS 클래스를 설정합니다. * @param className - CSS 클래스명 */ setTrailClassName(className) { this.options.trailClassName = className; this.trail.setAttribute("class", className); } /** * 텍스트에 CSS 클래스를 설정합니다. * @param className - CSS 클래스명 */ setTextClassName(className) { this.options.textClassName = className; this.text.setAttribute("class", className); } /** * 인라인 스타일 활성화/비활성화를 설정합니다. * @param disable - true면 인라인 스타일 비활성화, false면 활성화 */ setDisableInlineStyles(disable) { this.options.disableInlineStyles = disable; if (!disable) { this.render(); } } /** * 현재 CSS 클래스 설정을 반환합니다. * @returns CSS 클래스 설정 객체 */ getClassNames() { return { className: this.options.className, gaugeClassName: this.options.gaugeClassName, trailClassName: this.options.trailClassName, textClassName: this.options.textClassName }; } /** * SVG 요소를 반환합니다. * 외부에서 직접 스타일링할 때 사용합니다. * @returns SVG 요소 */ getSVGElement() { return this.svg; } /** * 게이지 원 요소를 반환합니다. * @returns 게이지 원 요소 */ getGaugeElement() { return this.gauge; } /** * 트레일 원 요소를 반환합니다. * @returns 트레일 원 요소 */ getTrailElement() { return this.trail; } /** * 텍스트 요소를 반환합니다. * @returns 텍스트 요소 */ getTextElement() { return this.text; } } export { CircularProgressBar }; //# sourceMappingURL=CircularProgressBar.js.map