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;
}
}