DEV/javascript

Javascript 커스텀 로딩(Custom Modal Loading)

석봉 2022. 10. 31. 17:33

커스텀 모달 로딩 (완성한 모습)

중국 개발자가 만든 커스텀 로딩(모달 로딩) 이다. 

 

단순하게 로딩만 사용하고 싶다면 최하단 Custom Loading (Modal)에 있는 js와 css로만 구성하면 된다.

 

사용은 다음과 같이 사용.

 

test.js

// Loading 시작
let loading = new Loading();
// 아래 옵션변경과 함께 사용 가능
// let loading = new Loading(getLoadingSet());

// Loading 종료
loading.out();

/******************************************************
 * 로딩 설정값 반환 (옵션 변경)
 * @param {string} title 
 ******************************************************/
function getLoadingSet(title) {
    return {
        title:                  title || '데이터를 불러오는 중입니다.',
        titleColor:             'gray',
        discription:            'Loading...',
        discriptionColor:       'rgb(77, 150, 223)',
        animationOriginColor:   '#123f8a',
        mask:                   true,
        loadingPadding:         '20px 20px',
        loadingBgColor:         'rgb(255 255 255)',
        animationOut:           false,
        animationDuration:      20,
        defaultApply:           true,
    }
}

 

하지만 문제는 여러 API가 호출되거나 로딩을 계속 추가하는 등 로딩이 겹치는 일이 발생하게 되면 정상적으로 작동이 되지 않았다.

 

나는 그래서 로딩이 필요한 순간마다 배열에 추가하는 방식으로 해결하였다.

(하단 코드 test2.js 참조)

 

test2.js

 

/**
 * 준비물 : 하단 3개의 함수와 배열.
 * loadingStart: new function () { },
 * loadingEnd: new function () { },
 * loadingStack: new Array(),
 */


/**
 * [Product App Loading]
 *  - product app 로딩은 해당 함수로 사용.
 *  - 사용 이유 : 앱 화면 꽉 차게 로딩(딤드)를 사용하기 위하여
 *  - loadingStart : 로딩 시작에 선언
 *  - loadingEnd : 로딩 종료에 선언
 */
loadingStart = () => {
    let index = loadingStack.length;
    let properties = getLoadingSet();

    index = index === 0 ? properties.maskBgColor = 'rgba(0, 0, 0, .6)' : properties.maskBgColor = 'rgba(0, 0, 0, 0)';
    loadingStack.push(new Loading(properties));
}

loadingEnd = () => {
    if (!isEmpty(loadingStack[1])) { loadingStack[1].set.maskBgColor = 'rgba(0, 0, 0, .6)' }
    loadingStack.pop().out();
}

 


Custom Loading (Modal) 

modal-loading.js

/**
 * [modal-loading.js]
 * Modal Loading JavaScript Library
 * @author 						c
 * @date    					2017-11-06
 * @param  {window} 	global  
 * @param  {jQuery} 	$       
 * @param  {function} 	factory 
 * @return {void}         		
 * @version 1.0.0
 */
(function(window, $, factory) {

	window.Loading = factory(window, $);

})(window, jQuery, function(window, $) {

	var windowWidth;
	var windowHeight;

	/**
	 * 构造Loading
	 * @author  				c
	 * @date 					2017-11-06
	 * @param {Object} options	构造Loading的具体参数
	 * @return {Loading} 		Loading对象
	 */
	function Loading(options) {
		return new Loading.prototype._init($('body'), options);
	}

	/**
	 * 初始化函数
	 * @author  				c
	 * @date 					2017-11-06
	 * @param {Object} $this	jQuery对象
	 * @param {Object} options	构造Loading的具体参数
	 * @return {Loading} 		Loading对象
	 */
	const init = Loading.prototype._init = function($target, options) {
		
		this.version = '1.0.0';

		this.$target = $target;

		this.set = $.extend(true, {}, this.set, options);

		this._build();

		return this;

	};

	/**
	 * 构建Loading
	 * @return {void} 
	 */
	Loading.prototype._build = function() {

		this.$modalMask = $('<div class="modal-mask"></div>');

		this.$modalLoading = $('<div class="modal-loading"></div>');

		this.$loadingTitle = $('<p class="loading-title"></p>');

		this.$loadingAnimation = $('<div class="loading-animate"></div>');

		this.$animationOrigin = $('<div class="animate-origin"><span></span></div>');

		this.$animationImage = $('<img/>');

		this.$loadingDiscription = $('<p class="loading-discription"></p>');

		// zIndex
		if(this.set.zIndex <= 0) {
			this.set.zIndex = (this.$target.siblings().length-1 || this.$target.children().siblings().length) + 10001;
		}

		// var attr, value;
		// for(attr in this.set) {
		// 	if(attr !== 'zIndex' && attr !== 'animationDuration') {
		// 		value = this.set[attr];
		// 		if(typeof value === 'number') {
		// 			if(value <= 0) {
		// 				this.set[attr] = 'auto';
		// 			} else {
		// 				this.set[attr] = (value + this.set.unit);
		// 			}
		// 		}
		// 	}
		// }

		// 构建Loading
		this._buildMask();

		this._buildLoading();

		this._buildTitle();
		
		this._buildLoadingAnimation();
		
		this._buildDiscription();

		// 是否初始化过
		this._init = false;

		if(this.set.defaultApply) {
			this.apply();
		}

	}

	/**
	 * 构建Mask
	 * @return {void} 
	 */
	Loading.prototype._buildMask = function() {

		// 如果不适用遮罩层
		if(!this.set.mask) {
			this.$modalMask.css({
				position: 	'absolute',
				top: 		'-200%',
			});
			return ;
		}

		// 遮罩层样式
		this.$modalMask.css({
			backgroundColor: 	this.set.maskBgColor,
			zIndex: 			this.set.zIndex,
		});

		// 添加额外的class
		this.$modalMask.addClass(this.set.maskClassName);

	}

	/**
	 * 构建Loading
	 * @return {void} 
	 */
	Loading.prototype._buildLoading = function() {

		this.$modalLoading.css({
			width: 				this.set.loadingWidth,
			height: 			this.set.loadingHeight,
			padding: 			this.set.loadingPadding,
			backgroundColor: 	this.set.loadingBgColor,
			borderRadius: 		this.set.loadingBorderRadius,
		});

		// 布局方式
		if(this.set.direction === 'hor') {
			this.$modalLoading.addClass('modal-hor-layout');
		}

		// 将loading添加到mask中
		this.$modalMask.append(this.$modalLoading);

	}

	/**
	 * 构建Title
	 * @return {void} 
	 */
	Loading.prototype._buildTitle = function() {

		if(!this.set.title) {
			return ;
		}


		this.$loadingTitle.css({
			color: 		this.set.titleColor,
			fontSize: 	this.set.titleFontSize,
		});

		this.$loadingTitle.addClass(this.set.titleClassName);

		this.$loadingTitle.text(this.set.title);

		// 将title添加到loading中
		this.$modalLoading.append(this.$loadingTitle);

	}

	/**
	 * 构建LoadingAnimation
	 * @return {void} 
	 */
	Loading.prototype._buildLoadingAnimation = function() {

		// loadingAnimation
		this.$loadingAnimation.css({
			width: this.set.animationWidth,
			height: this.set.animationHeight,
		});

		if(this.set.loadingAnimation === 'origin') { // origin动画
			this.$animationOrigin.children().css({
				width: this.set.animationOriginWidth,
				height: this.set.animationOriginHeight,
				backgroundColor: this.set.animationOriginColor,
			});
			for(var i = 0; i < 5; i++) {
				this.$loadingAnimation.append(this.$animationOrigin.clone());
			}
		} else if(this.set.loadingAnimation === 'image') { // 图片加载动画
			this.$animationImage.attr('src', this.set.animationSrc);
			this.$loadingAnimation.append(this.$animationImage);
		} //else {
		// 	throw new Error("[loadingAnimation] 参数错误. 参数值只能为['origin', 'image']");
		// }

		this.$loadingAnimation.addClass(this.set.animationClassName);

		// 将loadingAnimation添加到loading中
		this.$modalLoading.append(this.$loadingAnimation);

	}

	/**
	 * 构建Discription
	 * @return {void} 
	 */
	Loading.prototype._buildDiscription = function() {

		if(!this.set.discription) {
			return ;
		}

		this.$loadingDiscription.css({
			color: 		this.set.discriptionColor,
			fontSize: 	this.set.discriptionFontSize,
		});

		this.$loadingDiscription.addClass(this.set.discriptionClassName);

		this.$loadingDiscription.text(this.set.discription);

		// 将title添加到loading中
		this.$modalLoading.append(this.$loadingDiscription);

	}

	/**
	 * 定位
	 * @return {void} 
	 */
	Loading.prototype._position = function() {

		windowWidth = $(window).width();
		windowHeight = $(window).height(); 

		var loadingWidth = this.$modalLoading.outerWidth();
		var loadingHeight = this.$modalLoading.outerHeight();

		var x1 = windowWidth >>> 1;
		var x2 = loadingWidth >>> 1;
		var left = x1 - x2;

		var y1 = windowHeight >>> 1;
		var y2 = loadingHeight >>> 1;
		var top = y1 - y2;

		this.$modalLoading.css({ top, left });

	}

	/**
	 * 入屏过度动画
	 * @return {void} 
	 */
	Loading.prototype._transitionAnimationIn = function() {

		if(!this.set.animationIn) {
			this.$modalMask.css({ display: 'block' });
		} else {
			// this.$modalMask.removeClass(this.set.animationOut).addClass(this.set.animationIn);
			this.$modalMask.addClass(this.set.animationIn);
		}
		
	}

	/**
	 * 出屏过度动画
	 * @return {void} 
	 */
	Loading.prototype._transitionAnimationOut = function() {

		
		if(!this.set.animationOut) {
			
			// this.$modalMask.css({ display: 'none' });
			this.$modalMask.remove();

		} else {
			
			this._timer && this._timer.clearTimeout(this._timer);

			this.$modalMask.removeClass(this.set.animationIn).addClass(this.set.animationOut);

			// this._timer = setTimeout(() => {
			// 	this.$modalMask.remove();
			// }, this.set.animationDuration);

			var self = this;

			this._timer = setTimeout(function() {
				self.$modalMask.remove();
			}, this.set.animationDuration);

		}
	}

	/**
	 * 显示Loading
	 * @return {void} 
	 */
	Loading.prototype.apply = function() {
		this._transitionAnimationIn();

		// 这样按理说可以增加性能, 因为不需要从内存中寻找_initLoading方法.
		if(!this._init) {
			// 初始化Loading
			this._initLoading();
		}

	}

	/**
	 * 隐藏Loading
	 * @return {void} 
	 */
	Loading.prototype.out = function() {
		this._transitionAnimationOut();
	}

	/**
	 * 初始化Loading
	 * @return {void} 
	 */
	Loading.prototype._initLoading = function() {

		// 已经初始过 无需再次初始化
		if(this._init) {
			return ;
		}

		// 添加到页面中
		this.$target.append(this.$modalMask);

		// 定位
		this._position();

		// $(window).resize(() => {
		// 	windowWidth = $(window).width();
		// 	windowHeight = $(window).height();
		// 	this._position();
		// });

		var self = this;
		
		$(window).resize(function() {
			windowWidth = $(window).width();
			windowHeight = $(window).height();
			self._position();
		});

		this._init = true;
	}

	/**
	 * Loading参数属性
	 * 可以简单的设置一些css样式, 复杂的css样式可以通过增加class来更改样式.
	 *
	 * 像素单位: 如果是字符串, 则原文设置. 如果是数字类型, 默认单位为{unit}. zIndex除外.
	 *
	 * 如果字体样式为undefined(例如: titleFontFamily), 那么将会适用全局的字体样式(fontFamily)
	 * 
	 * @author  c
	 * @date 	2017-11-06
	 * @version 1.0.0
	 */
	Loading.prototype.set = {
		direction: 				'ver',	 					// 方向. ver: 垂直, hor: 水平.

		title: 					undefined, 					// 标题内容.
		titleColor: 			'#FFF', 					// 标题文字颜色.
		titleFontSize: 			14, 						// 标题文字字体大小. 
		titleClassName: 		undefined,					// 标题额外的class值.
		// titleFontFamily: 	undefined,					// 标题字体样式
		
		discription: 			undefined, 					// 描述内容.
		discriptionColor: 		'#FFF',						// 描述文字颜色.
		discriptionFontSize: 	14,							// 描述文字字体大小. 
		discriptionClassName: 	undefined,					// 描述额外的class值.
		// directionFontFamily: undefined,					// 描述字体样式.

		loadingWidth: 			'auto',						// Loading宽度.
		loadingHeight: 			'auto',						// Loading高度.
		loadingPadding: 		20,							// Loading内边距.
		loadingBgColor: 		'#252525',					// Loading背景颜色.
		loadingBorderRadius: 	12,							// Loading的borderRadius.
		// loadingPosition: 		'fixed',					// Loading的position

		mask: 					true, 						// 遮罩层. true: 显示遮罩层, false: 不显示. 
		maskBgColor: 			'rgba(0, 0, 0, .6)',		// 遮罩层背景颜色.
		maskClassName: 			undefined,					// 为遮罩层添加.
		// maskPosition: 			'fixed',					// 遮罩层position

		loadingAnimation: 		'origin',					// 加载动画. origin: 表示使用默认的原点动画, image: 表示使用自定义图片作为加载动画.
		animationSrc: 			undefined,					// 图片加载动画的地址. (前提: loadingAnimation=origin, 以下简称origin或者image)
		animationWidth: 		40, 						// 动画宽度. 为image时表示图片的宽度.
		animationHeight: 		40,							// 动画高度. 为image时表示图片的高度.
		animationOriginWidth:   4,							// 原点动画宽度.    (前提: origin)
		animationOriginHeight:  4,							// 原点动画高度.    (前提: origin)
		animationOriginColor:   '#FFF',						// 原点动画的颜色.  (前提: origin)
		animationClassName: 	undefined,					// 为动画添加一个额外的class值.
		
		defaultApply: 			true,						// 默认自动显示.
		animationIn: 			'animated fadeIn', 			// 入屏动画. 
		animationOut: 			'animated fadeOut',			// 出屏动画.
		animationDuration: 		1000,						// 动画持续时间(单位:ms)
		// fontFamily: 			'sans-serif',				// 文字字体样式.
		// position: 				'fixed',				// 定位. mask和loading的定位.
		// unit: 				'px', 						// 设置默认单位.
		zIndex: 				0,							// 最外围层级(mask). 如果是0或者负数, 则为{$this.siblings() + 10001}.

	};

	init.prototype = Loading.prototype;

	return Loading;
});

modal-loading.css

@charset "UTF-8";

/**
 * Modal Loading Css Library
 * @author  c
 * @date    2017-11-06
 * @version 1.0.0
 */
 
.modal-mask {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	/*background: rgba(22, 22, 22, 0.2);*/
}

.modal-mask .modal-loading {
	position: fixed;
	top: 0;
	left: 0;
    /*-webkit-border-radius: 12px;*/
    /*border-radius: 12px;*/
    /*padding: 5px 15px;*/
	/*background: rgb(49, 41, 35);*/
	text-align: center;
}

.modal-mask .modal-loading .loading-title {
	/*font-size: 1.4rem;*/
	/*color: #FFF;*/
}


.modal-mask .modal-loading .loading-discription {
	/*font-size: 1.2rem;*/
	/*color: #FFF;*/
}

/* start loading-animate */
.modal-mask .modal-loading .loading-animate {
	/*width: 40px;*/
	/*height: 40px;*/
	background: transparent;
	position: relative;
	margin: 0 auto;
}

/* 图片加载动画 */
.modal-mask .modal-loading .loading-animate img {
	width: 100%;
	height: 100%;
}

.modal-mask .modal-loading .loading-animate .animate-origin {
    width: 60%;
    height: 60%;
    position: absolute;
    left: 20%;
    top: 20%;
    opacity: 1;
	-webkit-animation: load 2.28s linear infinite;
	animation: load 2.28s linear infinite;
}

.modal-mask .modal-loading .loading-animate .animate-origin span {
	display: block;
	/*width: 4px;*/
	/*height: 4px;*/
	/*background: #FFF;*/
	border-radius: 50%;
}

.modal-mask .modal-loading .loading-animate .animate-origin:nth-child(1) {
	-webkit-animation-delay: 0.2s;
	animation-delay: 0.2s;
}

.modal-mask .modal-loading .loading-animate .animate-origin:nth-child(2) {
	-webkit-animation-delay: 0.4s;
	animation-delay: 0.4s;
}

.modal-mask .modal-loading .loading-animate .animate-origin:nth-child(3) {
	-webkit-animation-delay: 0.6s;
	animation-delay: 0.6s;
}

.modal-mask .modal-loading .loading-animate .animate-origin:nth-child(4) {
	-webkit-animation-delay: 0.8s;
	animation-delay: 0.8s;
}

.modal-mask .modal-loading .loading-animate .animate-origin:nth-child(5) {
	-webkit-animation-delay: 1s;
	animation-delay: 1s;
}

@-webkit-keyframes load {
	0% {
		-webkit-transform: rotate(0deg);
		transform: rotate(0deg);
	}
	10% {
		-webkit-transform: rotate(45deg);
		transform: rotate(45deg);
	}
	50% {
		opacity: 1;
		-webkit-transform: rotate(160deg);
		transform: rotate(160deg);
	}
	62% {
		opacity: 0;
	}
	65% {
		opacity: 0;
		-webkit-transform: rotate(200deg);
		transform: rotate(200deg);
	}
	100% {
		-webkit-transform: rotate(360deg);
		transform: rotate(360deg);
	}
}

@keyframes load {
	0% {
		-webkit-transform: rotate(0deg);
		transform: rotate(0deg);
	}
	10% {
		-webkit-transform: rotate(45deg);
		transform: rotate(45deg);
	}
	50% {
		opacity: 1;
		-webkit-transform: rotate(160deg);
		transform: rotate(160deg);
	}
	62% {
		opacity: 0;
	}
	65% {
		opacity: 0;
		-webkit-transform: rotate(200deg);
		transform: rotate(200deg);
	}
	100% {
		-webkit-transform: rotate(360deg);
		transform: rotate(360deg);
	}
}
/* loading-animate end */

/* 水平布局 */
.modal-mask .modal-loading.modal-hor-layout .loading-title {
}

.modal-mask .modal-loading.modal-hor-layout .loading-animate {
	display: inline-block;
	vertical-align: middle;
}

.modal-mask .modal-loading.modal-hor-layout .loading-discription {
	display: inline-block;
	padding-left: 15px;
	/*font-size: 1.4rem;*/
}​

modal-loading-animate.css

@charset "UTF-8";

/**
 * Modal Loading Animation Library
 * @author  c
 * @date    2017-11-06
 * @version 1.0.0
 */

.animated {
    -webkit-animation-duration: 1s;
    animation-duration: 1s;
    -webkit-animation-fill-mode: both;
    animation-fill-mode: both;
}

.fadeIn {
    -webkit-animation-name: fadeIn;
    animation-name: fadeIn;
}

.fadeOut {
    -webkit-animation-name: fadeOut;
    animation-name: fadeOut;
}

@-webkit-keyframes fadeIn {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@keyframes fadeIn {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@-webkit-keyframes fadeOut {
    0% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}

@keyframes fadeOut {
    0% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}