gojongwon-circular-progress-bar
Version:
A lightweight, customizable circular progress bar component for web applications
380 lines (379 loc) • 13.8 kB
JavaScript
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