ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Javascript 커스텀 로딩(Custom Modal Loading)
    DEV/javascript 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;
        }
    }

    댓글

Designed by Tistory.