';
+ this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`;
+ let strinner = "
";
strinner += this.options.message;
strinner += "
";
this.ntf.innerHTML = strinner;
@@ -124,7 +122,6 @@
/**
* Dismiss the notification
- *
* @param {boolean} [close] call the onClose callback at the end
*/
NotificationFx.prototype.dismiss = function (close = true) {
@@ -157,4 +154,4 @@
* Add to global namespace
*/
window.NotificationFx = NotificationFx;
-})(window);
+}(window));
diff --git a/modules/default/alert/styles/notificationFx.css b/modules/default/alert/styles/notificationFx.css
index 8e033e0d61..df3407514c 100644
--- a/modules/default/alert/styles/notificationFx.css
+++ b/modules/default/alert/styles/notificationFx.css
@@ -1,7 +1,7 @@
/* Based on work by https://tympanus.net/codrops/licensing/ */
.ns-box {
- background-color: rgba(0, 0, 0, 0.93);
+ background-color: rgb(0 0 0 / 93%);
padding: 17px;
line-height: 1.4;
margin-bottom: 10px;
@@ -55,15 +55,15 @@
.ns-effect-flip.ns-show,
.ns-effect-flip.ns-hide {
- animation-name: animFlipFront;
+ animation-name: anim-flip-front;
animation-duration: 0.3s;
}
.ns-effect-flip.ns-hide {
- animation-name: animFlipBack;
+ animation-name: anim-flip-back;
}
-@keyframes animFlipFront {
+@keyframes anim-flip-front {
0% {
transform: perspective(1000px) rotate3d(1, 0, 0, -90deg);
}
@@ -73,7 +73,7 @@
}
}
-@keyframes animFlipBack {
+@keyframes anim-flip-back {
0% {
transform: perspective(1000px) rotate3d(1, 0, 0, 90deg);
}
@@ -85,11 +85,11 @@
.ns-effect-bouncyflip.ns-show,
.ns-effect-bouncyflip.ns-hide {
- animation-name: flipInX;
+ animation-name: flip-in-x;
animation-duration: 0.8s;
}
-@keyframes flipInX {
+@keyframes flip-in-x {
0% {
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
transition-timing-function: ease-in;
@@ -117,11 +117,11 @@
}
.ns-effect-bouncyflip.ns-hide {
- animation-name: flipInXSimple;
+ animation-name: flip-in-x-simple;
animation-duration: 0.3s;
}
-@keyframes flipInXSimple {
+@keyframes flip-in-x-simple {
0% {
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
transition-timing-function: ease-in;
@@ -141,11 +141,11 @@
}
.ns-effect-exploader.ns-show {
- animation-name: animLoad;
+ animation-name: anim-load;
animation-duration: 1s;
}
-@keyframes animLoad {
+@keyframes anim-load {
0% {
opacity: 1;
transform: scale3d(0, 0.3, 1);
@@ -158,7 +158,7 @@
}
.ns-effect-exploader.ns-hide {
- animation-name: animFade;
+ animation-name: anim-fade;
animation-duration: 0.3s;
}
@@ -170,15 +170,15 @@
}
.ns-effect-exploader.ns-show .ns-close {
- animation-name: animFade;
+ animation-name: anim-fade;
}
.ns-effect-exploader.ns-show .ns-box-inner {
- animation-name: animFadeMove;
+ animation-name: anim-fade-move;
animation-timing-function: ease-out;
}
-@keyframes animFadeMove {
+@keyframes anim-fade-move {
0% {
opacity: 0;
transform: translate3d(0, 10px, 0);
@@ -190,7 +190,7 @@
}
}
-@keyframes animFade {
+@keyframes anim-fade {
0% {
opacity: 0;
}
@@ -202,11 +202,11 @@
.ns-effect-scale.ns-show,
.ns-effect-scale.ns-hide {
- animation-name: animScale;
+ animation-name: anim-scale;
animation-duration: 0.25s;
}
-@keyframes animScale {
+@keyframes anim-scale {
0% {
opacity: 0;
transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1);
@@ -219,168 +219,169 @@
}
.ns-effect-jelly.ns-show {
- animation-name: animJelly;
+ animation-name: anim-jelly;
animation-duration: 1s;
animation-timing-function: linear;
}
.ns-effect-jelly.ns-hide {
- animation-name: animFade;
+ animation-name: anim-fade;
animation-duration: 0.3s;
}
-@keyframes animFade {
+@keyframes anim-fade {
0% {
opacity: 0;
}
+
100% {
opacity: 1;
}
}
-@keyframes animJelly {
+@keyframes anim-jelly {
0% {
transform: matrix3d(0.7, 0, 0, 0, 0, 0.7, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
2.083333% {
- transform: matrix3d(0.75266, 0, 0, 0, 0, 0.76342, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.7527, 0, 0, 0, 0, 0.7634, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
4.166667% {
- transform: matrix3d(0.81071, 0, 0, 0, 0, 0.84545, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.8107, 0, 0, 0, 0, 0.8454, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
6.25% {
- transform: matrix3d(0.86808, 0, 0, 0, 0, 0.9286, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.8681, 0, 0, 0, 0, 0.929, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
8.333333% {
- transform: matrix3d(0.92038, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
10.416667% {
- transform: matrix3d(0.96482, 0, 0, 0, 0, 1.05202, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9648, 0, 0, 0, 0, 1.052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
12.5% {
- transform: matrix3d(1, 0, 0, 0, 0, 1.08204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1.082, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
14.583333% {
- transform: matrix3d(1.02563, 0, 0, 0, 0, 1.09149, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0256, 0, 0, 0, 0, 1.0915, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
16.666667% {
- transform: matrix3d(1.04227, 0, 0, 0, 0, 1.08453, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0423, 0, 0, 0, 0, 1.0845, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
18.75% {
- transform: matrix3d(1.05102, 0, 0, 0, 0, 1.06666, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.051, 0, 0, 0, 0, 1.0667, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
20.833333% {
- transform: matrix3d(1.05334, 0, 0, 0, 0, 1.04355, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0533, 0, 0, 0, 0, 1.0436, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
22.916667% {
- transform: matrix3d(1.05078, 0, 0, 0, 0, 1.02012, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0508, 0, 0, 0, 0, 1.0201, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
25% {
- transform: matrix3d(1.04487, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0449, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
27.083333% {
- transform: matrix3d(1.03699, 0, 0, 0, 0, 0.98534, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.037, 0, 0, 0, 0, 0.9853, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
29.166667% {
- transform: matrix3d(1.02831, 0, 0, 0, 0, 0.97688, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0283, 0, 0, 0, 0, 0.9769, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
31.25% {
- transform: matrix3d(1.01973, 0, 0, 0, 0, 0.97422, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0197, 0, 0, 0, 0, 0.9742, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
33.333333% {
- transform: matrix3d(1.01191, 0, 0, 0, 0, 0.97618, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0119, 0, 0, 0, 0, 0.9762, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
35.416667% {
- transform: matrix3d(1.00526, 0, 0, 0, 0, 0.98122, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0053, 0, 0, 0, 0, 0.9812, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
37.5% {
- transform: matrix3d(1, 0, 0, 0, 0, 0.98773, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 0.9877, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
39.583333% {
- transform: matrix3d(0.99617, 0, 0, 0, 0, 0.99433, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9962, 0, 0, 0, 0, 0.9943, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
41.666667% {
- transform: matrix3d(0.99368, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9937, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
43.75% {
- transform: matrix3d(0.99237, 0, 0, 0, 0, 1.00413, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0041, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
45.833333% {
- transform: matrix3d(0.99202, 0, 0, 0, 0, 1.00651, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.992, 0, 0, 0, 0, 1.0065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
47.916667% {
- transform: matrix3d(0.99241, 0, 0, 0, 0, 1.00726, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0073, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
50% {
- transform: matrix3d(0.99329, 0, 0, 0, 0, 1.00671, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9933, 0, 0, 0, 0, 1.0067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
52.083333% {
- transform: matrix3d(0.99447, 0, 0, 0, 0, 1.00529, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9945, 0, 0, 0, 0, 1.0053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
54.166667% {
- transform: matrix3d(0.99577, 0, 0, 0, 0, 1.00346, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9958, 0, 0, 0, 0, 1.0035, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
56.25% {
- transform: matrix3d(0.99705, 0, 0, 0, 0, 1.0016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.997, 0, 0, 0, 0, 1.002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
58.333333% {
- transform: matrix3d(0.99822, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
60.416667% {
- transform: matrix3d(0.99921, 0, 0, 0, 0, 0.99884, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9992, 0, 0, 0, 0, 0.9989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
62.5% {
- transform: matrix3d(1, 0, 0, 0, 0, 0.99816, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
64.583333% {
- transform: matrix3d(1.00057, 0, 0, 0, 0, 0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0006, 0, 0, 0, 0, 0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
66.666667% {
- transform: matrix3d(1.00095, 0, 0, 0, 0, 0.99811, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.001, 0, 0, 0, 0, 0.9981, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
68.75% {
- transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
70.833333% {
- transform: matrix3d(1.00119, 0, 0, 0, 0, 0.99903, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0012, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
72.916667% {
- transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99955, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
75% {
@@ -388,47 +389,47 @@
}
77.083333% {
- transform: matrix3d(1.00083, 0, 0, 0, 0, 1.00033, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0008, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
79.166667% {
- transform: matrix3d(1.00063, 0, 0, 0, 0, 1.00052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0006, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
81.25% {
- transform: matrix3d(1.00044, 0, 0, 0, 0, 1.00058, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0004, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
83.333333% {
- transform: matrix3d(1.00027, 0, 0, 0, 0, 1.00053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0003, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
85.416667% {
- transform: matrix3d(1.00012, 0, 0, 0, 0, 1.00042, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.0001, 0, 0, 0, 0, 1.0004, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
87.5% {
- transform: matrix3d(1, 0, 0, 0, 0, 1.00027, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
89.583333% {
- transform: matrix3d(0.99991, 0, 0, 0, 0, 1.00013, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9999, 0, 0, 0, 0, 1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
91.666667% {
- transform: matrix3d(0.99986, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
93.75% {
- transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
95.833333% {
- transform: matrix3d(0.99982, 0, 0, 0, 0, 0.99985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
97.916667% {
- transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99984, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
100% {
@@ -437,162 +438,162 @@
}
.ns-effect-slide-left.ns-show {
- animation-name: animSlideElasticLeft;
+ animation-name: anim-slide-elastic-left;
animation-duration: 1s;
animation-timing-function: linear;
}
-@keyframes animSlideElasticLeft {
+@keyframes anim-slide-elastic-left {
0% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1000, 0, 0, 1);
}
1.666667% {
- transform: matrix3d(1.92933, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.26805, 0, 0, 1);
+ transform: matrix3d(1.9293, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.2681, 0, 0, 1);
}
3.333333% {
- transform: matrix3d(1.96989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.82545, 0, 0, 1);
+ transform: matrix3d(1.9699, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.8255, 0, 0, 1);
}
5% {
- transform: matrix3d(1.70901, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.26115, 0, 0, 1);
+ transform: matrix3d(1.709, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.2612, 0, 0, 1);
}
6.666667% {
- transform: matrix3d(1.4235, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.3238, 0, 0, 1);
+ transform: matrix3d(1.424, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.324, 0, 0, 1);
}
8.333333% {
- transform: matrix3d(1.21065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.29848, 0, 0, 1);
+ transform: matrix3d(1.2107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.2985, 0, 0, 1);
}
10% {
- transform: matrix3d(1.08167, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.59273, 0, 0, 1);
+ transform: matrix3d(1.0817, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.5927, 0, 0, 1);
}
11.666667% {
- transform: matrix3d(1.0165, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.72371, 0, 0, 1);
+ transform: matrix3d(1.017, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.7237, 0, 0, 1);
}
13.333333% {
- transform: matrix3d(0.99057, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.12794, 0, 0, 1);
+ transform: matrix3d(0.9906, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.1279, 0, 0, 1);
}
15% {
- transform: matrix3d(0.98478, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.86339, 0, 0, 1);
+ transform: matrix3d(0.9848, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.8634, 0, 0, 1);
}
16.666667% {
- transform: matrix3d(0.98719, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.40503, 0, 0, 1);
+ transform: matrix3d(0.9872, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.405, 0, 0, 1);
}
18.333333% {
- transform: matrix3d(0.9916, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.75275, 0, 0, 1);
+ transform: matrix3d(0.992, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.7528, 0, 0, 1);
}
20% {
- transform: matrix3d(0.99541, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.10141, 0, 0, 1);
+ transform: matrix3d(0.9954, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.1014, 0, 0, 1);
}
21.666667% {
- transform: matrix3d(0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.98271, 0, 0, 1);
+ transform: matrix3d(0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.9827, 0, 0, 1);
}
23.333333% {
- transform: matrix3d(0.99936, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.40752, 0, 0, 1);
+ transform: matrix3d(0.9994, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.4075, 0, 0, 1);
}
25% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.99558, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.9956, 0, 0, 1);
}
26.666667% {
- transform: matrix3d(1.00021, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.08575, 0, 0, 1);
+ transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.0858, 0, 0, 1);
}
28.333333% {
- transform: matrix3d(1.00022, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.82507, 0, 0, 1);
+ transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.8251, 0, 0, 1);
}
30% {
- transform: matrix3d(1.00016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.23737, 0, 0, 1);
+ transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.2374, 0, 0, 1);
}
31.666667% {
- transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.27389, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.2739, 0, 0, 1);
}
33.333333% {
- transform: matrix3d(1.00005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.84893, 0, 0, 1);
+ transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.8489, 0, 0, 1);
}
35% {
- transform: matrix3d(1.00002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.86364, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.8636, 0, 0, 1);
}
36.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.22079, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.2208, 0, 0, 1);
}
38.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16687, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1669, 0, 0, 1);
}
40% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.37284, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3728, 0, 0, 1);
}
41.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.45594, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4559, 0, 0, 1);
}
43.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.46116, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4612, 0, 0, 1);
}
45% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4214, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.421, 0, 0, 1);
}
46.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.35963, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3596, 0, 0, 1);
}
48.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.29103, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.291, 0, 0, 1);
}
50% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.22487, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.2249, 0, 0, 1);
}
51.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16624, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1662, 0, 0, 1);
}
53.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.11734, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1173, 0, 0, 1);
}
55% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.07854, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0785, 0, 0, 1);
}
56.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.04909, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0491, 0, 0, 1);
}
58.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.02773, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0277, 0, 0, 1);
}
60% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.01295, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.013, 0, 0, 1);
}
61.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00331, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0033, 0, 0, 1);
}
63.333333% {
@@ -600,67 +601,67 @@
}
65% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00559, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0056, 0, 0, 1);
}
66.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00684, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0068, 0, 0, 1);
}
68.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00692, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0069, 0, 0, 1);
}
70% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00632, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0063, 0, 0, 1);
}
71.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00539, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0054, 0, 0, 1);
}
73.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00436, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0044, 0, 0, 1);
}
75% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00337, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0034, 0, 0, 1);
}
76.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00249, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0025, 0, 0, 1);
}
78.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00176, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0018, 0, 0, 1);
}
80% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00118, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0012, 0, 0, 1);
}
81.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00074, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0007, 0, 0, 1);
}
83.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00042, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0004, 0, 0, 1);
}
85% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00019, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0002, 0, 0, 1);
}
86.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00005, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0001, 0, 0, 1);
}
88.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00004, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
}
90% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
}
91.666667% {
@@ -672,15 +673,15 @@
}
95% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00009, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
}
96.666667% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
}
98.333333% {
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00007, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
}
100% {
@@ -689,11 +690,11 @@
}
.ns-effect-slide-left.ns-hide {
- animation-name: animSlideLeft;
+ animation-name: anim-slide-left;
animation-duration: 0.25s;
}
-@keyframes animSlideLeft {
+@keyframes anim-slide-left {
0% {
transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0);
}
@@ -704,10 +705,10 @@
}
.ns-effect-slide-right.ns-show {
- animation: animSlideElasticRight 2000ms linear both;
+ animation: anim-slide-elastic-right 2000ms linear both;
}
-@keyframes animSlideElasticRight {
+@keyframes anim-slide-elastic-right {
0% {
transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1);
}
@@ -786,11 +787,11 @@
}
.ns-effect-slide-right.ns-hide {
- animation-name: animSlideRight;
+ animation-name: anim-slide-right;
animation-duration: 0.25s;
}
-@keyframes animSlideRight {
+@keyframes anim-slide-right {
0% {
transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0);
}
@@ -801,10 +802,10 @@
}
.ns-effect-slide-center.ns-show {
- animation: animSlideElasticCenter 2000ms linear both;
+ animation: anim-slide-elastic-center 2000ms linear both;
}
-@keyframes animSlideElasticCenter {
+@keyframes anim-slide-elastic-center {
0% {
transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1);
}
@@ -883,11 +884,11 @@
}
.ns-effect-slide-center.ns-hide {
- animation-name: animSlideCenter;
+ animation-name: anim-slide-center;
animation-duration: 0.25s;
}
-@keyframes animSlideCenter {
+@keyframes anim-slide-center {
0% {
transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0);
}
@@ -899,11 +900,11 @@
.ns-effect-genie.ns-show,
.ns-effect-genie.ns-hide {
- animation-name: animGenie;
+ animation-name: anim-genie;
animation-duration: 0.4s;
}
-@keyframes animGenie {
+@keyframes anim-genie {
0% {
opacity: 0;
transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1);
diff --git a/modules/default/alert/templates/alert.njk b/modules/default/alert/templates/alert.njk
index a748d2684e..b7db3a4de4 100644
--- a/modules/default/alert/templates/alert.njk
+++ b/modules/default/alert/templates/alert.njk
@@ -1,18 +1,20 @@
{% if imageUrl or imageFA %}
- {% set imageHeight = imageHeight if imageHeight else "80px" %}
- {% if imageUrl %}
-

- {% else %}
-
- {% endif %}
-
+ {% set imageHeight = imageHeight if imageHeight else "80px" %}
+ {% if imageUrl %}
+

+ {% else %}
+
+ {% endif %}
+
{% endif %}
{% if title %}
-
{{ title }}
+
{{ title if titleType == 'text' else title | safe }}
{% endif %}
{% if message %}
- {% if title %}
-
- {% endif %}
-
{{ message }}
+ {% if title %}
{% endif %}
+
{{ message if messageType == 'text' else message | safe }}
{% endif %}
diff --git a/modules/default/alert/templates/notification.njk b/modules/default/alert/templates/notification.njk
index 1d67bcda7d..0de6908b26 100644
--- a/modules/default/alert/templates/notification.njk
+++ b/modules/default/alert/templates/notification.njk
@@ -1,9 +1,7 @@
{% if title %}
-
{{ title }}
+
{{ title if titleType == 'text' else title | safe }}
{% endif %}
{% if message %}
- {% if title %}
-
- {% endif %}
-
{{ message }}
+ {% if title %}
{% endif %}
+
{{ message if messageType == 'text' else message | safe }}
{% endif %}
diff --git a/modules/default/alert/translations/th.json b/modules/default/alert/translations/th.json
new file mode 100644
index 0000000000..a1894bf3f1
--- /dev/null
+++ b/modules/default/alert/translations/th.json
@@ -0,0 +1,4 @@
+{
+ "sysTitle": "การแจ้งเตือน MagicMirror²",
+ "welcome": "ยินดีต้อนรับ การเริ่มต้นสำเร็จแล้ว!"
+}
diff --git a/modules/default/calendar/calendar.css b/modules/default/calendar/calendar.css
index f04d683872..f8e3bd7929 100644
--- a/modules/default/calendar/calendar.css
+++ b/modules/default/calendar/calendar.css
@@ -14,6 +14,7 @@
.calendar .title {
padding-left: 0;
padding-right: 0;
+ vertical-align: top;
}
.calendar .time {
diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js
index 3694a21603..4b0409c01c 100644
--- a/modules/default/calendar/calendar.js
+++ b/modules/default/calendar/calendar.js
@@ -1,17 +1,12 @@
-/* global cloneObject */
-
-/* MagicMirror²
- * Module: Calendar
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
+/* global CalendarUtils */
+
Module.register("calendar", {
// Define module defaults
defaults: {
maximumEntries: 10, // Total Maximum Entries
maximumNumberOfDays: 365,
limitDays: 0, // Limit the number of days shown, 0 = no limit
+ pastDaysCount: 0,
displaySymbol: true,
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
defaultSymbolClassName: "fas fa-fw fa-",
@@ -20,28 +15,29 @@ Module.register("calendar", {
defaultRepeatingCountTitle: "",
maxTitleLength: 25,
maxLocationTitleLength: 25,
- wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
+ wrapEvents: false, // Wrap events to multiple lines breaking at maxTitleLength
wrapLocationEvents: false,
maxTitleLines: 3,
maxEventTitleLines: 3,
- fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
+ fetchInterval: 60 * 60 * 1000, // Update every hour
animationSpeed: 2000,
fade: true,
+ fadePoint: 0.25, // Start on 1/4th of the list.
urgency: 7,
timeFormat: "relative",
dateFormat: "MMM Do",
dateEndFormat: "LT",
fullDayEventDateFormat: "MMM Do",
showEnd: false,
+ showEndsOnlyWithDuration: false,
getRelative: 6,
- fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false,
hideOngoing: false,
hideTime: false,
+ hideDuplicates: true,
showTimeToday: false,
colored: false,
- coloredSymbolOnly: false,
- customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
+ forceUseCurrentTime: false,
tableClass: "small",
calendars: [
{
@@ -49,10 +45,11 @@ Module.register("calendar", {
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
}
],
- titleReplace: {
- "De verjaardag van ": "",
- "'s birthday": ""
- },
+ customEvents: [
+ // Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
+ { keyword: ".*", transform: { search: "De verjaardag van ", replace: "" } },
+ { keyword: ".*", transform: { search: "'s birthday", replace: "" } }
+ ],
locationTitleReplace: {
"street ": ""
},
@@ -61,23 +58,30 @@ Module.register("calendar", {
sliceMultiDayEvents: false,
broadcastPastEvents: false,
nextDaysRelative: false,
- selfSignedCert: false
+ selfSignedCert: false,
+ coloredText: false,
+ coloredBorder: false,
+ coloredSymbol: false,
+ coloredBackground: false,
+ limitDaysNeverSkip: false,
+ flipDateHeaderTitle: false,
+ updateOnFetch: true
},
requiresVersion: "2.1.0",
// Define required scripts.
- getStyles: function () {
+ getStyles () {
return ["calendar.css", "font-awesome.css"];
},
// Define required scripts.
- getScripts: function () {
- return ["moment.js"];
+ getScripts () {
+ return ["calendarutils.js", "moment.js"];
},
// Define required translations.
- getTranslations: function () {
+ getTranslations () {
// The translations for the default modules are defined in the core translation files.
// Therefore we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
@@ -85,11 +89,22 @@ Module.register("calendar", {
},
// Override start method.
- start: function () {
- Log.info("Starting module: " + this.name);
+ start () {
+ Log.info(`Starting module: ${this.name}`);
+
+ if (this.config.colored) {
+ Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
+ this.config.coloredText = true;
+ this.config.coloredSymbol = true;
+ }
+ if (this.config.coloredSymbolOnly) {
+ Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
+ this.config.coloredText = false;
+ this.config.coloredSymbol = true;
+ }
// Set locale.
- moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
+ moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
// clear data holder before start
this.calendarData = {};
@@ -97,30 +112,36 @@ Module.register("calendar", {
// indicate no data available yet
this.loaded = false;
+ // data holder of calendar url. Avoid fade out/in on updateDom (one for each calendar update)
+ this.calendarDisplayer = {};
+
this.config.calendars.forEach((calendar) => {
calendar.url = calendar.url.replace("webcal://", "http://");
const calendarConfig = {
maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays,
+ pastDaysCount: calendar.pastDaysCount,
broadcastPastEvents: calendar.broadcastPastEvents,
- selfSignedCert: calendar.selfSignedCert
+ selfSignedCert: calendar.selfSignedCert,
+ excludedEvents: calendar.excludedEvents,
+ fetchInterval: calendar.fetchInterval
};
- if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
+ if (typeof calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
calendarConfig.symbolClass = "";
}
- if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
+ if (typeof calendar.titleClass === "undefined" || calendar.titleClass === null) {
calendarConfig.titleClass = "";
}
- if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
+ if (typeof calendar.timeClass === "undefined" || calendar.timeClass === null) {
calendarConfig.timeClass = "";
}
// we check user and password here for backwards compatibility with old configs
if (calendar.user && calendar.pass) {
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
- Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
+ Log.warn("https://docs.magicmirror.builders/modules/calendar.html#configuration-options");
calendar.auth = {
user: calendar.user,
pass: calendar.pass
@@ -131,10 +152,20 @@ Module.register("calendar", {
// fetcher till cycle
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
});
+
+ // for backward compatibility titleReplace
+ if (typeof this.config.titleReplace !== "undefined") {
+ Log.warn("Deprecation warning: Please consider upgrading your calendar titleReplace configuration to customEvents.");
+ for (const [titlesearchstr, titlereplacestr] of Object.entries(this.config.titleReplace)) {
+ this.config.customEvents.push({ keyword: ".*", transform: { search: titlesearchstr, replace: titlereplacestr } });
+ }
+ }
+
+ this.selfUpdate();
},
// Override socket notification handler.
- socketNotificationReceived: function (notification, payload) {
+ socketNotificationReceived (notification, payload) {
if (notification === "FETCH_CALENDAR") {
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
}
@@ -152,6 +183,18 @@ Module.register("calendar", {
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
+
+ if (!this.config.updateOnFetch) {
+ if (this.calendarDisplayer[payload.url] === undefined) {
+ // calendar will never displayed, so display it
+ this.updateDom(this.config.animationSpeed);
+ // set this calendar as displayed
+ this.calendarDisplayer[payload.url] = true;
+ } else {
+ Log.debug("[Calendar] DOM not updated waiting self update()");
+ }
+ return;
+ }
}
} else if (notification === "CALENDAR_ERROR") {
let error_message = this.translate(payload.error_type);
@@ -162,8 +205,13 @@ Module.register("calendar", {
this.updateDom(this.config.animationSpeed);
},
+ eventEndingWithinNextFullTimeUnit (event, ONE_DAY) {
+ const now = new Date();
+ return event.endDate - now <= ONE_DAY;
+ },
+
// Override dom generator.
- getDom: function () {
+ getDom () {
const ONE_SECOND = 1000; // 1,000 milliseconds
const ONE_MINUTE = ONE_SECOND * 60;
const ONE_HOUR = ONE_MINUTE * 60;
@@ -175,13 +223,13 @@ Module.register("calendar", {
if (this.error) {
wrapper.innerHTML = this.error;
- wrapper.className = this.config.tableClass + " dimmed";
+ wrapper.className = `${this.config.tableClass} dimmed`;
return wrapper;
}
if (events.length === 0) {
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
- wrapper.className = this.config.tableClass + " dimmed";
+ wrapper.className = `${this.config.tableClass} dimmed`;
return wrapper;
}
@@ -204,9 +252,12 @@ Module.register("calendar", {
if (this.config.timeFormat === "dateheaders") {
if (lastSeenDate !== dateAsString) {
const dateRow = document.createElement("tr");
- dateRow.className = "normal";
+ dateRow.className = "dateheader normal";
if (event.today) dateRow.className += " today";
+ else if (event.dayBeforeYesterday) dateRow.className += " dayBeforeYesterday";
+ else if (event.yesterday) dateRow.className += " yesterday";
else if (event.tomorrow) dateRow.className += " tomorrow";
+ else if (event.dayAfterTomorrow) dateRow.className += " dayAfterTomorrow";
const dateCell = document.createElement("td");
dateCell.colSpan = "3";
@@ -227,23 +278,34 @@ Module.register("calendar", {
const eventWrapper = document.createElement("tr");
- if (this.config.colored && !this.config.coloredSymbolOnly) {
- eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
+ if (this.config.coloredText) {
+ eventWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
}
- eventWrapper.className = "normal event";
+ if (this.config.coloredBackground) {
+ eventWrapper.style.backgroundColor = this.colorForUrl(event.url, true);
+ }
+
+ if (this.config.coloredBorder) {
+ eventWrapper.style.borderColor = this.colorForUrl(event.url, false);
+ }
+
+ eventWrapper.className = "event-wrapper normal event";
if (event.today) eventWrapper.className += " today";
+ else if (event.dayBeforeYesterday) eventWrapper.className += " dayBeforeYesterday";
+ else if (event.yesterday) eventWrapper.className += " yesterday";
else if (event.tomorrow) eventWrapper.className += " tomorrow";
+ else if (event.dayAfterTomorrow) eventWrapper.className += " dayAfterTomorrow";
const symbolWrapper = document.createElement("td");
if (this.config.displaySymbol) {
- if (this.config.colored && this.config.coloredSymbolOnly) {
- symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
+ if (this.config.coloredSymbol) {
+ symbolWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
}
const symbolClass = this.symbolClassForUrl(event.url);
- symbolWrapper.className = "symbol align-right " + symbolClass;
+ symbolWrapper.className = `symbol align-right ${symbolClass}`;
const symbols = this.symbolsForEvent(event);
symbols.forEach((s, index) => {
@@ -271,60 +333,74 @@ Module.register("calendar", {
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear;
- repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
+ repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
}
}
- // Color events if custom color is specified
+ var transformedTitle = event.title;
+
+ // Color events if custom color or eventClass are specified, transform title if required
if (this.config.customEvents.length > 0) {
for (let ev in this.config.customEvents) {
- if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
- let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
- if (needle.test(event.title)) {
+ let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
+ if (needle.test(event.title)) {
+ if (typeof this.config.customEvents[ev].transform === "object") {
+ transformedTitle = CalendarUtils.titleTransform(transformedTitle, [this.config.customEvents[ev].transform]);
+ }
+ if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
// Respect parameter ColoredSymbolOnly also for custom events
- if (!this.config.coloredSymbolOnly) {
- eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
- titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
+ if (this.config.coloredText) {
+ eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
+ titleWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
}
- if (this.config.displaySymbol) {
- symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
+ if (this.config.displaySymbol && this.config.coloredSymbol) {
+ symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
}
- break;
+ }
+ if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
+ eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
}
}
}
}
- titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
+ titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
const titleClass = this.titleClassForUrl(event.url);
- if (!this.config.colored) {
- titleWrapper.className = "title bright " + titleClass;
+ if (!this.config.coloredText) {
+ titleWrapper.className = `title bright ${titleClass}`;
} else {
- titleWrapper.className = "title " + titleClass;
+ titleWrapper.className = `title ${titleClass}`;
}
if (this.config.timeFormat === "dateheaders") {
+ if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
+
if (event.fullDayEvent) {
titleWrapper.colSpan = "2";
titleWrapper.classList.add("align-left");
} else {
const timeWrapper = document.createElement("td");
- timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url);
+ timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
timeWrapper.style.paddingLeft = "2px";
+ timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
// Add endDate to dataheaders if showEnd is enabled
if (this.config.showEnd) {
- timeWrapper.innerHTML += " - " + moment(event.endDate, "x").format("LT");
+ if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
+ // no duration here, don't display end
+ } else {
+ timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
+ }
}
eventWrapper.appendChild(timeWrapper);
- titleWrapper.classList.add("align-right");
- }
- eventWrapper.appendChild(titleWrapper);
+ if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right");
+ }
+ if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
} else {
const timeWrapper = document.createElement("td");
@@ -333,52 +409,58 @@ Module.register("calendar", {
if (this.config.timeFormat === "absolute") {
// Use dateFormat
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
// Add end time if showEnd
if (this.config.showEnd) {
- timeWrapper.innerHTML += "-";
- timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
+ if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
+ // no duration here, don't display end
+ } else {
+ timeWrapper.innerHTML += "-";
+ timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
+ }
}
// For full day events we use the fullDayEventDateFormat
if (event.fullDayEvent) {
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
event.endDate -= ONE_SECOND;
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
} else if (this.config.getRelative > 0 && event.startDate < now) {
// Ongoing and getRelative is set
- timeWrapper.innerHTML = this.capFirst(
+ timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
- fallback: this.translate("RUNNING") + " {timeUntilEnd}",
+ fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
})
);
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
// Within urgency days
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
}
if (event.fullDayEvent && this.config.nextDaysRelative) {
// Full days events within the next two days
if (event.today) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
+ } else if (event.yesterday) {
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
- timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
}
} else {
// Show relative times
- if (event.startDate >= now || (event.fullDayEvent && event.today)) {
+ if (event.startDate >= now || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) {
// Use relative time
if (!this.config.hideTime && !event.fullDayEvent) {
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
} else {
- timeWrapper.innerHTML = this.capFirst(
+ timeWrapper.innerHTML = CalendarUtils.capFirst(
moment(event.startDate, "x").calendar(null, {
- sameDay: this.config.showTimeToday ? "LT" : "[" + this.translate("TODAY") + "]",
- nextDay: "[" + this.translate("TOMORROW") + "]",
+ sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
+ nextDay: `[${this.translate("TOMORROW")}]`,
nextWeek: "dddd",
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
})
@@ -386,57 +468,79 @@ Module.register("calendar", {
}
if (event.fullDayEvent) {
// Full days events within the next two days
- if (event.today) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
+ if (event.today || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) {
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
+ } else if (event.dayBeforeYesterday) {
+ if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
+ }
+ } else if (event.yesterday) {
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
- timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
}
} else {
// Ongoing event
- timeWrapper.innerHTML = this.capFirst(
+ timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
- fallback: this.translate("RUNNING") + " {timeUntilEnd}",
+ fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
})
);
}
}
- timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
+ timeWrapper.className = `time light ${this.timeClassForUrl(event.url)}`;
eventWrapper.appendChild(timeWrapper);
}
- wrapper.appendChild(eventWrapper);
-
// Create fade effect.
if (index >= startFade) {
currentFadeStep = index - startFade;
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
}
+ wrapper.appendChild(eventWrapper);
if (this.config.showLocation) {
if (event.location !== false) {
const locationRow = document.createElement("tr");
- locationRow.className = "normal xsmall light";
+ locationRow.className = "event-wrapper-location normal xsmall light";
if (event.today) locationRow.className += " today";
+ else if (event.dayBeforeYesterday) locationRow.className += " dayBeforeYesterday";
+ else if (event.yesterday) locationRow.className += " yesterday";
else if (event.tomorrow) locationRow.className += " tomorrow";
+ else if (event.dayAfterTomorrow) locationRow.className += " dayAfterTomorrow";
if (this.config.displaySymbol) {
const symbolCell = document.createElement("td");
locationRow.appendChild(symbolCell);
}
+ if (this.config.coloredText) {
+ locationRow.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
+ }
+
+ if (this.config.coloredBackground) {
+ locationRow.style.backgroundColor = this.colorForUrl(event.url, true);
+ }
+
+ if (this.config.coloredBorder) {
+ locationRow.style.borderColor = this.colorForUrl(event.url, false);
+ }
+
const descCell = document.createElement("td");
descCell.className = "location";
descCell.colSpan = "2";
- descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
+
+ const transformedTitle = CalendarUtils.titleTransform(event.location, this.config.locationTitleReplace);
+ descCell.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxLocationTitleLength, this.config.wrapLocationEvents, this.config.maxEventTitleLines);
locationRow.appendChild(descCell);
wrapper.appendChild(locationRow);
@@ -452,35 +556,12 @@ Module.register("calendar", {
return wrapper;
},
- /**
- * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
- * corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
- * it will a localeSpecification object with the system locale time format.
- *
- * @param {number} timeFormat Specifies either 12 or 24 hour time format
- * @returns {moment.LocaleSpecification} formatted time
- */
- getLocaleSpecification: function (timeFormat) {
- switch (timeFormat) {
- case 12: {
- return { longDateFormat: { LT: "h:mm A" } };
- }
- case 24: {
- return { longDateFormat: { LT: "HH:mm" } };
- }
- default: {
- return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
- }
- }
- },
-
/**
* Checks if this config contains the calendar url.
- *
* @param {string} url The calendar url
* @returns {boolean} True if the calendar config contains the url, False otherwise
*/
- hasCalendarURL: function (url) {
+ hasCalendarURL (url) {
for (const calendar of this.config.calendars) {
if (calendar.url === url) {
return true;
@@ -492,24 +573,31 @@ Module.register("calendar", {
/**
* Creates the sorted list of all events.
- *
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
* @returns {object[]} Array with events.
*/
- createEventList: function (limitNumberOfEntries) {
+ createEventList (limitNumberOfEntries) {
const ONE_SECOND = 1000; // 1,000 milliseconds
const ONE_MINUTE = ONE_SECOND * 60;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
- const now = new Date();
- const today = moment().startOf("day");
- const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
+ let now, today, future;
+ if (this.config.forceUseCurrentTime || this.defaults.forceUseCurrentTime) {
+ now = new Date();
+ today = moment().startOf("day");
+ future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
+ } else {
+ now = new Date(Date.now()); // Can use overridden time
+ today = moment(now).startOf("day");
+ future = moment(now).startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
+ }
let events = [];
for (const calendarUrl in this.calendarData) {
const calendar = this.calendarData[calendarUrl];
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
+ let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
for (const e in calendar) {
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
@@ -518,22 +606,26 @@ Module.register("calendar", {
continue;
}
if (limitNumberOfEntries) {
- if (event.endDate < now) {
+ if (event.endDate < maxPastDaysCompare) {
continue;
}
if (this.config.hideOngoing && event.startDate < now) {
continue;
}
- if (this.listContainsEvent(events, event)) {
+ if (this.config.hideDuplicates && this.listContainsEvent(events, event)) {
continue;
}
if (--remainingEntries < 0) {
break;
}
}
+
event.url = calendarUrl;
event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
+ event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY;
+ event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today;
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
+ event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
@@ -541,14 +633,19 @@ Module.register("calendar", {
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
if (this.config.sliceMultiDayEvents && maxCount > 1) {
const splitEvents = [];
- let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
+ let midnight
+ = moment(event.startDate, "x")
+ .clone()
+ .startOf("day")
+ .add(1, "day")
+ .format("x");
let count = 1;
while (event.endDate > midnight) {
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
thisEvent.endDate = midnight;
- thisEvent.title += " (" + count + "/" + maxCount + ")";
+ thisEvent.title += ` (${count}/${maxCount})`;
splitEvents.push(thisEvent);
event.startDate = midnight;
@@ -556,7 +653,7 @@ Module.register("calendar", {
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
}
// Last day
- event.title += " (" + count + "/" + maxCount + ")";
+ event.title += ` (${count}/${maxCount})`;
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
splitEvents.push(event);
@@ -592,7 +689,7 @@ Module.register("calendar", {
// check if we already are showing max unique days
if (eventDate > lastDate) {
// if the only entry in the first day is a full day event that day is not counted as unique
- if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
+ if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
days--;
}
days++;
@@ -610,9 +707,9 @@ Module.register("calendar", {
return events.slice(0, this.config.maximumEntries);
},
- listContainsEvent: function (eventList, event) {
+ listContainsEvent (eventList, event) {
for (const evt of eventList) {
- if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
+ if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate) && parseInt(evt.endDate) === parseInt(event.endDate)) {
return true;
}
}
@@ -621,19 +718,19 @@ Module.register("calendar", {
/**
* Requests node helper to add calendar url.
- *
* @param {string} url The calendar url to add
* @param {object} auth The authentication method and credentials
* @param {object} calendarConfig The config of the specific calendar
*/
- addCalendar: function (url, auth, calendarConfig) {
+ addCalendar (url, auth, calendarConfig) {
this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier,
url: url,
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
- fetchInterval: this.config.fetchInterval,
+ pastDaysCount: calendarConfig.pastDaysCount || this.config.pastDaysCount,
+ fetchInterval: calendarConfig.fetchInterval || this.config.fetchInterval,
symbolClass: calendarConfig.symbolClass,
titleClass: calendarConfig.titleClass,
timeClass: calendarConfig.timeClass,
@@ -645,11 +742,10 @@ Module.register("calendar", {
/**
* Retrieves the symbols for a specific event.
- *
* @param {object} event Event to look for.
* @returns {string[]} The symbols
*/
- symbolsForEvent: function (event) {
+ symbolsForEvent (event) {
let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
@@ -665,7 +761,9 @@ Module.register("calendar", {
if (typeof ev.symbol !== "undefined" && ev.symbol !== "") {
let needle = new RegExp(ev.keyword, "gi");
if (needle.test(event.title)) {
- symbols[0] = ev.symbol;
+ // Get the default prefix for this class name and add to the custom symbol provided
+ const className = this.getCalendarProperty(event.url, "symbolClassName", this.config.defaultSymbolClassName);
+ symbols[0] = className + ev.symbol;
break;
}
}
@@ -674,7 +772,7 @@ Module.register("calendar", {
return symbols;
},
- mergeUnique: function (arr1, arr2) {
+ mergeUnique (arr1, arr2) {
return arr1.concat(
arr2.filter(function (item) {
return arr1.indexOf(item) === -1;
@@ -684,83 +782,85 @@ Module.register("calendar", {
/**
* Retrieves the symbolClass for a specific calendar url.
- *
* @param {string} url The calendar url
* @returns {string} The class to be used for the symbols of the calendar
*/
- symbolClassForUrl: function (url) {
+ symbolClassForUrl (url) {
return this.getCalendarProperty(url, "symbolClass", "");
},
/**
* Retrieves the titleClass for a specific calendar url.
- *
* @param {string} url The calendar url
* @returns {string} The class to be used for the title of the calendar
*/
- titleClassForUrl: function (url) {
+ titleClassForUrl (url) {
return this.getCalendarProperty(url, "titleClass", "");
},
/**
* Retrieves the timeClass for a specific calendar url.
- *
* @param {string} url The calendar url
* @returns {string} The class to be used for the time of the calendar
*/
- timeClassForUrl: function (url) {
+ timeClassForUrl (url) {
return this.getCalendarProperty(url, "timeClass", "");
},
/**
* Retrieves the calendar name for a specific calendar url.
- *
* @param {string} url The calendar url
* @returns {string} The name of the calendar
*/
- calendarNameForUrl: function (url) {
+ calendarNameForUrl (url) {
return this.getCalendarProperty(url, "name", "");
},
/**
* Retrieves the color for a specific calendar url.
- *
* @param {string} url The calendar url
+ * @param {boolean} isBg Determines if we fetch the bgColor or not
* @returns {string} The color
*/
- colorForUrl: function (url) {
- return this.getCalendarProperty(url, "color", "#fff");
+ colorForUrl (url, isBg) {
+ return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
},
/**
* Retrieves the count title for a specific calendar url.
- *
* @param {string} url The calendar url
* @returns {string} The title
*/
- countTitleForUrl: function (url) {
+ countTitleForUrl (url) {
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
},
/**
* Retrieves the maximum entry count for a specific calendar url.
- *
* @param {string} url The calendar url
* @returns {number} The maximum entry count
*/
- maximumEntriesForUrl: function (url) {
+ maximumEntriesForUrl (url) {
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
},
+ /**
+ * Retrieves the maximum count of past days which events of should be displayed for a specific calendar url.
+ * @param {string} url The calendar url
+ * @returns {number} The maximum past days count
+ */
+ maximumPastDaysForUrl (url) {
+ return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
+ },
+
/**
* Helper method to retrieve the property for a specific calendar url.
- *
* @param {string} url The calendar url
* @param {string} property The property to look for
* @param {string} defaultValue The value if the property is not found
* @returns {*} The property
*/
- getCalendarProperty: function (url, property, defaultValue) {
+ getCalendarProperty (url, property, defaultValue) {
for (const calendar of this.config.calendars) {
if (calendar.url === url && calendar.hasOwnProperty(property)) {
return calendar[property];
@@ -770,7 +870,7 @@ Module.register("calendar", {
return defaultValue;
},
- getCalendarPropertyAsArray: function (url, property, defaultValue) {
+ getCalendarPropertyAsArray (url, property, defaultValue) {
let p = this.getCalendarProperty(url, property, defaultValue);
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
@@ -781,115 +881,48 @@ Module.register("calendar", {
return p;
},
- hasCalendarProperty: function (url, property) {
+ hasCalendarProperty (url, property) {
return !!this.getCalendarProperty(url, property, undefined);
},
- /**
- * Shortens a string if it's longer than maxLength and add a ellipsis to the end
- *
- * @param {string} string Text string to shorten
- * @param {number} maxLength The max length of the string
- * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
- * @param {number} maxTitleLines The max number of vertical lines before cutting event title
- * @returns {string} The shortened string
- */
- shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
- if (typeof string !== "string") {
- return "";
- }
-
- if (wrapEvents === true) {
- const words = string.split(" ");
- let temp = "";
- let currentLine = "";
- let line = 0;
-
- for (let i = 0; i < words.length; i++) {
- const word = words[i];
- if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
- // max - 1 to account for a space
- currentLine += word + " ";
- } else {
- line++;
- if (line > maxTitleLines - 1) {
- if (i < words.length) {
- currentLine += "…";
- }
- break;
- }
-
- if (currentLine.length > 0) {
- temp += currentLine + "
" + word + " ";
- } else {
- temp += word + "
";
- }
- currentLine = "";
- }
- }
-
- return (temp + currentLine).trim();
- } else {
- if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
- return string.trim().slice(0, maxLength) + "…";
- } else {
- return string.trim();
- }
- }
- },
-
- /**
- * Capitalize the first letter of a string
- *
- * @param {string} string The string to capitalize
- * @returns {string} The capitalized string
- */
- capFirst: function (string) {
- return string.charAt(0).toUpperCase() + string.slice(1);
- },
-
- /**
- * Transforms the title of an event for usage.
- * Replaces parts of the text as defined in config.titleReplace.
- * Shortens title based on config.maxTitleLength and config.wrapEvents
- *
- * @param {string} title The title to transform.
- * @param {object} titleReplace Pairs of strings to be replaced in the title
- * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
- * @param {number} maxTitleLength The max length of the string
- * @param {number} maxTitleLines The max number of vertical lines before cutting event title
- * @returns {string} The transformed title.
- */
- titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
- for (let needle in titleReplace) {
- const replacement = titleReplace[needle];
-
- const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
- if (regParts) {
- // the parsed pattern is a regexp.
- needle = new RegExp(regParts[1], regParts[2]);
- }
-
- title = title.replace(needle, replacement);
- }
-
- title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
- return title;
- },
-
/**
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.
*/
- broadcastEvents: function () {
+ broadcastEvents () {
const eventList = this.createEventList(false);
for (const event of eventList) {
event.symbol = this.symbolsForEvent(event);
event.calendarName = this.calendarNameForUrl(event.url);
- event.color = this.colorForUrl(event.url);
+ event.color = this.colorForUrl(event.url, false);
delete event.url;
}
this.sendNotification("CALENDAR_EVENTS", eventList);
+ },
+
+ /**
+ * Refresh the DOM every minute if needed: When using relative date format for events that start
+ * or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
+ * --
+ * When updateOnFetch is not set, it will Avoid fade out/in on updateDom when many calendars are used
+ * and it's allow to refresh The DOM every minute with animation speed too
+ * (because updateDom is not set in CALENDAR_EVENTS for this case)
+ */
+ selfUpdate () {
+ const ONE_MINUTE = 60 * 1000;
+ setTimeout(
+ () => {
+ setInterval(() => {
+ Log.debug("[Calendar] self update");
+ if (this.config.updateOnFetch) {
+ this.updateDom(1);
+ } else {
+ this.updateDom(this.config.animationSpeed);
+ }
+ }, ONE_MINUTE);
+ },
+ ONE_MINUTE - (new Date() % ONE_MINUTE)
+ );
}
});
diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js
index 0798d1785c..824c7c5b4f 100644
--- a/modules/default/calendar/calendarfetcher.js
+++ b/modules/default/calendar/calendarfetcher.js
@@ -1,16 +1,8 @@
-/* MagicMirror²
- * Node Helper: Calendar - CalendarFetcher
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
-const CalendarUtils = require("./calendarutils");
+const https = require("node:https");
+const ical = require("node-ical");
const Log = require("logger");
const NodeHelper = require("node_helper");
-const ical = require("node-ical");
-const fetch = require("fetch");
-const digest = require("digest-fetch");
-const https = require("https");
+const CalendarFetcherUtils = require("./calendarfetcherutils");
/**
*
@@ -38,10 +30,9 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
clearTimeout(reloadTimer);
reloadTimer = null;
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
- let fetcher = null;
let httpsAgent = null;
let headers = {
- "User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version
+ "User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
};
if (selfSignedCert) {
@@ -51,18 +42,13 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
}
if (auth) {
if (auth.method === "bearer") {
- headers.Authorization = "Bearer " + auth.pass;
- } else if (auth.method === "digest") {
- fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
+ headers.Authorization = `Bearer ${auth.pass}`;
} else {
- headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
+ headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
}
}
- if (fetcher === null) {
- fetcher = fetch(url, { headers: headers, agent: httpsAgent });
- }
- fetcher
+ fetch(url, { headers: headers, agent: httpsAgent })
.then(NodeHelper.checkFetchStatus)
.then((response) => response.text())
.then((responseData) => {
@@ -70,8 +56,8 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try {
data = ical.parseICS(responseData);
- Log.debug("parsed data=" + JSON.stringify(data));
- events = CalendarUtils.filterEvents(data, {
+ Log.debug(`parsed data=${JSON.stringify(data)}`);
+ events = CalendarFetcherUtils.filterEvents(data, {
excludedEvents,
includePastEvents,
maximumEntries,
@@ -114,13 +100,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
* Broadcast the existing events.
*/
this.broadcastEvents = function () {
- Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
+ Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`);
eventsReceivedCallback(this);
};
/**
* Sets the on success callback
- *
* @param {Function} callback The on success callback.
*/
this.onReceive = function (callback) {
@@ -129,7 +114,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Sets the on error callback
- *
* @param {Function} callback The on error callback.
*/
this.onError = function (callback) {
@@ -138,7 +122,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Returns the url of this fetcher.
- *
* @returns {string} The url of this fetcher.
*/
this.url = function () {
@@ -147,7 +130,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Returns current available events for this fetcher.
- *
* @returns {object[]} The current available events for this fetcher.
*/
this.events = function () {
diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js
new file mode 100644
index 0000000000..eec3540f29
--- /dev/null
+++ b/modules/default/calendar/calendarfetcherutils.js
@@ -0,0 +1,585 @@
+/**
+ * @external Moment
+ */
+const path = require("node:path");
+const moment = require("moment");
+
+const zoneTable = require(path.join(__dirname, "windowsZones.json"));
+const Log = require("../../../js/logger");
+
+const CalendarFetcherUtils = {
+
+ /**
+ * Calculate the time correction, either dst/std or full day in cases where
+ * utc time is day before plus offset
+ * @param {object} event the event which needs adjustment
+ * @param {Date} date the date on which this event happens
+ * @returns {number} the necessary adjustment in hours
+ */
+ calculateTimezoneAdjustment (event, date) {
+ let adjustHours = 0;
+ // if a timezone was specified
+ if (!event.start.tz) {
+ Log.debug(" if no tz, guess based on now");
+ event.start.tz = moment.tz.guess();
+ }
+ Log.debug(`initial tz=${event.start.tz}`);
+
+ // if there is a start date specified
+ if (event.start.tz) {
+ // if this is a windows timezone
+ if (event.start.tz.includes(" ")) {
+ // use the lookup table to get theIANA name as moment and date don't know MS timezones
+ let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz);
+ Log.debug(`corrected TZ=${tz}`);
+ // watch out for unregistered windows timezone names
+ // if we had a successful lookup
+ if (tz) {
+ // change the timezone to the IANA name
+ event.start.tz = tz;
+ // Log.debug("corrected timezone="+event.start.tz)
+ }
+ }
+ Log.debug(`corrected tz=${event.start.tz}`);
+ let current_offset = 0; // offset from TZ string or calculated
+ let mm = 0; // date with tz or offset
+ let start_offset = 0; // utc offset of created with tz
+ // if there is still an offset, lookup failed, use it
+ if (event.start.tz.startsWith("(")) {
+ const regex = /[+|-]\d*:\d*/;
+ const start_offsetString = event.start.tz.match(regex).toString().split(":");
+ let start_offset = parseInt(start_offsetString[0]);
+ start_offset *= event.start.tz[1] === "-" ? -1 : 1;
+ adjustHours = start_offset;
+ Log.debug(`defined offset=${start_offset} hours`);
+ current_offset = start_offset;
+ event.start.tz = "";
+ Log.debug(`ical offset=${current_offset} date=${date}`);
+ mm = moment(date);
+ let x = moment(new Date()).utcOffset();
+ Log.debug(`net mins=${current_offset * 60 - x}`);
+
+ mm = mm.add(x - current_offset * 60, "minutes");
+ adjustHours = (current_offset * 60 - x) / 60;
+ event.start = mm.toDate();
+ Log.debug(`adjusted date=${event.start}`);
+ } else {
+ // get the start time in that timezone
+ let es = moment(event.start);
+ // check for start date prior to start of daylight changing date
+ if (es.format("YYYY") < 2007) {
+ es.set("year", 2013); // if so, use a closer date
+ }
+ Log.debug(`start date/time=${es.toDate()}`);
+ start_offset = moment.tz(es, event.start.tz).utcOffset();
+ Log.debug(`start offset=${start_offset}`);
+
+ Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
+
+ // get the specified date in that timezone
+ mm = moment.tz(moment(date), event.start.tz);
+ Log.debug(`event date=${mm.toDate()}`);
+ current_offset = mm.utcOffset();
+ }
+ Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
+
+ // if the offset is greater than 0, east of london
+ if (current_offset !== start_offset) {
+ // big offset
+ Log.debug("offset");
+ let h = parseInt(mm.format("H"));
+ // check if the event time is less than the offset
+ if (h > 0 && h < Math.abs(current_offset) / 60) {
+ // if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
+ // we need to fix that
+ //adjustHours = 24;
+ // Log.debug("adjusting date")
+ }
+ //-300 > -240
+ //if (Math.abs(current_offset) > Math.abs(start_offset)){
+ if (current_offset > start_offset) {
+ adjustHours -= 1;
+ Log.debug("adjust down 1 hour dst change");
+ //} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
+ } else if (current_offset < start_offset) {
+ adjustHours += 1;
+ Log.debug("adjust up 1 hour dst change");
+ }
+ }
+ }
+ Log.debug(`adjustHours=${adjustHours}`);
+ return adjustHours;
+ },
+
+ /**
+ * Filter the events from ical according to the given config
+ * @param {object} data the calendar data from ical
+ * @param {object} config The configuration object
+ * @returns {string[]} the filtered events
+ */
+ filterEvents (data, config) {
+ const newEvents = [];
+
+ // limitFunction doesn't do much limiting, see comment re: the dates
+ // array in rrule section below as to why we need to do the filtering
+ // ourselves
+ const limitFunction = function (date, i) {
+ return true;
+ };
+
+ const eventDate = function (event, time) {
+ return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time]).startOf("day") : moment(event[time]);
+ };
+
+ Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
+
+ const now = new Date(Date.now());
+ const todayLocal = moment(now).startOf("day").toDate();
+ const futureLocalDate
+ = moment(now)
+ .startOf("day")
+ .add(config.maximumNumberOfDays, "days")
+ .subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat.
+ .toDate();
+
+ Object.entries(data).forEach(([key, event]) => {
+ Log.debug("Processing entry...");
+ let pastLocalDate = todayLocal;
+
+ if (config.includePastEvents) {
+ pastLocalDate = moment(now).startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
+ }
+
+ // FIXME: Ugly fix to solve the facebook birthday issue.
+ // Otherwise, the recurring events only show the birthday for next year.
+ let isFacebookBirthday = false;
+ if (typeof event.uid !== "undefined") {
+ if (event.uid.indexOf("@facebook.com") !== -1) {
+ isFacebookBirthday = true;
+ }
+ }
+
+ if (event.type === "VEVENT") {
+ Log.debug(`Event:\n${JSON.stringify(event)}`);
+ let startMoment = eventDate(event, "start");
+ let endMoment;
+
+ if (typeof event.end !== "undefined") {
+ endMoment = eventDate(event, "end");
+ } else if (typeof event.duration !== "undefined") {
+ endMoment = startMoment.clone().add(moment.duration(event.duration));
+ } else {
+ if (!isFacebookBirthday) {
+ // make copy of start date, separate storage area
+ endMoment = moment(startMoment.valueOf());
+ } else {
+ endMoment = moment(startMoment).add(1, "days");
+ }
+ }
+
+ Log.debug(`start: ${startMoment.toDate()}`);
+ Log.debug(`end:: ${endMoment.toDate()}`);
+
+ // Calculate the duration of the event for use with recurring events.
+ const durationMs = endMoment.valueOf() - startMoment.valueOf();
+ Log.debug(`duration: ${durationMs}`);
+
+ // FIXME: Since the parsed json object from node-ical comes with time information
+ // this check could be removed (?)
+ if (event.start.length === 8) {
+ startMoment = startMoment.startOf("day");
+ }
+
+ const title = CalendarFetcherUtils.getTitleFromEvent(event);
+ Log.debug(`title: ${title}`);
+
+ let excluded = false,
+ dateFilter = null;
+
+ for (let f in config.excludedEvents) {
+ let filter = config.excludedEvents[f],
+ testTitle = title.toLowerCase(),
+ until = null,
+ useRegex = false,
+ regexFlags = "g";
+
+ if (filter instanceof Object) {
+ if (typeof filter.until !== "undefined") {
+ until = filter.until;
+ }
+
+ if (typeof filter.regex !== "undefined") {
+ useRegex = filter.regex;
+ }
+
+ // If additional advanced filtering is added in, this section
+ // must remain last as we overwrite the filter object with the
+ // filterBy string
+ if (filter.caseSensitive) {
+ filter = filter.filterBy;
+ testTitle = title;
+ } else if (useRegex) {
+ filter = filter.filterBy;
+ testTitle = title;
+ regexFlags += "i";
+ } else {
+ filter = filter.filterBy.toLowerCase();
+ }
+ } else {
+ filter = filter.toLowerCase();
+ }
+
+ if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
+ if (until) {
+ dateFilter = until;
+ } else {
+ excluded = true;
+ }
+ break;
+ }
+ }
+
+ if (excluded) {
+ return;
+ }
+
+ const location = event.location || false;
+ const geo = event.geo || false;
+ const description = event.description || false;
+
+ if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) {
+ const rule = event.rrule;
+
+ const pastMoment = moment(pastLocalDate);
+ const futureMoment = moment(futureLocalDate);
+
+ // can cause problems with e.g. birthdays before 1900
+ if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
+ rule.origOptions.dtstart.setYear(1900);
+ rule.options.dtstart.setYear(1900);
+ }
+
+ // For recurring events, get the set of start dates that fall within the range
+ // of dates we're looking for.
+ // kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
+ let pastLocal;
+ let futureLocal;
+ if (CalendarFetcherUtils.isFullDayEvent(event)) {
+ Log.debug("fullday");
+ // if full day event, only use the date part of the ranges
+ pastLocal = pastMoment.toDate();
+ futureLocal = futureMoment.toDate();
+
+ Log.debug(`pastLocal: ${pastLocal}`);
+ Log.debug(`futureLocal: ${futureLocal}`);
+ } else {
+ // if we want past events
+ if (config.includePastEvents) {
+ // use the calculated past time for the between from
+ pastLocal = pastMoment.toDate();
+ } else {
+ // otherwise use NOW.. cause we shouldn't use any before now
+ pastLocal = moment(now).toDate(); //now
+ }
+ futureLocal = futureMoment.toDate(); // future
+ }
+ Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
+ const hasByWeekdayRule = rule.options.byweekday !== undefined && rule.options.byweekday !== null;
+ const oneDayInMs = 24 * 60 * 60 * 1000;
+ Log.debug(`RRule: ${rule.toString()}`);
+ rule.options.tzid = null; // RRule gets *very* confused with timezones
+ let dates = rule.between(new Date(pastLocal.valueOf() - oneDayInMs), new Date(futureLocal.valueOf() + oneDayInMs), true, () => { return true; });
+ Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
+ dates = dates.filter((d) => {
+ if (JSON.stringify(d) === "null") return false;
+ else return true;
+ });
+
+ // RRule can generate dates with an incorrect recurrence date. Process the array here and apply date correction.
+ if (hasByWeekdayRule) {
+ Log.debug("Rule has byweekday, checking for correction");
+ dates.forEach((date, index, arr) => {
+ // NOTE: getTimezoneOffset() is negative of the expected value. For America/Los_Angeles under DST (GMT-7),
+ // this value is +420. For Australia/Sydney under DST (GMT+11), this value is -660.
+ const tzOffset = date.getTimezoneOffset() / 60;
+ const hour = date.getHours();
+ if ((tzOffset < 0) && (hour < -tzOffset)) { // east of GMT
+ Log.debug(`East of GMT (tzOffset: ${tzOffset}) and hour=${hour} < ${-tzOffset}, Subtracting 1 day from ${date}`);
+ arr[index] = new Date(date.valueOf() - oneDayInMs);
+ } else if ((tzOffset > 0) && (hour >= (24 - tzOffset))) { // west of GMT
+ Log.debug(`West of GMT (tzOffset: ${tzOffset}) and hour=${hour} >= 24-${tzOffset}, Adding 1 day to ${date}`);
+ arr[index] = new Date(date.valueOf() + oneDayInMs);
+ }
+ });
+ // Adjusting the dates could push it beyond the 'until' date, so filter those out here.
+ if (rule.options.until !== null) {
+ dates = dates.filter((date) => {
+ return date.valueOf() <= rule.options.until.valueOf();
+ });
+ }
+ }
+
+ // The dates array from rrule can be confused by DST. If the event was created during DST and we
+ // are querying a date range during non-DST, rrule can have the incorrect time for the date range.
+ // Reprocess the array here computing and applying the time offset.
+ dates.forEach((date, index, arr) => {
+ let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
+ if (adjustHours !== 0) {
+ Log.debug(`Applying timezone adjustment hours=${adjustHours} to ${date}`);
+ arr[index] = new Date(date.valueOf() + (adjustHours * 60 * 60 * 1000));
+ }
+ });
+
+ // The "dates" array contains the set of dates within our desired date range range that are valid
+ // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
+ // had its date changed from outside the range to inside the range. For the time being,
+ // we'll handle this by adding *all* recurrence entries into the set of dates that we check,
+ // because the logic below will filter out any recurrences that don't actually belong within
+ // our display range.
+ // Would be great if there was a better way to handle this.
+ Log.debug(`event.recurrences: ${event.recurrences}`);
+ if (event.recurrences !== undefined) {
+ for (let dateKey in event.recurrences) {
+ // Only add dates that weren't already in the range we added from the rrule so that
+ // we don't double-add those events.
+ let d = new Date(dateKey);
+ if (!moment(d).isBetween(pastMoment, futureMoment)) {
+ dates.push(d);
+ }
+ }
+ }
+
+ // Lastly, sometimes rrule doesn't include the event.start even if it is in the requested range. Ensure
+ // inclusion here. Unfortunately dates.includes() doesn't find it so we have to do forEach().
+ {
+ let found = false;
+ dates.forEach((d) => { if (d.valueOf() === event.start.valueOf()) found = true; });
+ if (!found) {
+ Log.debug(`event.start=${event.start} was not included in results from rrule; adding`);
+ dates.splice(0, 0, event.start);
+ }
+ }
+
+ // Loop through the set of date entries to see which recurrences should be added to our event list.
+ for (let d in dates) {
+ let date = dates[d];
+ let curEvent = event;
+ let curDurationMs = durationMs;
+ let showRecurrence = true;
+
+ startMoment = moment(date);
+
+ // Remove the time information of each date by using its substring, using the following method:
+ // .toISOString().substring(0,10).
+ // since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
+ // (see https://momentjs.com/docs/#/displaying/as-iso-string/).
+ // This must be done after `date` is adjusted
+ const dateKey = date.toISOString().substring(0, 10);
+
+ // For each date that we're checking, it's possible that there is a recurrence override for that one day.
+ if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
+ // We found an override, so for this recurrence, use a potentially different title, start date, and duration.
+ curEvent = curEvent.recurrences[dateKey];
+ startMoment = moment(curEvent.start);
+ curDurationMs = curEvent.end.valueOf() - startMoment.valueOf();
+ }
+ // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
+ else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
+ // This date is an exception date, which means we should skip it in the recurrence pattern.
+ showRecurrence = false;
+ }
+ Log.debug(`duration: ${curDurationMs}`);
+
+ endMoment = moment(startMoment.valueOf() + curDurationMs);
+ if (startMoment.valueOf() === endMoment.valueOf()) {
+ endMoment = endMoment.endOf("day");
+ }
+
+ const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent);
+
+ // If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
+ // it to the event list.
+ if (endMoment.isBefore(pastLocal) || startMoment.isAfter(futureLocal)) {
+ showRecurrence = false;
+ }
+
+ if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
+ showRecurrence = false;
+ }
+
+ if (showRecurrence === true) {
+ Log.debug(`saving event: ${description}`);
+ newEvents.push({
+ title: recurrenceTitle,
+ startDate: startMoment.format("x"),
+ endDate: endMoment.format("x"),
+ fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
+ recurringEvent: true,
+ class: event.class,
+ firstYear: event.start.getFullYear(),
+ location: location,
+ geo: geo,
+ description: description
+ });
+ }
+ }
+ // End recurring event parsing.
+ } else {
+ // Single event.
+ const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
+ // Log.debug("full day event")
+
+ // if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
+ if (fullDayEvent && startMoment.valueOf() === endMoment.valueOf()) {
+ endMoment = endMoment.endOf("day");
+ }
+
+ if (config.includePastEvents) {
+ // Past event is too far in the past, so skip.
+ if (endMoment < pastLocalDate) {
+ return;
+ }
+ } else {
+ // It's not a fullday event, and it is in the past, so skip.
+ if (!fullDayEvent && endMoment < now) {
+ return;
+ }
+
+ // It's a fullday event, and it is before today, So skip.
+ if (fullDayEvent && endMoment <= todayLocal) {
+ return;
+ }
+ }
+
+ // It exceeds the maximumNumberOfDays limit, so skip.
+ if (startMoment > futureLocalDate) {
+ return;
+ }
+
+ if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
+ return;
+ }
+
+ // get correction for date saving and dst change between now and then
+ let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startMoment.toDate());
+ // This shouldn't happen
+ if (adjustHours) {
+ Log.warn(`Unexpected timezone adjustment of ${adjustHours} hours on non-recurring event`);
+ }
+ // Every thing is good. Add it to the list.
+ newEvents.push({
+ title: title,
+ startDate: startMoment.add(adjustHours, "hours").format("x"),
+ endDate: endMoment.add(adjustHours, "hours").format("x"),
+ fullDayEvent: fullDayEvent,
+ class: event.class,
+ location: location,
+ geo: geo,
+ description: description
+ });
+ }
+ }
+ });
+
+ newEvents.sort(function (a, b) {
+ return a.startDate - b.startDate;
+ });
+
+ return newEvents;
+ },
+
+ /**
+ * Lookup iana tz from windows
+ * @param {string} msTZName the timezone name to lookup
+ * @returns {string|null} the iana name or null of none is found
+ */
+ getIanaTZFromMS (msTZName) {
+ // Get hash entry
+ const he = zoneTable[msTZName];
+ // If found return iana name, else null
+ return he ? he.iana[0] : null;
+ },
+
+ /**
+ * Gets the title from the event.
+ * @param {object} event The event object to check.
+ * @returns {string} The title of the event, or "Event" if no title is found.
+ */
+ getTitleFromEvent (event) {
+ let title = "Event";
+ if (event.summary) {
+ title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
+ } else if (event.description) {
+ title = event.description;
+ }
+
+ return title;
+ },
+
+ /**
+ * Checks if an event is a fullday event.
+ * @param {object} event The event object to check.
+ * @returns {boolean} True if the event is a fullday event, false otherwise
+ */
+ isFullDayEvent (event) {
+ if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
+ return true;
+ }
+
+ const start = event.start || 0;
+ const startDate = new Date(start);
+ const end = event.end || 0;
+ if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
+ // Is 24 hours, and starts on the middle of the night.
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Determines if the user defined time filter should apply
+ * @param {Date} now Date object using previously created object for consistency
+ * @param {Moment} endDate Moment object representing the event end date
+ * @param {string} filter The time to subtract from the end date to determine if an event should be shown
+ * @returns {boolean} True if the event should be filtered out, false otherwise
+ */
+ timeFilterApplies (now, endDate, filter) {
+ if (filter) {
+ const until = filter.split(" "),
+ value = parseInt(until[0]),
+ increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
+ filterUntil = moment(endDate.format()).subtract(value, increment);
+
+ return now < filterUntil.toDate();
+ }
+
+ return false;
+ },
+
+ /**
+ * Determines if the user defined title filter should apply
+ * @param {string} title the title of the event
+ * @param {string} filter the string to look for, can be a regex also
+ * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
+ * @param {string} regexFlags flags that should be applied to the regex
+ * @returns {boolean} True if the title should be filtered out, false otherwise
+ */
+ titleFilterApplies (title, filter, useRegex, regexFlags) {
+ if (useRegex) {
+ let regexFilter = filter;
+ // Assume if leading slash, there is also trailing slash
+ if (filter[0] === "/") {
+ // Strip leading and trailing slashes
+ regexFilter = filter.substr(1).slice(0, -1);
+ }
+ return new RegExp(regexFilter, regexFlags).test(title);
+ } else {
+ return title.includes(filter);
+ }
+ }
+};
+
+if (typeof module !== "undefined") {
+ module.exports = CalendarFetcherUtils;
+}
diff --git a/modules/default/calendar/calendarutils.js b/modules/default/calendar/calendarutils.js
index 20c8ff0a25..5cbc8d6824 100644
--- a/modules/default/calendar/calendarutils.js
+++ b/modules/default/calendar/calendarutils.js
@@ -1,610 +1,125 @@
-/* MagicMirror²
- * Calendar Util Methods
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
-
-/**
- * @external Moment
- */
-const moment = require("moment");
-const path = require("path");
-const zoneTable = require(path.join(__dirname, "windowsZones.json"));
-const Log = require("../../../js/logger.js");
-
const CalendarUtils = {
+
/**
- * Calculate the time correction, either dst/std or full day in cases where
- * utc time is day before plus offset
- *
- * @param {object} event the event which needs adjustement
- * @param {Date} date the date on which this event happens
- * @returns {number} the necessary adjustment in hours
+ * Capitalize the first letter of a string
+ * @param {string} string The string to capitalize
+ * @returns {string} The capitalized string
*/
- calculateTimezoneAdjustment: function (event, date) {
- let adjustHours = 0;
- // if a timezone was specified
- if (!event.start.tz) {
- Log.debug(" if no tz, guess based on now");
- event.start.tz = moment.tz.guess();
- }
- Log.debug("initial tz=" + event.start.tz);
+ capFirst (string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ },
- // if there is a start date specified
- if (event.start.tz) {
- // if this is a windows timezone
- if (event.start.tz.includes(" ")) {
- // use the lookup table to get theIANA name as moment and date don't know MS timezones
- let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
- Log.debug("corrected TZ=" + tz);
- // watch out for unregistered windows timezone names
- // if we had a successful lookup
- if (tz) {
- // change the timezone to the IANA name
- event.start.tz = tz;
- // Log.debug("corrected timezone="+event.start.tz)
- }
+ /**
+ * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
+ * corresponding time-format to be used in the calendar display. If no number is given (or otherwise invalid input)
+ * it will a localeSpecification object with the system locale time format.
+ * @param {number} timeFormat Specifies either 12 or 24-hour time format
+ * @returns {moment.LocaleSpecification} formatted time
+ */
+ getLocaleSpecification (timeFormat) {
+ switch (timeFormat) {
+ case 12: {
+ return { longDateFormat: { LT: "h:mm A" } };
}
- Log.debug("corrected tz=" + event.start.tz);
- let current_offset = 0; // offset from TZ string or calculated
- let mm = 0; // date with tz or offset
- let start_offset = 0; // utc offset of created with tz
- // if there is still an offset, lookup failed, use it
- if (event.start.tz.startsWith("(")) {
- const regex = /[+|-]\d*:\d*/;
- const start_offsetString = event.start.tz.match(regex).toString().split(":");
- let start_offset = parseInt(start_offsetString[0]);
- start_offset *= event.start.tz[1] === "-" ? -1 : 1;
- adjustHours = start_offset;
- Log.debug("defined offset=" + start_offset + " hours");
- current_offset = start_offset;
- event.start.tz = "";
- Log.debug("ical offset=" + current_offset + " date=" + date);
- mm = moment(date);
- let x = parseInt(moment(new Date()).utcOffset());
- Log.debug("net mins=" + (current_offset * 60 - x));
-
- mm = mm.add(x - current_offset * 60, "minutes");
- adjustHours = (current_offset * 60 - x) / 60;
- event.start = mm.toDate();
- Log.debug("adjusted date=" + event.start);
- } else {
- // get the start time in that timezone
- let es = moment(event.start);
- // check for start date prior to start of daylight changing date
- if (es.format("YYYY") < 2007) {
- es.set("year", 2013); // if so, use a closer date
- }
- Log.debug("start date/time=" + es.toDate());
- start_offset = moment.tz(es, event.start.tz).utcOffset();
- Log.debug("start offset=" + start_offset);
-
- Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
-
- // get the specified date in that timezone
- mm = moment.tz(moment(date), event.start.tz);
- Log.debug("event date=" + mm.toDate());
- current_offset = mm.utcOffset();
+ case 24: {
+ return { longDateFormat: { LT: "HH:mm" } };
}
- Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
-
- // if the offset is greater than 0, east of london
- if (current_offset !== start_offset) {
- // big offset
- Log.debug("offset");
- let h = parseInt(mm.format("H"));
- // check if the event time is less than the offset
- if (h > 0 && h < Math.abs(current_offset) / 60) {
- // if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
- // we need to fix that
- //adjustHours = 24;
- // Log.debug("adjusting date")
- }
- //-300 > -240
- //if (Math.abs(current_offset) > Math.abs(start_offset)){
- if (current_offset > start_offset) {
- adjustHours -= 1;
- Log.debug("adjust down 1 hour dst change");
- //} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
- } else if (current_offset < start_offset) {
- adjustHours += 1;
- Log.debug("adjust up 1 hour dst change");
- }
+ default: {
+ return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
}
}
- Log.debug("adjustHours=" + adjustHours);
- return adjustHours;
},
/**
- * Filter the events from ical according to the given config
- *
- * @param {object} data the calendar data from ical
- * @param {object} config The configuration object
- * @returns {string[]} the filtered events
+ * Shortens a string if it's longer than maxLength and add an ellipsis to the end
+ * @param {string} string Text string to shorten
+ * @param {number} maxLength The max length of the string
+ * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
+ * @param {number} maxTitleLines The max number of vertical lines before cutting event title
+ * @returns {string} The shortened string
*/
- filterEvents: function (data, config) {
- const newEvents = [];
-
- // limitFunction doesn't do much limiting, see comment re: the dates
- // array in rrule section below as to why we need to do the filtering
- // ourselves
- const limitFunction = function (date, i) {
- return true;
- };
-
- const eventDate = function (event, time) {
- return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
- };
-
- Log.debug("There are " + Object.entries(data).length + " calendar entries.");
- Object.entries(data).forEach(([key, event]) => {
- Log.debug("Processing entry...");
- const now = new Date();
- const today = moment().startOf("day").toDate();
- const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
- let past = today;
-
- if (config.includePastEvents) {
- past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
- }
-
- // FIXME: Ugly fix to solve the facebook birthday issue.
- // Otherwise, the recurring events only show the birthday for next year.
- let isFacebookBirthday = false;
- if (typeof event.uid !== "undefined") {
- if (event.uid.indexOf("@facebook.com") !== -1) {
- isFacebookBirthday = true;
- }
- }
-
- if (event.type === "VEVENT") {
- Log.debug("Event:\n" + JSON.stringify(event));
- let startDate = eventDate(event, "start");
- let endDate;
+ shorten (string, maxLength, wrapEvents, maxTitleLines) {
+ if (typeof string !== "string") {
+ return "";
+ }
- if (typeof event.end !== "undefined") {
- endDate = eventDate(event, "end");
- } else if (typeof event.duration !== "undefined") {
- endDate = startDate.clone().add(moment.duration(event.duration));
+ if (wrapEvents === true) {
+ const words = string.split(" ");
+ let temp = "";
+ let currentLine = "";
+ let line = 0;
+
+ for (let i = 0; i < words.length; i++) {
+ const word = words[i];
+ if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
+ // max - 1 to account for a space
+ currentLine += `${word} `;
} else {
- if (!isFacebookBirthday) {
- // make copy of start date, separate storage area
- endDate = moment(startDate.format("x"), "x");
- } else {
- endDate = moment(startDate).add(1, "days");
- }
- }
-
- Log.debug("start: " + startDate.toDate());
- Log.debug("end:: " + endDate.toDate());
-
- // Calculate the duration of the event for use with recurring events.
- let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
- Log.debug("duration: " + duration);
-
- // FIXME: Since the parsed json object from node-ical comes with time information
- // this check could be removed (?)
- if (event.start.length === 8) {
- startDate = startDate.startOf("day");
- }
-
- const title = CalendarUtils.getTitleFromEvent(event);
- Log.debug("title: " + title);
-
- let excluded = false,
- dateFilter = null;
-
- for (let f in config.excludedEvents) {
- let filter = config.excludedEvents[f],
- testTitle = title.toLowerCase(),
- until = null,
- useRegex = false,
- regexFlags = "g";
-
- if (filter instanceof Object) {
- if (typeof filter.until !== "undefined") {
- until = filter.until;
- }
-
- if (typeof filter.regex !== "undefined") {
- useRegex = filter.regex;
- }
-
- // If additional advanced filtering is added in, this section
- // must remain last as we overwrite the filter object with the
- // filterBy string
- if (filter.caseSensitive) {
- filter = filter.filterBy;
- testTitle = title;
- } else if (useRegex) {
- filter = filter.filterBy;
- testTitle = title;
- regexFlags += "i";
- } else {
- filter = filter.filterBy.toLowerCase();
- }
- } else {
- filter = filter.toLowerCase();
- }
-
- if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
- if (until) {
- dateFilter = until;
- } else {
- excluded = true;
+ line++;
+ if (line > maxTitleLines - 1) {
+ if (i < words.length) {
+ currentLine += "…";
}
break;
}
- }
-
- if (excluded) {
- return;
- }
-
- const location = event.location || false;
- const geo = event.geo || false;
- const description = event.description || false;
-
- if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
- const rule = event.rrule;
- let addedEvents = 0;
-
- const pastMoment = moment(past);
- const futureMoment = moment(future);
-
- // can cause problems with e.g. birthdays before 1900
- if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
- rule.origOptions.dtstart.setYear(1900);
- rule.options.dtstart.setYear(1900);
- }
-
- // For recurring events, get the set of start dates that fall within the range
- // of dates we're looking for.
- // kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
- let pastLocal = 0;
- let futureLocal = 0;
- if (CalendarUtils.isFullDayEvent(event)) {
- Log.debug("fullday");
- // if full day event, only use the date part of the ranges
- pastLocal = pastMoment.toDate();
- futureLocal = futureMoment.toDate();
-
- Log.debug("pastLocal: " + pastLocal);
- Log.debug("futureLocal: " + futureLocal);
- } else {
- // if we want past events
- if (config.includePastEvents) {
- // use the calculated past time for the between from
- pastLocal = pastMoment.toDate();
- } else {
- // otherwise use NOW.. cause we shouldn't use any before now
- pastLocal = moment().toDate(); //now
- }
- futureLocal = futureMoment.toDate(); // future
- }
- Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal);
- const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
- Log.debug("Title: " + event.summary + ", with dates: " + JSON.stringify(dates));
- // The "dates" array contains the set of dates within our desired date range range that are valid
- // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
- // had its date changed from outside the range to inside the range. For the time being,
- // we'll handle this by adding *all* recurrence entries into the set of dates that we check,
- // because the logic below will filter out any recurrences that don't actually belong within
- // our display range.
- // Would be great if there was a better way to handle this.
- Log.debug("event.recurrences: " + event.recurrences);
- if (event.recurrences !== undefined) {
- for (let r in event.recurrences) {
- // Only add dates that weren't already in the range we added from the rrule so that
- // we don"t double-add those events.
- if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
- dates.push(new Date(r));
- }
- }
- }
- // Loop through the set of date entries to see which recurrences should be added to our event list.
- for (let d in dates) {
- let date = dates[d];
- // Remove the time information of each date by using its substring, using the following method:
- // .toISOString().substring(0,10).
- // since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
- // (see https://momentjs.com/docs/#/displaying/as-iso-string/).
- const dateKey = date.toISOString().substring(0, 10);
- let curEvent = event;
- let showRecurrence = true;
-
- // Get the offset of today where we are processing
- // This will be the correction, we need to apply.
- let nowOffset = new Date().getTimezoneOffset();
- // For full day events, the time might be off from RRULE/Luxon problem
- // Get time zone offset of the rule calculated event
- let dateoffset = date.getTimezoneOffset();
-
- // Reduce the time by the following offset.
- Log.debug(" recurring date is " + date + " offset is " + dateoffset);
-
- let dh = moment(date).format("HH");
- Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh);
-
- if (CalendarUtils.isFullDayEvent(event)) {
- Log.debug("Fullday");
- // If the offset is negative (east of GMT), where the problem is
- if (dateoffset < 0) {
- if (dh < Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- // reduce the time by the offset
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // Apply the correction to the date/time to get it UTC relative
- date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug("new recurring date1 fulldate is " + date);
- }
- } else {
- // if the timezones are the same, correct date if needed
- //if (event.start.tz === moment.tz.guess()) {
- // if the date hour is less than the offset
- if (24 - dh <= Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // apply the correction to the date/time back to right day
- date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug("new recurring date2 fulldate is " + date);
- }
- //}
- }
- } else {
- // not full day, but luxon can still screw up the date on the rule processing
- // we need to correct the date to get back to the right event for
- if (dateoffset < 0) {
- // if the date hour is less than the offset
- if (dh <= Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // Reduce the time by t:
- // Apply the correction to the date/time to get it UTC relative
- date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug("new recurring date1 is " + date);
- }
- } else {
- // if the timezones are the same, correct date if needed
- //if (event.start.tz === moment.tz.guess()) {
- // if the date hour is less than the offset
- if (24 - dh <= Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // apply the correction to the date/time back to right day
- date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug("new recurring date2 is " + date);
- }
- //}
- }
- }
- startDate = moment(date);
- Log.debug("Corrected startDate: " + startDate.toDate());
-
- let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
-
- // For each date that we're checking, it's possible that there is a recurrence override for that one day.
- if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
- // We found an override, so for this recurrence, use a potentially different title, start date, and duration.
- curEvent = curEvent.recurrences[dateKey];
- startDate = moment(curEvent.start);
- duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
- }
- // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
- else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
- // This date is an exception date, which means we should skip it in the recurrence pattern.
- showRecurrence = false;
- }
- Log.debug("duration: " + duration);
-
- endDate = moment(parseInt(startDate.format("x")) + duration, "x");
- if (startDate.format("x") === endDate.format("x")) {
- endDate = endDate.endOf("day");
- }
-
- const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
-
- // If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
- // it to the event list.
- if (endDate.isBefore(past) || startDate.isAfter(future)) {
- showRecurrence = false;
- }
-
- if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
- showRecurrence = false;
- }
-
- if (showRecurrence === true) {
- Log.debug("saving event: " + description);
- addedEvents++;
- newEvents.push({
- title: recurrenceTitle,
- startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
- endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
- fullDayEvent: CalendarUtils.isFullDayEvent(event),
- recurringEvent: true,
- class: event.class,
- firstYear: event.start.getFullYear(),
- location: location,
- geo: geo,
- description: description
- });
- }
- }
- // End recurring event parsing.
- } else {
- // Single event.
- const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
- // Log.debug("full day event")
- if (config.includePastEvents) {
- // Past event is too far in the past, so skip.
- if (endDate < past) {
- return;
- }
+ if (currentLine.length > 0) {
+ temp += `${currentLine}
${word} `;
} else {
- // It's not a fullday event, and it is in the past, so skip.
- if (!fullDayEvent && endDate < new Date()) {
- return;
- }
-
- // It's a fullday event, and it is before today, So skip.
- if (fullDayEvent && endDate <= today) {
- return;
- }
- }
-
- // It exceeds the maximumNumberOfDays limit, so skip.
- if (startDate > future) {
- return;
+ temp += `${word}
`;
}
-
- if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
- return;
- }
-
- // if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
- if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
- endDate = endDate.endOf("day");
- }
- // get correction for date saving and dst change between now and then
- let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
- // Every thing is good. Add it to the list.
- newEvents.push({
- title: title,
- startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
- endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
- fullDayEvent: fullDayEvent,
- class: event.class,
- location: location,
- geo: geo,
- description: description
- });
+ currentLine = "";
}
}
- });
-
- newEvents.sort(function (a, b) {
- return a.startDate - b.startDate;
- });
-
- return newEvents;
- },
- /**
- * Lookup iana tz from windows
- *
- * @param {string} msTZName the timezone name to lookup
- * @returns {string|null} the iana name or null of none is found
- */
- getIanaTZFromMS: function (msTZName) {
- // Get hash entry
- const he = zoneTable[msTZName];
- // If found return iana name, else null
- return he ? he.iana[0] : null;
- },
-
- /**
- * Gets the title from the event.
- *
- * @param {object} event The event object to check.
- * @returns {string} The title of the event, or "Event" if no title is found.
- */
- getTitleFromEvent: function (event) {
- let title = "Event";
- if (event.summary) {
- title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
- } else if (event.description) {
- title = event.description;
- }
-
- return title;
- },
-
- /**
- * Checks if an event is a fullday event.
- *
- * @param {object} event The event object to check.
- * @returns {boolean} True if the event is a fullday event, false otherwise
- */
- isFullDayEvent: function (event) {
- if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
- return true;
- }
-
- const start = event.start || 0;
- const startDate = new Date(start);
- const end = event.end || 0;
- if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
- // Is 24 hours, and starts on the middle of the night.
- return true;
+ return (temp + currentLine).trim();
+ } else {
+ if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
+ return `${string.trim().slice(0, maxLength)}…`;
+ } else {
+ return string.trim();
+ }
}
-
- return false;
},
/**
- * Determines if the user defined time filter should apply
- *
- * @param {Date} now Date object using previously created object for consistency
- * @param {Moment} endDate Moment object representing the event end date
- * @param {string} filter The time to subtract from the end date to determine if an event should be shown
- * @returns {boolean} True if the event should be filtered out, false otherwise
+ * Transforms the title of an event for usage.
+ * Replaces parts of the text as defined in config.titleReplace.
+ * @param {string} title The title to transform.
+ * @param {object} titleReplace object definition of parts to be replaced in the title
+ * object definition:
+ * search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calcluation, the element matching the year must be in a RegEx group
+ * replace: {string,required} Replacement string, may contain match group references (latter is required for year calculation)
+ * yearmatchgroup: {number,optional} match group for year element
+ * @returns {string} The transformed title.
*/
- timeFilterApplies: function (now, endDate, filter) {
- if (filter) {
- const until = filter.split(" "),
- value = parseInt(until[0]),
- increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
- filterUntil = moment(endDate.format()).subtract(value, increment);
-
- return now < filterUntil.format("x");
- }
-
- return false;
- },
+ titleTransform (title, titleReplace) {
+ let transformedTitle = title;
+ for (let tr in titleReplace) {
+ let transform = titleReplace[tr];
+ if (typeof transform === "object") {
+ if (typeof transform.search !== "undefined" && transform.search !== "" && typeof transform.replace !== "undefined") {
+ let regParts = transform.search.match(/^\/(.+)\/([gim]*)$/);
+ let needle = new RegExp(transform.search, "g");
+ if (regParts) {
+ // the parsed pattern is a regexp with flags.
+ needle = new RegExp(regParts[1], regParts[2]);
+ }
- /**
- * Determines if the user defined title filter should apply
- *
- * @param {string} title the title of the event
- * @param {string} filter the string to look for, can be a regex also
- * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
- * @param {string} regexFlags flags that should be applied to the regex
- * @returns {boolean} True if the title should be filtered out, false otherwise
- */
- titleFilterApplies: function (title, filter, useRegex, regexFlags) {
- if (useRegex) {
- // Assume if leading slash, there is also trailing slash
- if (filter[0] === "/") {
- // Strip leading and trailing slashes
- filter = filter.substr(1).slice(0, -1);
+ let replacement = transform.replace;
+ if (typeof transform.yearmatchgroup !== "undefined" && transform.yearmatchgroup !== "") {
+ const yearmatch = [...title.matchAll(needle)];
+ if (yearmatch[0].length >= transform.yearmatchgroup + 1 && yearmatch[0][transform.yearmatchgroup] * 1 >= 1900) {
+ let calcage = new Date().getFullYear() - yearmatch[0][transform.yearmatchgroup] * 1;
+ let searchstr = `$${transform.yearmatchgroup}`;
+ replacement = replacement.replace(searchstr, calcage);
+ }
+ }
+ transformedTitle = transformedTitle.replace(needle, replacement);
+ }
}
-
- filter = new RegExp(filter, regexFlags);
-
- return filter.test(title);
- } else {
- return title.includes(filter);
}
+ return transformedTitle;
}
};
diff --git a/modules/default/calendar/debug.js b/modules/default/calendar/debug.js
index 08cd377006..71ed2f4abd 100644
--- a/modules/default/calendar/debug.js
+++ b/modules/default/calendar/debug.js
@@ -1,14 +1,11 @@
/* CalendarFetcher Tester
* use this script with `node debug.js` to test the fetcher without the need
* of starting the MagicMirror² core. Adjust the values below to your desire.
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
*/
// Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register");
-const CalendarFetcher = require("./calendarfetcher.js");
+const CalendarFetcher = require("./calendarfetcher");
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
//const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
diff --git a/modules/default/calendar/node_helper.js b/modules/default/calendar/node_helper.js
index 2121c30925..7901abf099 100644
--- a/modules/default/calendar/node_helper.js
+++ b/modules/default/calendar/node_helper.js
@@ -1,22 +1,16 @@
-/* MagicMirror²
- * Node Helper: Calendar
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
const NodeHelper = require("node_helper");
-const CalendarFetcher = require("./calendarfetcher.js");
const Log = require("logger");
+const CalendarFetcher = require("./calendarfetcher");
module.exports = NodeHelper.create({
// Override start method.
- start: function () {
- Log.log("Starting node helper for: " + this.name);
+ start () {
+ Log.log(`Starting node helper for: ${this.name}`);
this.fetchers = [];
},
// Override socketNotificationReceived method.
- socketNotificationReceived: function (notification, payload) {
+ socketNotificationReceived (notification, payload) {
if (notification === "ADD_CALENDAR") {
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
} else if (notification === "FETCH_CALENDAR") {
@@ -33,7 +27,6 @@ module.exports = NodeHelper.create({
/**
* Creates a fetcher for a new url if it doesn't exist yet.
* Otherwise it reuses the existing one.
- *
* @param {string} url The url of the calendar
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
@@ -44,7 +37,7 @@ module.exports = NodeHelper.create({
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
* @param {string} identifier ID of the module
*/
- createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
+ createFetcher (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
try {
new URL(url);
} catch (error) {
@@ -54,9 +47,14 @@ module.exports = NodeHelper.create({
}
let fetcher;
+ let fetchIntervalCorrected;
if (typeof this.fetchers[identifier + url] === "undefined") {
- Log.log("Create new calendarfetcher for url: " + url + " - Interval: " + fetchInterval);
- fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
+ if (fetchInterval < 60000) {
+ Log.warn(`fetchInterval for url ${url} must be >= 60000`);
+ fetchIntervalCorrected = 60000;
+ }
+ Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchIntervalCorrected || fetchInterval}`);
+ fetcher = new CalendarFetcher(url, fetchIntervalCorrected || fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
fetcher.onReceive((fetcher) => {
this.broadcastEvents(fetcher, identifier);
@@ -73,7 +71,7 @@ module.exports = NodeHelper.create({
this.fetchers[identifier + url] = fetcher;
} else {
- Log.log("Use existing calendarfetcher for url: " + url);
+ Log.log(`Use existing calendarfetcher for url: ${url}`);
fetcher = this.fetchers[identifier + url];
fetcher.broadcastEvents();
}
@@ -86,7 +84,7 @@ module.exports = NodeHelper.create({
* @param {object} fetcher the fetcher associated with the calendar
* @param {string} identifier the identifier of the calendar
*/
- broadcastEvents: function (fetcher, identifier) {
+ broadcastEvents (fetcher, identifier) {
this.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(),
diff --git a/modules/default/clock/clock.js b/modules/default/clock/clock.js
index 7c9310f008..42b066ab58 100644
--- a/modules/default/clock/clock.js
+++ b/modules/default/clock/clock.js
@@ -1,11 +1,5 @@
-/* global SunCalc */
-
-/* MagicMirror²
- * Module: Clock
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
+/* global SunCalc, formatTime */
+
Module.register("clock", {
// Module config defaults.
defaults: {
@@ -22,6 +16,7 @@ Module.register("clock", {
showTime: true,
showWeek: false,
dateFormat: "dddd, LL",
+ sendNotifications: false,
/* specific to the analog clock */
analogSize: "200px",
@@ -31,21 +26,21 @@ Module.register("clock", {
secondsColor: "#888888",
showSunTimes: false,
- showMoonTimes: false,
+ showMoonTimes: false, // options: false, 'times' (rise/set), 'percent' (lit percent), 'phase' (current phase), or 'both' (percent & phase)
lat: 47.630539,
lon: -122.344147
},
// Define required scripts.
- getScripts: function () {
+ getScripts () {
return ["moment.js", "moment-timezone.js", "suncalc.js"];
},
// Define styles.
- getStyles: function () {
+ getStyles () {
return ["clock_styles.css"];
},
// Define start sequence.
- start: function () {
- Log.info("Starting module: " + this.name);
+ start () {
+ Log.info(`Starting module: ${this.name}`);
// Schedule update interval.
this.second = moment().second();
@@ -66,41 +61,44 @@ Module.register("clock", {
const notificationTimer = () => {
this.updateDom();
- // If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
- if (this.config.displaySeconds) {
- this.second = moment().second();
- if (this.second !== 0) {
- this.sendNotification("CLOCK_SECOND", this.second);
- setTimeout(notificationTimer, delayCalculator(0));
- return;
+ if (this.config.sendNotifications) {
+ // If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
+ if (this.config.displaySeconds) {
+ this.second = moment().second();
+ if (this.second !== 0) {
+ this.sendNotification("CLOCK_SECOND", this.second);
+ setTimeout(notificationTimer, delayCalculator(0));
+ return;
+ }
}
+
+ // If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
+ this.minute = moment().minute();
+ this.sendNotification("CLOCK_MINUTE", this.minute);
}
- // If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
- this.minute = moment().minute();
- this.sendNotification("CLOCK_MINUTE", this.minute);
setTimeout(notificationTimer, delayCalculator(0));
};
- // Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
+ // Set the initial timeout with the amount of seconds elapsed as
+ // reducedSeconds, so it will trigger when the minute changes
setTimeout(notificationTimer, delayCalculator(this.second));
// Set locale.
moment.locale(config.language);
},
// Override dom generator.
- getDom: function () {
+ getDom () {
const wrapper = document.createElement("div");
- wrapper.classList.add("clockGrid");
+ wrapper.classList.add("clock-grid");
/************************************
* Create wrappers for analog and digital clock
*/
const analogWrapper = document.createElement("div");
- analogWrapper.className = "clockCircle";
+ analogWrapper.className = "clock-circle";
const digitalWrapper = document.createElement("div");
digitalWrapper.className = "digital";
- digitalWrapper.style.gridArea = "center";
/************************************
* Create wrappers for DIGITAL clock
@@ -124,7 +122,7 @@ Module.register("clock", {
// Set content of wrappers.
// The moment().format("h") method has a bug on the Raspberry Pi.
// So we need to generate the timestring manually.
- // See issue: https://github.com/MichMich/MagicMirror/issues/181
+ // See issue: https://github.com/MagicMirrorOrg/MagicMirror/issues/181
let timeString;
const now = moment();
if (this.config.timezone) {
@@ -137,9 +135,9 @@ Module.register("clock", {
}
if (this.config.clockBold) {
- timeString = now.format(hourSymbol + '[
]mm[]');
+ timeString = now.format(`${hourSymbol}[
]mm[]`);
} else {
- timeString = now.format(hourSymbol + ":mm");
+ timeString = now.format(`${hourSymbol}:mm`);
}
if (this.config.showDate) {
@@ -164,21 +162,6 @@ Module.register("clock", {
digitalWrapper.appendChild(timeWrapper);
}
- /**
- * Format the time according to the config
- *
- * @param {object} config The config of the module
- * @param {object} time time to format
- * @returns {string} The formatted time string
- */
- function formatTime(config, time) {
- let formatString = hourSymbol + ":mm";
- if (config.showPeriod && config.timeFormat !== 24) {
- formatString += config.showPeriodUpper ? "A" : "a";
- }
- return moment(time).format(formatString);
- }
-
/****************************************************************
* Create wrappers for Sun Times, only if specified in config
*/
@@ -195,19 +178,11 @@ Module.register("clock", {
nextEvent = tomorrowSunTimes.sunrise;
}
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
- const untilNextEventString = untilNextEvent.hours() + "h " + untilNextEvent.minutes() + "m";
- sunWrapper.innerHTML =
- '
' +
- untilNextEventString +
- "" +
- '
' +
- formatTime(this.config, sunTimes.sunrise) +
- "" +
- '
' +
- formatTime(this.config, sunTimes.sunset) +
- "";
+ const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
+ sunWrapper.innerHTML
+ = `
${untilNextEventString}`
+ + `
${formatTime(this.config, sunTimes.sunrise)}`
+ + `
${formatTime(this.config, sunTimes.sunset)}`;
digitalWrapper.appendChild(sunWrapper);
}
@@ -226,19 +201,15 @@ Module.register("clock", {
moonSet = nextMoonTimes.set;
}
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
- const illuminatedFractionString = Math.round(moonIllumination.fraction * 100) + "%";
- moonWrapper.innerHTML =
- '
' +
- illuminatedFractionString +
- "" +
- '
' +
- (moonRise ? formatTime(this.config, moonRise) : "...") +
- "" +
- '
' +
- (moonSet ? formatTime(this.config, moonSet) : "...") +
- "";
+ const showFraction = ["both", "percent"].includes(this.config.showMoonTimes);
+ const showUnicode = ["both", "phase"].includes(this.config.showMoonTimes);
+ const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
+ const image = showUnicode ? [..."🌑🌒🌓🌔🌕🌖🌗🌘"][Math.floor(moonIllumination.phase * 8)] : "
";
+
+ moonWrapper.innerHTML
+ = `
${image} ${showFraction ? illuminatedFractionString : ""}`
+ + `
${moonRise ? formatTime(this.config, moonRise) : "..."}`
+ + `
${moonSet ? formatTime(this.config, moonSet) : "..."}`;
digitalWrapper.appendChild(moonWrapper);
}
@@ -266,26 +237,26 @@ Module.register("clock", {
analogWrapper.style.height = this.config.analogSize;
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
- analogWrapper.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
+ analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
analogWrapper.style.backgroundSize = "100%";
- // The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
+ // The following line solves issue: https://github.com/MagicMirrorOrg/MagicMirror/issues/611
// analogWrapper.style.border = "1px solid black";
analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
} else if (this.config.analogFace !== "none") {
analogWrapper.style.border = "2px solid white";
}
const clockFace = document.createElement("div");
- clockFace.className = "clockFace";
+ clockFace.className = "clock-face";
const clockHour = document.createElement("div");
- clockHour.id = "clockHour";
- clockHour.style.transform = "rotate(" + hour + "deg)";
- clockHour.className = "clockHour";
+ clockHour.id = "clock-hour";
+ clockHour.style.transform = `rotate(${hour}deg)`;
+ clockHour.className = "clock-hour";
const clockMinute = document.createElement("div");
- clockMinute.id = "clockMinute";
- clockMinute.style.transform = "rotate(" + minute + "deg)";
- clockMinute.className = "clockMinute";
+ clockMinute.id = "clock-minute";
+ clockMinute.style.transform = `rotate(${minute}deg)`;
+ clockMinute.className = "clock-minute";
// Combine analog wrappers
clockFace.appendChild(clockHour);
@@ -293,9 +264,9 @@ Module.register("clock", {
if (this.config.displaySeconds) {
const clockSecond = document.createElement("div");
- clockSecond.id = "clockSecond";
- clockSecond.style.transform = "rotate(" + second + "deg)";
- clockSecond.className = "clockSecond";
+ clockSecond.id = "clock-second";
+ clockSecond.style.transform = `rotate(${second}deg)`;
+ clockSecond.className = "clock-second";
clockSecond.style.backgroundColor = this.config.secondsColor;
clockFace.appendChild(clockSecond);
}
@@ -303,24 +274,29 @@ Module.register("clock", {
}
/*******************************************
- * Update placement, respect old analogShowDate even if its not needed anymore
+ * Update placement, respect old analogShowDate even if it's not needed anymore
*/
if (this.config.displayType === "analog") {
// Display only an analog clock
- if (this.config.analogShowDate === "top") {
- wrapper.classList.add("clockGrid--bottom");
- } else if (this.config.analogShowDate === "bottom") {
- wrapper.classList.add("clockGrid--top");
- } else {
- //analogWrapper.style.gridArea = "center";
+ if (this.config.showDate) {
+ // Add date to the analog clock
+ dateWrapper.innerHTML = now.format(this.config.dateFormat);
+ wrapper.appendChild(dateWrapper);
}
+ if (this.config.analogShowDate === "bottom") {
+ wrapper.classList.add("clock-grid-bottom");
+ } else if (this.config.analogShowDate === "top") {
+ wrapper.classList.add("clock-grid-top");
+ }
+ wrapper.appendChild(analogWrapper);
+ } else if (this.config.displayType === "digital") {
+ wrapper.appendChild(digitalWrapper);
} else if (this.config.displayType === "both") {
- wrapper.classList.add("clockGrid--" + this.config.analogPlacement);
+ wrapper.classList.add(`clock-grid-${this.config.analogPlacement}`);
+ wrapper.appendChild(analogWrapper);
+ wrapper.appendChild(digitalWrapper);
}
- wrapper.appendChild(analogWrapper);
- wrapper.appendChild(digitalWrapper);
-
// Return the wrapper to the dom.
return wrapper;
}
diff --git a/modules/default/clock/clock_styles.css b/modules/default/clock/clock_styles.css
index e3eb7e94cc..e938dd2e8e 100644
--- a/modules/default/clock/clock_styles.css
+++ b/modules/default/clock/clock_styles.css
@@ -1,37 +1,37 @@
-.clockGrid {
+.clock-grid {
display: inline-flex;
gap: 15px;
}
-.clockGrid--left {
+.clock-grid-left {
flex-direction: row;
}
-.clockGrid--right {
+.clock-grid-right {
flex-direction: row-reverse;
}
-.clockGrid--top {
+.clock-grid-top {
flex-direction: column;
}
-.clockGrid--bottom {
+.clock-grid-bottom {
flex-direction: column-reverse;
}
-.clockCircle {
+.clock-circle {
place-self: center;
position: relative;
border-radius: 50%;
background-size: 100%;
}
-.clockFace {
+.clock-face {
width: 100%;
height: 100%;
}
-.clockFace::after {
+.clock-face::after {
position: absolute;
top: 50%;
left: 50%;
@@ -44,7 +44,7 @@
display: block;
}
-.clockHour {
+.clock-hour {
width: 0;
height: 0;
position: absolute;
@@ -57,7 +57,7 @@
border-radius: 3px 0 0 3px;
}
-.clockMinute {
+.clock-minute {
width: 0;
height: 0;
position: absolute;
@@ -70,7 +70,7 @@
border-radius: 3px 0 0 3px;
}
-.clockSecond {
+.clock-second {
width: 0;
height: 0;
position: absolute;
diff --git a/modules/default/compliments/compliments.js b/modules/default/compliments/compliments.js
index 23c1a6831e..39e6c34d49 100644
--- a/modules/default/compliments/compliments.js
+++ b/modules/default/compliments/compliments.js
@@ -1,9 +1,3 @@
-/* MagicMirror²
- * Module: Compliments
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
Module.register("compliments", {
// Module config defaults.
defaults: {
@@ -28,21 +22,20 @@ Module.register("compliments", {
currentWeatherType: "",
// Define required scripts.
- getScripts: function () {
+ getScripts () {
return ["moment.js"];
},
// Define start sequence.
- start: function () {
- Log.info("Starting module: " + this.name);
+ async start () {
+ Log.info(`Starting module: ${this.name}`);
this.lastComplimentIndex = -1;
if (this.config.remoteFile !== null) {
- this.complimentFile((response) => {
- this.config.compliments = JSON.parse(response);
- this.updateDom();
- });
+ const response = await this.loadComplimentFile();
+ this.config.compliments = JSON.parse(response);
+ this.updateDom();
}
// Schedule update timer.
@@ -53,12 +46,11 @@ Module.register("compliments", {
/**
* Generate a random index for a list of compliments.
- *
* @param {string[]} compliments Array with compliments.
* @returns {number} a random index of given array
*/
- randomIndex: function (compliments) {
- if (compliments.length === 1) {
+ randomIndex (compliments) {
+ if (compliments.length <= 1) {
return 0;
}
@@ -79,35 +71,34 @@ Module.register("compliments", {
/**
* Retrieve an array of compliments for the time of the day.
- *
* @returns {string[]} array with compliments for the time of the day.
*/
- complimentArray: function () {
+ complimentArray () {
const hour = moment().hour();
const date = moment().format("YYYY-MM-DD");
- let compliments;
+ let compliments = [];
+ // Add time of day compliments
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
- compliments = this.config.compliments.morning.slice(0);
+ compliments = [...this.config.compliments.morning];
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
- compliments = this.config.compliments.afternoon.slice(0);
+ compliments = [...this.config.compliments.afternoon];
} else if (this.config.compliments.hasOwnProperty("evening")) {
- compliments = this.config.compliments.evening.slice(0);
- }
-
- if (typeof compliments === "undefined") {
- compliments = [];
+ compliments = [...this.config.compliments.evening];
}
+ // Add compliments based on weather
if (this.currentWeatherType in this.config.compliments) {
- compliments.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
+ Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
}
- compliments.push.apply(compliments, this.config.compliments.anytime);
+ // Add compliments for anytime
+ Array.prototype.push.apply(compliments, this.config.compliments.anytime);
+ // Add compliments for special days
for (let entry in this.config.compliments) {
if (new RegExp(entry).test(date)) {
- compliments.push.apply(compliments, this.config.compliments[entry]);
+ Array.prototype.push.apply(compliments, this.config.compliments[entry]);
}
}
@@ -116,29 +107,20 @@ Module.register("compliments", {
/**
* Retrieve a file from the local filesystem
- *
- * @param {Function} callback Called when the file is retrieved.
+ * @returns {Promise} Resolved when the file is loaded
*/
- complimentFile: function (callback) {
- const xobj = new XMLHttpRequest(),
- isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
- path = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
- xobj.overrideMimeType("application/json");
- xobj.open("GET", path, true);
- xobj.onreadystatechange = function () {
- if (xobj.readyState === 4 && xobj.status === 200) {
- callback(xobj.responseText);
- }
- };
- xobj.send(null);
+ async loadComplimentFile () {
+ const isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
+ url = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
+ const response = await fetch(url);
+ return await response.text();
},
/**
* Retrieve a random compliment.
- *
* @returns {string} a compliment
*/
- randomCompliment: function () {
+ getRandomCompliment () {
// get the current time of day compliments list
const compliments = this.complimentArray();
// variable for index to next message to display
@@ -157,38 +139,37 @@ Module.register("compliments", {
},
// Override dom generator.
- getDom: function () {
+ getDom () {
const wrapper = document.createElement("div");
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
// get the compliment text
- const complimentText = this.randomCompliment();
+ const complimentText = this.getRandomCompliment();
// split it into parts on newline text
const parts = complimentText.split("\n");
- // create a span to hold it all
+ // create a span to hold the compliment
const compliment = document.createElement("span");
// process all the parts of the compliment text
for (const part of parts) {
- // create a text element for each part
- compliment.appendChild(document.createTextNode(part));
- // add a break `
- compliment.appendChild(document.createElement("BR"));
+ if (part !== "") {
+ // create a text element for each part
+ compliment.appendChild(document.createTextNode(part));
+ // add a break
+ compliment.appendChild(document.createElement("BR"));
+ }
+ }
+ // only add compliment to wrapper if there is actual text in there
+ if (compliment.children.length > 0) {
+ // remove the last break
+ compliment.lastElementChild.remove();
+ wrapper.appendChild(compliment);
}
- // remove the last break
- compliment.lastElementChild.remove();
- wrapper.appendChild(compliment);
-
return wrapper;
},
- // From data currentweather set weather type
- setCurrentWeatherType: function (type) {
- this.currentWeatherType = type;
- },
-
// Override notification handler.
- notificationReceived: function (notification, payload, sender) {
+ notificationReceived (notification, payload, sender) {
if (notification === "CURRENTWEATHER_TYPE") {
- this.setCurrentWeatherType(payload.type);
+ this.currentWeatherType = payload.type;
}
}
});
diff --git a/modules/default/defaultmodules.js b/modules/default/defaultmodules.js
index c74e94a45c..e28413ff7b 100644
--- a/modules/default/defaultmodules.js
+++ b/modules/default/defaultmodules.js
@@ -1,8 +1,5 @@
-/* MagicMirror² Default Modules List
+/* Default Modules List
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
*/
const defaultModules = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather"];
diff --git a/modules/default/helloworld/helloworld.js b/modules/default/helloworld/helloworld.js
index 53357d0fd2..53fbd80c03 100644
--- a/modules/default/helloworld/helloworld.js
+++ b/modules/default/helloworld/helloworld.js
@@ -1,20 +1,14 @@
-/* MagicMirror²
- * Module: HelloWorld
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
Module.register("helloworld", {
// Default module config.
defaults: {
text: "Hello World!"
},
- getTemplate: function () {
+ getTemplate () {
return "helloworld.njk";
},
- getTemplateData: function () {
+ getTemplateData () {
return this.config;
}
});
diff --git a/modules/default/helloworld/helloworld.njk b/modules/default/helloworld/helloworld.njk
index 005ca28e3f..05e5edd37f 100644
--- a/modules/default/helloworld/helloworld.njk
+++ b/modules/default/helloworld/helloworld.njk
@@ -2,4 +2,4 @@
Use ` | safe` to allow html tages within the text string.
https://mozilla.github.io/nunjucks/templating.html#autoescaping
-->
-
{{text | safe}}
+
{{ text | safe }}
diff --git a/modules/default/newsfeed/fullarticle.njk b/modules/default/newsfeed/fullarticle.njk
index 6570396e5e..0be4b04256 100644
--- a/modules/default/newsfeed/fullarticle.njk
+++ b/modules/default/newsfeed/fullarticle.njk
@@ -1,3 +1,3 @@
-
\ No newline at end of file
+
diff --git a/modules/default/newsfeed/newsfeed.css b/modules/default/newsfeed/newsfeed.css
index ea65eca294..2c690a48e2 100644
--- a/modules/default/newsfeed/newsfeed.css
+++ b/modules/default/newsfeed/newsfeed.css
@@ -1,5 +1,6 @@
iframe.newsfeed-fullarticle {
width: 100vw;
+
/* very large height value to allow scrolling */
height: 3000px;
top: 0;
diff --git a/modules/default/newsfeed/newsfeed.js b/modules/default/newsfeed/newsfeed.js
index 6b3bba7d29..c66f8990d8 100644
--- a/modules/default/newsfeed/newsfeed.js
+++ b/modules/default/newsfeed/newsfeed.js
@@ -1,9 +1,3 @@
-/* MagicMirror²
- * Module: NewsFeed
- *
- * By Michael Teeuw https://michaelteeuw.nl
- * MIT Licensed.
- */
Module.register("newsfeed", {
// Default module config.
defaults: {
@@ -42,26 +36,26 @@ Module.register("newsfeed", {
dangerouslyDisableAutoEscaping: false
},
- getUrlPrefix: function (item) {
+ getUrlPrefix (item) {
if (item.useCorsProxy) {
- return location.protocol + "//" + location.host + "/cors?url=";
+ return `${location.protocol}//${location.host}/cors?url=`;
} else {
return "";
}
},
// Define required scripts.
- getScripts: function () {
+ getScripts () {
return ["moment.js"];
},
//Define required styles.
- getStyles: function () {
+ getStyles () {
return ["newsfeed.css"];
},
// Define required translations.
- getTranslations: function () {
+ getTranslations () {
// The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
@@ -69,8 +63,8 @@ Module.register("newsfeed", {
},
// Define start sequence.
- start: function () {
- Log.info("Starting module: " + this.name);
+ start () {
+ Log.info(`Starting module: ${this.name}`);
// Set locale.
moment.locale(config.language);
@@ -87,7 +81,7 @@ Module.register("newsfeed", {
},
// Override socket notification handler.
- socketNotificationReceived: function (notification, payload) {
+ socketNotificationReceived (notification, payload) {
if (notification === "NEWS_ITEMS") {
this.generateFeed(payload);
@@ -107,7 +101,7 @@ Module.register("newsfeed", {
},
//Override fetching of template name
- getTemplate: function () {
+ getTemplate () {
if (this.config.feedUrl) {
return "oldconfig.njk";
} else if (this.config.showFullArticle) {
@@ -117,28 +111,34 @@ Module.register("newsfeed", {
},
//Override template data and return whats used for the current template
- getTemplateData: function () {
+ getTemplateData () {
+ if (this.activeItem >= this.newsItems.length) {
+ this.activeItem = 0;
+ }
+ this.activeItemCount = this.newsItems.length;
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
if (this.config.showFullArticle) {
+ this.activeItemHash = this.newsItems[this.activeItem]?.hash;
return {
url: this.getActiveItemURL()
};
}
if (this.error) {
+ this.activeItemHash = undefined;
return {
error: this.error
};
}
if (this.newsItems.length === 0) {
+ this.activeItemHash = undefined;
return {
empty: true
};
}
- if (this.activeItem >= this.newsItems.length) {
- this.activeItem = 0;
- }
const item = this.newsItems[this.activeItem];
+ this.activeItemHash = item.hash;
+
const items = this.newsItems.map(function (item) {
item.publishDate = moment(new Date(item.pubdate)).fromNow();
return item;
@@ -150,13 +150,13 @@ Module.register("newsfeed", {
sourceTitle: item.sourceTitle,
publishDate: moment(new Date(item.pubdate)).fromNow(),
title: item.title,
- url: this.getUrlPrefix(item) + item.url,
+ url: this.getActiveItemURL(),
description: item.description,
items: items
};
},
- getActiveItemURL: function () {
+ getActiveItemURL () {
const item = this.newsItems[this.activeItem];
if (item) {
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
@@ -168,7 +168,7 @@ Module.register("newsfeed", {
/**
* Registers the feeds to be used by the backend.
*/
- registerFeeds: function () {
+ registerFeeds () {
for (let feed of this.config.feeds) {
this.sendSocketNotification("ADD_FEED", {
feed: feed,
@@ -179,10 +179,9 @@ Module.register("newsfeed", {
/**
* Generate an ordered list of items for this configured module.
- *
* @param {object} feeds An object with feeds returned by the node helper.
*/
- generateFeed: function (feeds) {
+ generateFeed (feeds) {
let newsItems = [];
for (let feed in feeds) {
const feedItems = feeds[feed];
@@ -272,11 +271,10 @@ Module.register("newsfeed", {
/**
* Check if this module is configured to show this feed.
- *
* @param {string} feedUrl Url of the feed to check.
* @returns {boolean} True if it is subscribed, false otherwise
*/
- subscribedToFeed: function (feedUrl) {
+ subscribedToFeed (feedUrl) {
for (let feed of this.config.feeds) {
if (feed.url === feedUrl) {
return true;
@@ -287,11 +285,10 @@ Module.register("newsfeed", {
/**
* Returns title for the specific feed url.
- *
* @param {string} feedUrl Url of the feed
* @returns {string} The title of the feed
*/
- titleForFeed: function (feedUrl) {
+ titleForFeed (feedUrl) {
for (let feed of this.config.feeds) {
if (feed.url === feedUrl) {
return feed.title || "";
@@ -303,7 +300,7 @@ Module.register("newsfeed", {
/**
* Schedule visual update.
*/
- scheduleUpdateInterval: function () {
+ scheduleUpdateInterval () {
this.updateDom(this.config.animationSpeed);
// Broadcast NewsFeed if needed
@@ -315,8 +312,27 @@ Module.register("newsfeed", {
if (this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
- this.activeItem++;
- this.updateDom(this.config.animationSpeed);
+
+ /*
+ * When animations are enabled, don't update the DOM unless we are actually changing what we are displaying.
+ * (Animating from a headline to itself is unsightly.)
+ * Cases:
+ *
+ * Number of items | Number of items | Display
+ * at last update | right now | Behaviour
+ * ----------------------------------------------------
+ * 0 | 0 | do not update
+ * 0 | >0 | update
+ * 1 | 0 or >1 | update
+ * 1 | 1 | update only if item details (hash value) changed
+ * >1 | any | update
+ *
+ * (N.B. We set activeItemCount and activeItemHash in getTemplateData().)
+ */
+ if (this.newsItems.length > 1 || this.newsItems.length !== this.activeItemCount || this.activeItemHash !== this.newsItems[0]?.hash) {
+ this.activeItem++; // this is OK if newsItems.Length==1; getTemplateData will wrap it around
+ this.updateDom(this.config.animationSpeed);
+ }
// Broadcast NewsFeed if needed
if (this.config.broadcastNewsFeeds) {
@@ -325,7 +341,7 @@ Module.register("newsfeed", {
}, this.config.updateInterval);
},
- resetDescrOrFullArticleAndTimer: function () {
+ resetDescrOrFullArticleAndTimer () {
this.isShowingDescription = this.config.showDescription;
this.config.showFullArticle = false;
this.scrollPosition = 0;
@@ -336,7 +352,7 @@ Module.register("newsfeed", {
}
},
- notificationReceived: function (notification, payload, sender) {
+ notificationReceived (notification, payload, sender) {
const before = this.activeItem;
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
this.hide();
@@ -346,7 +362,7 @@ Module.register("newsfeed", {
this.activeItem = 0;
}
this.resetDescrOrFullArticleAndTimer();
- Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
+ Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
this.updateDom(100);
} else if (notification === "ARTICLE_PREVIOUS") {
this.activeItem--;
@@ -354,7 +370,7 @@ Module.register("newsfeed", {
this.activeItem = this.newsItems.length - 1;
}
this.resetDescrOrFullArticleAndTimer();
- Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
+ Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
this.updateDom(100);
}
// if "more details" is received the first time: show article summary, on second time show full article
@@ -363,8 +379,8 @@ Module.register("newsfeed", {
if (this.config.showFullArticle === true) {
this.scrollPosition += this.config.scrollLength;
window.scrollTo(0, this.scrollPosition);
- Log.debug(this.name + " - scrolling down");
- Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
+ Log.debug(`${this.name} - scrolling down`);
+ Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
} else {
this.showFullArticle();
}
@@ -372,12 +388,12 @@ Module.register("newsfeed", {
if (this.config.showFullArticle === true) {
this.scrollPosition -= this.config.scrollLength;
window.scrollTo(0, this.scrollPosition);
- Log.debug(this.name + " - scrolling up");
- Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
+ Log.debug(`${this.name} - scrolling up`);
+ Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
}
} else if (notification === "ARTICLE_LESS_DETAILS") {
this.resetDescrOrFullArticleAndTimer();
- Log.debug(this.name + " - showing only article titles again");
+ Log.debug(`${this.name} - showing only article titles again`);
this.updateDom(100);
} else if (notification === "ARTICLE_TOGGLE_FULL") {
if (this.config.showFullArticle) {
@@ -397,7 +413,7 @@ Module.register("newsfeed", {
}
},
- showFullArticle: function () {
+ showFullArticle () {
this.isShowingDescription = !this.isShowingDescription;
this.config.showFullArticle = !this.isShowingDescription;
// make bottom bar align to top to allow scrolling
@@ -406,7 +422,7 @@ Module.register("newsfeed", {
}
clearInterval(this.timer);
this.timer = null;
- Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
+ Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
this.updateDom(100);
}
});
diff --git a/modules/default/newsfeed/newsfeed.njk b/modules/default/newsfeed/newsfeed.njk
index 9e7e9d78bb..5bdfe2cd49 100644
--- a/modules/default/newsfeed/newsfeed.njk
+++ b/modules/default/newsfeed/newsfeed.njk
@@ -1,27 +1,31 @@
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
- {% if dangerouslyDisableAutoEscaping %}
- {{ text | safe}}
- {% else %}
- {{ text }}
- {% endif %}
+ {% if dangerouslyDisableAutoEscaping -%}
+ {{ text | safe }}
+ {%- else -%}
+ {{ text }}
+ {%- endif %}
{% endmacro %}
-
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
- {% if dangerouslyDisableAutoEscaping %}
- {% if showTitleAsUrl %}
-