使用纯JavaScript封装一个消息提示条功能示例详解
黄景圣 人气:0介绍
一个类似Element UI
、Ant-Design UI
等 UI 框架的消息提示功能,方便在任何网页环境中直接调用函数使用;区别在不依赖 js 及 css 引用,而是使用纯 js 进行封装实现,代码更精简,同时保持和 UI 框架一样的视觉效果(可自行修改成自己喜欢的样式)
在线预览效果(点击【登录】、【点击复制】按钮时触发提示效果)
思路&布局
- 先来写单个提示条,并实现想要的过渡效果,最后再用逻辑操作输出节点即可;这里不需要父节点包裹,直接输出到
<body>
中,保证提示条的代码结构位置永远在最上层。
<style> .msg-box { position: fixed; top: 0; left: 50%; display: flex; padding: 12px 16px; border-radius: 2px; background-color: #fff; box-shadow: 0 3px 3px -2px rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 1px 8px 0 rgba(0,0,0,.12); transform: translate3d(-50%, 0%, 0); } </style> <body> <div class="msg-box"></div> </body>
- 最基础的样式写好之后,来定义进入的过渡动画,这里使用
animation
作为进入动画,因为节点一输出就会执行了
.msg-box { ...省略以上代码 animation: msg-move .4s; } @keyframes msg-move { 0% { opacity: 0; transform: translate3d(-50%, -100%, 0); } 100% { opacity: 1; transform: translate3d(-50%, 0%, 0); } }
- 最后就是过渡结束动画,这里使用
transition
的过渡方式,即定义一个.hide
,之后通过 js 去控制切换 class 去实现过渡切换
.msg-box { ...省略以上代码 opacity: 1; transition: .4s all; // 保持和 animation 的过渡时间一致 } .msg-box.hide { opacity: 0; transform: translate3d(-50%, -100%, 0); }
这里样式布局就全部完成了,剩下的交给 js 去处理对应的操作逻辑。
操作逻辑
- 因为调用时,消息条是多个,并且为往下叠加的效果,且节点是散布在
<body>
下,有可能给其他dom
操作插入节点。所以在输出节点的时候要将其存放起来,然后通过循环的方式去设置每一个节点的padding-top
,这样就能保证视觉排列效果和代码操作的顺序保持一致了,之后所做的删除操作也是通过循环去设置每一个节点的padding-top
。
/** * 消息队列 * @type {Array<HTMLElement>} */ const messageList = []; /** * 获取指定`item`的定位`top` * @param {HTMLElement=} el */ function getItemTop(el) { let top = 10; // 起始的边距 for (let i = 0; i < messageList.length; i++) { const item = messageList[i]; if (el && el === item) { break; } top += item.clientHeight + 20; // 两个消息条的间距为20 } return top; } /** * 删除指定列表项 * @param {HTMLElement} el */ function removeItem(el) { for (let i = 0; i < messageList.length; i++) { const item = messageList[i]; if (item === el) { messageList.splice(i, 1); break; } } el.classList.add(".hide"); messageList.forEach(function (item) { item.style.top = `${getItemTop(item)}px`; }); }
- 输出节点,并监听 动画进入过渡、持续时间、动画退出过渡 ;
- 进入的过渡使用
addEventListener("animationend", fn)
- 持续时间使用
setTimeout
(延迟 N 秒之后为节点添加.hide
) - 退出的过渡使用
addEventListener("transitionend", fn)
/** 一直累加的定位层级 */ let zIndex = 1000; /** * 显示一条消息 * @param {string} content 内容 * @param {number} duration 持续时间,优先级比默认值高 */ function show(content, duration) { const el = document.createElement("div"); el.style.top = `${getItemTop()}px`; el.style.zIndex = zIndex; el.innerHTML = content; zIndex++; messageList.push(el); document.body.appendChild(el); // 添加动画监听事件 function animationEnd() { el.removeEventListener("animationend", animationEnd); setTimeout(removeItem, duration || 3000, el); } el.addEventListener("animationend", animationEnd); function transitionEnd() { if (getComputedStyle(el).opacity !== "0") return; el.removeEventListener("transitionend", transitionEnd); el.remove(); } el.addEventListener("transitionend", transitionEnd); }
整个消息输出功能就完成了,最后只需要把对应的方法封装起来并暴露需要调用的函数,并把css
样式写进方法里就可以在任意地方使用了;css
写进js
里其实就是把上面写好的样式,通过字符串模板的方式用变量接收并赋值给动态输入的<style>
标签就完事,另外样式隔离可以通过模拟css.modeule
的方案去实现,具体看下面完整代码。
完整代码
/** * 消息提示条 * @param {object} params * @param {number} params.duration 持续时间(毫秒),默认`3000` * @param {number} params.zIndex 起始定位层级,默认`1000` */ function useMessage(params = {}) { const doc = document; const cssModule = `__${Math.random().toString(36).slice(2, 7)}`; const className = { box: `msg-box${cssModule}`, hide: `hide${cssModule}`, text: `msg-text${cssModule}`, icon: `msg-icon${cssModule}` } const style = doc.createElement("style"); style.textContent = ` .${className.box}, .${className.icon}, .${className.text} { padding: 0; margin: 0; box-sizing: border-box; } .${className.box} { position: fixed; top: 0; left: 50%; display: flex; padding: 12px 16px; border-radius: 2px; background-color: #fff; box-shadow: 0 3px 3px -2px rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 1px 8px 0 rgba(0,0,0,.12); white-space: nowrap; animation: ${className.box}-move .4s; transition: .4s all; transform: translate3d(-50%, 0%, 0); opacity: 1; overflow: hidden; } .${className.box}::after { content: ""; position: absolute; left: 0; top: 0; height: 100%; width: 4px; } @keyframes ${className.box}-move { 0% { opacity: 0; transform: translate3d(-50%, -100%, 0); } 100% { opacity: 1; transform: translate3d(-50%, 0%, 0); } } .${className.box}.${className.hide} { opacity: 0; /* transform: translate3d(-50%, -100%, 0); */ transform: translate3d(-50%, -100%, 0) scale(0); } .${className.icon} { display: inline-block; width: 18px; height: 18px; border-radius: 50%; overflow: hidden; margin-right: 6px; position: relative; } .${className.text} { font-size: 14px; line-height: 18px; color: #555; } .${className.icon}::after, .${className.icon}::before { position: absolute; content: ""; background-color: #fff; } .${className.box}.info .${className.icon}, .${className.box}.info::after { background-color: #1890ff; } .${className.box}.success .${className.icon}, .${className.box}.success::after { background-color: #52c41a; } .${className.box}.warning .${className.icon}, .${className.box}.warning::after { background-color: #faad14; } .${className.box}.error .${className.icon}, .${className.box}.error::after { background-color: #ff4d4f; } .${className.box}.info .${className.icon}::after, .${className.box}.warning .${className.icon}::after { top: 15%; left: 50%; margin-left: -1px; width: 2px; height: 2px; border-radius: 50%; } .${className.box}.info .${className.icon}::before, .${className.box}.warning .${className.icon}::before { top: calc(15% + 4px); left: 50%; margin-left: -1px; width: 2px; height: 40%; } .${className.box}.error .${className.icon}::after, .${className.box}.error .${className.icon}::before { top: 20%; left: 50%; width: 2px; height: 60%; margin-left: -1px; border-radius: 1px; } .${className.box}.error .${className.icon}::after { transform: rotate(-45deg); } .${className.box}.error .${className.icon}::before { transform: rotate(45deg); } .${className.box}.success .${className.icon}::after { box-sizing: content-box; background-color: transparent; border: 2px solid #fff; border-left: 0; border-top: 0; height: 50%; left: 35%; top: 13%; transform: rotate(45deg); width: 20%; transform-origin: center; } `.replace(/(\n|\t|\s)*/ig, "$1").replace(/\n|\t|\s(\{|\}|\,|\:|\;)/ig, "$1").replace(/(\{|\}|\,|\:|\;)\s/ig, "$1"); doc.head.appendChild(style); /** 一直累加的定位层级 */ let zIndex = params.zIndex || 1000; /** * 消息队列 * @type {Array<HTMLElement>} */ const messageList = []; /** * 获取指定`item`的定位`top` * @param {HTMLElement=} el */ function getItemTop(el) { let top = 10; for (let i = 0; i < messageList.length; i++) { const item = messageList[i]; if (el && el === item) { break; } top += item.clientHeight + 20; } return top; } /** * 删除指定列表项 * @param {HTMLElement} el */ function removeItem(el) { for (let i = 0; i < messageList.length; i++) { const item = messageList[i]; if (item === el) { messageList.splice(i, 1); break; } } el.classList.add(className.hide); messageList.forEach(function(item) { item.style.top = `${getItemTop(item)}px`; }); } /** * 显示一条消息 * @param {string} content 内容 * @param {"info"|"success"|"warning"|"error"} type 消息类型 * @param {number} duration 持续时间,优先级比默认值高 */ function show(content, type = "info", duration) { const el = doc.createElement("div"); el.className = `${className.box} ${type}`; el.style.top = `${getItemTop()}px`; el.style.zIndex = zIndex; el.innerHTML = ` <span class="${className.icon}"></span> <span class="${className.text}">${content}</span> `; zIndex++; messageList.push(el); doc.body.appendChild(el); // 添加动画监听事件 function animationEnd() { el.removeEventListener("animationend", animationEnd); setTimeout(removeItem, duration || params.duration || 3000, el); } el.addEventListener("animationend", animationEnd); function transitionEnd() { if (getComputedStyle(el).opacity !== "0") return; el.removeEventListener("transitionend", transitionEnd); el.remove(); } el.addEventListener("transitionend", transitionEnd); } return { show, /** * 普通描述提示 * @param {string} msg */ info(msg) { show(msg, "info"); }, /** * 成功提示 * @param {string} msg */ success(msg) { show(msg, "success"); }, /** * 警告提示 * @param {string} msg */ warning(msg) { show(msg, "warning"); }, /** * 错误提示 * @param {string} msg */ error(msg) { show(msg, "error"); } } }
加载全部内容