diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e52458..e9ca2aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,9 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al - updatenotification: Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules - updatenotification: allow force scanning with `SCAN_UPDATES` notification from other modules - Added per-calendar fetchInterval +- Added optional AnimateCSS animate for `hide()`, `show()`, `updateDom()` +- Added AnimateIn and animateOut in module config definition +- Apply AnimateIn rules on the first start ### Removed diff --git a/index.html b/index.html index 4673ef09..501c1af5 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,7 @@ + + diff --git a/js/animateCSS.js b/js/animateCSS.js new file mode 100644 index 00000000..ae6e7bec --- /dev/null +++ b/js/animateCSS.js @@ -0,0 +1,165 @@ +/* MagicMirror² + * AnimateCSS System from https://animate.style/ + * by @bugsounet + * for Michael Teeuw https://michaelteeuw.nl + * MIT Licensed. + */ + +/* enumeration of animations in Array **/ +const AnimateCSSIn = [ + // Attention seekers + "bounce", + "flash", + "pulse", + "rubberBand", + "shakeX", + "shakeY", + "headShake", + "swing", + "tada", + "wobble", + "jello", + "heartBeat", + // Back entrances + "backInDown", + "backInLeft", + "backInRight", + "backInUp", + // Bouncing entrances + "bounceIn", + "bounceInDown", + "bounceInLeft", + "bounceInRight", + "bounceInUp", + // Fading entrances + "fadeIn", + "fadeInDown", + "fadeInDownBig", + "fadeInLeft", + "fadeInLeftBig", + "fadeInRight", + "fadeInRightBig", + "fadeInUp", + "fadeInUpBig", + "fadeInTopLeft", + "fadeInTopRight", + "fadeInBottomLeft", + "fadeInBottomRight", + // Flippers + "flip", + "flipInX", + "flipInY", + // Lightspeed + "lightSpeedInRight", + "lightSpeedInLeft", + // Rotating entrances + "rotateIn", + "rotateInDownLeft", + "rotateInDownRight", + "rotateInUpLeft", + "rotateInUpRight", + // Specials + "jackInTheBox", + "rollIn", + // Zooming entrances + "zoomIn", + "zoomInDown", + "zoomInLeft", + "zoomInRight", + "zoomInUp", + // Sliding entrances + "slideInDown", + "slideInLeft", + "slideInRight", + "slideInUp" +]; + +const AnimateCSSOut = [ + // Back exits + "backOutDown", + "backOutLeft", + "backOutRight", + "backOutUp", + // Bouncing exits + "bounceOut", + "bounceOutDown", + "bounceOutLeft", + "bounceOutRight", + "bounceOutUp", + // Fading exits + "fadeOut", + "fadeOutDown", + "fadeOutDownBig", + "fadeOutLeft", + "fadeOutLeftBig", + "fadeOutRight", + "fadeOutRightBig", + "fadeOutUp", + "fadeOutUpBig", + "fadeOutTopLeft", + "fadeOutTopRight", + "fadeOutBottomRight", + "fadeOutBottomLeft", + // Flippers + "flipOutX", + "flipOutY", + // Lightspeed + "lightSpeedOutRight", + "lightSpeedOutLeft", + // Rotating exits + "rotateOut", + "rotateOutDownLeft", + "rotateOutDownRight", + "rotateOutUpLeft", + "rotateOutUpRight", + // Specials + "hinge", + "rollOut", + // Zooming exits + "zoomOut", + "zoomOutDown", + "zoomOutLeft", + "zoomOutRight", + "zoomOutUp", + // Sliding exits + "slideOutDown", + "slideOutLeft", + "slideOutRight", + "slideOutUp" +]; + +/** + * Create an animation with Animate CSS + * resolved as Promise when done + * @param {string} [element] div element to animate. + * @param {string} [animation] animation name. + * @param {number} [animationTime] animation duration. + */ +function AnimateCSS(element, animation, animationTime) { + /* We create a Promise and return it */ + return new Promise((resolve) => { + const animationName = `animate__${animation}`; + const node = document.getElementById(element); + if (!node) { + // don't execute animate and resolve + Log.warn(`AnimateCSS: node not found for`, element); + resolve(); + return; + } + node.style.setProperty("--animate-duration", `${animationTime}s`); + node.classList.add("animate__animated", animationName); + + /** + * When the animation ends, we clean the classes and resolve the Promise + * @param {object} event object + */ + function handleAnimationEnd(event) { + node.classList.remove("animate__animated", animationName); + node.style.removeProperty("--animate-duration", `${animationTime}s`); + event.stopPropagation(); + resolve(); + } + + node.addEventListener("animationend", handleAnimationEnd, { once: true }); + }); +} diff --git a/js/loader.js b/js/loader.js index 517999bb..a5dc36fc 100644 --- a/js/loader.js +++ b/js/loader.js @@ -88,6 +88,8 @@ const Loader = (function () { path: `${moduleFolder}/`, file: `${moduleName}.js`, position: moduleData.position, + animateIn: moduleData.animateIn, + animateOut: moduleData.animateOut, hiddenOnStartup: moduleData.hiddenOnStartup, header: moduleData.header, configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false, diff --git a/js/main.js b/js/main.js index 026410c7..5efce70a 100644 --- a/js/main.js +++ b/js/main.js @@ -1,4 +1,4 @@ -/* global Loader, defaults, Translator */ +/* global Loader, defaults, Translator, AnimateCSS, AnimateCSSIn, AnimateCSSOut */ /* MagicMirror² * Main System @@ -22,6 +22,10 @@ const MM = (function () { return; } + let haveAnimateIn = null; + // check if have valid animateIn in module definition (module.data.animateIn) + if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn; + const wrapper = selectWrapper(module.data.position); const dom = document.createElement("div"); @@ -50,7 +54,12 @@ const MM = (function () { moduleContent.className = "module-content"; dom.appendChild(moduleContent); - const domCreationPromise = updateDom(module, 0); + // create the domCreationPromise with AnimateCSS (with animateIn of module definition) + // or just display it + var domCreationPromise; + if (haveAnimateIn) domCreationPromise = updateDom(module, 1000, null, haveAnimateIn, true); + else domCreationPromise = updateDom(module, 0); + domCreationPromises.push(domCreationPromise); domCreationPromise .then(function () { @@ -101,11 +110,30 @@ const MM = (function () { /** * Update the dom for a specific module. * @param {Module} module The module that needs an update. - * @param {number} [speed] The (optional) number of microseconds for the animation. + * @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates) + * @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror) * @returns {Promise} Resolved when the dom is fully updated. */ - const updateDom = function (module, speed) { + const updateDom = function (module, updateOptions, createAnimatedDom = false) { return new Promise(function (resolve) { + let speed = updateOptions; + let animateOut = null; + let animateIn = null; + if (typeof updateOptions === "object") { + if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) { + speed = updateOptions.options.speed; + Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`); + if (typeof updateOptions.options.animate === "object") { + animateOut = updateOptions.options.animate.out; + animateIn = updateOptions.options.animate.in; + Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`); + } + } else { + Log.debug(`updateDom: ${module.identifier} Has no speed in object`); + speed = 0; + } + } + const newHeader = module.getHeader(); let newContentPromise = module.getDom(); @@ -116,7 +144,7 @@ const MM = (function () { newContentPromise .then(function (newContent) { - const updatePromise = updateDomWithContent(module, speed, newHeader, newContent); + const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom); updatePromise.then(resolve).catch(Log.error); }) @@ -130,9 +158,12 @@ const MM = (function () { * @param {number} [speed] The (optional) number of microseconds for the animation. * @param {string} newHeader The new header that is generated. * @param {HTMLElement} newContent The new content that is generated. + * @param {string} [animateOut] AnimateCss animation name before hidden + * @param {string} [animateIn] AnimateCss animation name on show + * @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start) * @returns {Promise} Resolved when the module dom has been updated. */ - const updateDomWithContent = function (module, speed, newHeader, newContent) { + const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) { return new Promise(function (resolve) { if (module.hidden || !speed) { updateModuleContent(module, newHeader, newContent); @@ -151,13 +182,28 @@ const MM = (function () { return; } - hideModule(module, speed / 2, function () { + if (createAnimatedDom && animateIn !== null) { + Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`); updateModuleContent(module, newHeader, newContent); if (!module.hidden) { - showModule(module, speed / 2); + showModule(module, speed, null, { animate: animateIn }); } resolve(); - }); + return; + } + + hideModule( + module, + speed / 2, + function () { + updateModuleContent(module, newHeader, newContent); + if (!module.hidden) { + showModule(module, speed / 2, null, { animate: animateIn }); + } + resolve(); + }, + { animate: animateOut } + ); }); }; @@ -223,7 +269,7 @@ const MM = (function () { * @param {Function} callback Called when the animation is done. * @param {object} [options] Optional settings for the hide method. */ - const hideModule = function (module, speed, callback, options = {}) { + const hideModule = async function (module, speed, callback, options = {}) { // set lockString if set in options. if (options.lockString) { // Log.log("Has lockstring: " + options.lockString); @@ -234,24 +280,49 @@ const MM = (function () { const moduleWrapper = document.getElementById(module.identifier); if (moduleWrapper !== null) { - moduleWrapper.style.transition = `opacity ${speed / 1000}s`; - moduleWrapper.style.opacity = 0; - moduleWrapper.classList.add("hidden"); - clearTimeout(module.showHideTimer); - module.showHideTimer = setTimeout(function () { - // To not take up any space, we just make the position absolute. - // since it's fade out anyway, we can see it lay above or - // below other modules. This works way better than adjusting - // the .display property. + + // haveAnimateName for verify if we are using AninateCSS library + // we check AnimateCSSOut Array for validate it + // and finaly return the animate name or `null` (for default MM² animation) + let haveAnimateName = null; + // check if have valid animateOut in module definition (module.data.animateOut) + if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut; + // can't be override with options.animate + else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate; + + if (haveAnimateName) { + // with AnimateCSS + Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`); + await AnimateCSS(module.identifier, haveAnimateName, speed / 1000); + // AnimateCSS is now done + moduleWrapper.style.opacity = 0; + moduleWrapper.classList.add("hidden"); moduleWrapper.style.position = "fixed"; updateWrapperStates(); - if (typeof callback === "function") { callback(); } - }, speed); + } else { + // default MM² Animate + moduleWrapper.style.transition = `opacity ${speed / 1000}s`; + moduleWrapper.style.opacity = 0; + moduleWrapper.classList.add("hidden"); + module.showHideTimer = setTimeout(function () { + // To not take up any space, we just make the position absolute. + // since it's fade out anyway, we can see it lay above or + // below other modules. This works way better than adjusting + // the .display property. + moduleWrapper.style.position = "fixed"; + + updateWrapperStates(); + + if (typeof callback === "function") { + callback(); + } + }, speed); + } } else { // invoke callback even if no content, issue 1308 if (typeof callback === "function") { @@ -267,7 +338,7 @@ const MM = (function () { * @param {Function} callback Called when the animation is done. * @param {object} [options] Optional settings for the show method. */ - const showModule = function (module, speed, callback, options = {}) { + const showModule = async function (module, speed, callback, options = {}) { // remove lockString if set in options. if (options.lockString) { const index = module.lockStrings.indexOf(options.lockString); @@ -296,7 +367,18 @@ const MM = (function () { const moduleWrapper = document.getElementById(module.identifier); if (moduleWrapper !== null) { - moduleWrapper.style.transition = `opacity ${speed / 1000}s`; + clearTimeout(module.showHideTimer); + + // haveAnimateName for verify if we are using AninateCSS library + // we check AnimateCSSIn Array for validate it + // and finaly return the animate name or `null` (for default MM² animation) + let haveAnimateName = null; + // check if have valid animateOut in module definition (module.data.animateIn) + if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn; + // can't be override with options.animate + else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate; + + if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`; // Restore the position. See hideModule() for more info. moduleWrapper.style.position = "static"; moduleWrapper.classList.remove("hidden"); @@ -307,12 +389,21 @@ const MM = (function () { const dummy = moduleWrapper.parentElement.parentElement.offsetHeight; moduleWrapper.style.opacity = 1; - clearTimeout(module.showHideTimer); - module.showHideTimer = setTimeout(function () { + if (haveAnimateName) { + // with AnimateCSS + Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`); + await AnimateCSS(module.identifier, haveAnimateName, speed / 1000); if (typeof callback === "function") { callback(); } - }, speed); + } else { + // default MM² Animate + module.showHideTimer = setTimeout(function () { + if (typeof callback === "function") { + callback(); + } + }, speed); + } } else { // invoke callback if (typeof callback === "function") { @@ -514,9 +605,9 @@ const MM = (function () { /** * Update the dom for a specific module. * @param {Module} module The module that needs an update. - * @param {number} [speed] The number of microseconds for the animation. + * @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates) */ - updateDom: function (module, speed) { + updateDom: function (module, updateOptions) { if (!(module instanceof Module)) { Log.error("updateDom: Sender should be a module."); return; @@ -528,7 +619,7 @@ const MM = (function () { } // Further implementation is done in the private method. - updateDom(module, speed); + updateDom(module, updateOptions); }, /** diff --git a/js/module.js b/js/module.js index 110ccc55..62534b01 100644 --- a/js/module.js +++ b/js/module.js @@ -193,7 +193,7 @@ const Module = Class.extend({ }, /********************************************* - * The methods below don"t need subclassing. * + * The methods below don't need subclassing. * *********************************************/ /** @@ -327,10 +327,10 @@ const Module = Class.extend({ /** * Request an (animated) update of the module. - * @param {number} [speed] The speed of the animation. + * @param {number|object} [updateOptions] The speed of the animation or object with for updateOptions (speed/animates) */ - updateDom: function (speed) { - MM.updateDom(this, speed); + updateDom: function (updateOptions) { + MM.updateDom(this, updateOptions); }, /** diff --git a/vendor/package-lock.json b/vendor/package-lock.json index 4f88463d..30ff41b0 100644 --- a/vendor/package-lock.json +++ b/vendor/package-lock.json @@ -8,6 +8,7 @@ "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^6.4.2", + "animate.css": "^4.1.1", "moment": "^2.29.4", "moment-timezone": "^0.5.43", "nunjucks": "^3.2.4", @@ -29,6 +30,11 @@ "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" }, + "node_modules/animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==" + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -107,6 +113,11 @@ "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" }, + "animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==" + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", diff --git a/vendor/package.json b/vendor/package.json index 376b1f07..9f2f93c5 100644 --- a/vendor/package.json +++ b/vendor/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^6.4.2", + "animate.css": "^4.1.1", "moment": "^2.29.4", "moment-timezone": "^0.5.43", "nunjucks": "^3.2.4",