@websline/cms-view-utils 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websline/cms-view-utils",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -208,17 +208,19 @@ const createAdRunner = ({
208
208
 
209
209
  /**
210
210
  * Following the ad ends it too: a visitor who acted on it has seen what it
211
- * had to say, and meeting it again on the next page reads as nagging.
211
+ * had to say, and meeting it again on the next page reads as nagging. A
212
+ * persistent ad stays until the visitor closes it.
212
213
  */
213
214
  click(ad) {
214
215
  if (!ad) return;
215
216
 
216
217
  track(ad.uuid, "click");
217
- release(ad);
218
+ if (!ad.persistent) release(ad);
218
219
  },
219
220
 
221
+ /** An ad that is not closable has no close button, so nothing to dismiss. */
220
222
  dismiss(ad) {
221
- if (!ad) return;
223
+ if (!ad || ad.closable === false) return;
222
224
 
223
225
  track(ad.uuid, "close");
224
226
  release(ad);
@@ -98,8 +98,12 @@ const filterEligibleAds = ({
98
98
  candidates.filter((ad) => {
99
99
  if (!hasConsent(ad, isConsentGiven)) return false;
100
100
  // Once per visit is the floor; a reload must not put it back on the screen.
101
- if (wasSeenThisVisit(ad, seenAt(ad.uuid))) return false;
102
- if (isSuppressed(ad, dismissedAt(ad.uuid), now)) return false;
101
+ // A persistent ad has no such floor: it stays on every page until closed.
102
+ if (!ad.persistent && wasSeenThisVisit(ad, seenAt(ad.uuid))) return false;
103
+ // `closable: false` has no close button, so no dismissal can hold it back.
104
+ if (ad.closable !== false && isSuppressed(ad, dismissedAt(ad.uuid), now)) {
105
+ return false;
106
+ }
103
107
 
104
108
  const { cookieName, cookieValue, paramName, paramValue } = ad.conditions ?? {};
105
109
 
@@ -40,15 +40,40 @@ const afterDelay = (seconds, fire, timers) => {
40
40
  };
41
41
 
42
42
  /**
43
- * Keeps listening until the page has really moved. `once` would drop the listener
44
- * on the first scroll event even when it arrived back at the top — an overscroll
45
- * bounce or a sideways scroll — and the ad would never appear for that visit.
43
+ * How far down the visitor is, in percent of what the page can scroll. A page
44
+ * that fits the window cannot be scrolled and so never reaches any share.
46
45
  */
47
- const onFirstScroll = (fire, target) => {
46
+ const scrolledPercent = (target, doc) => {
47
+ const scrollable =
48
+ (doc?.documentElement?.scrollHeight ?? 0) - (target.innerHeight ?? 0);
49
+
50
+ return scrollable > 0 ? (target.scrollY / scrollable) * 100 : 0;
51
+ };
52
+
53
+ /**
54
+ * Fixed rather than set per ad: far enough to mean interest, early enough that
55
+ * most visitors get there. Not offered in the CMS, since no editor tunes it.
56
+ */
57
+ const SCROLL_SHARE_PERCENT = 30;
58
+
59
+ /**
60
+ * Keeps listening until the share is reached: `once` would drop the listener on
61
+ * an overscroll bounce back at the top and lose the ad for that visit.
62
+ */
63
+ const onScrolledPast = (fire, target, doc) => {
64
+ const reached = () =>
65
+ target.scrollY > 0 && scrolledPercent(target, doc) >= SCROLL_SHARE_PERCENT;
66
+
67
+ // Already that far when the ad arrived — no further event may come.
68
+ if (reached()) {
69
+ fire();
70
+ return () => {};
71
+ }
72
+
48
73
  const stop = () => target.removeEventListener("scroll", handler);
49
74
 
50
75
  function handler() {
51
- if (target.scrollY <= 0) return;
76
+ if (!reached()) return;
52
77
 
53
78
  stop();
54
79
  fire();
@@ -120,14 +145,7 @@ const watchTrigger = (trigger, onFire, env = {}) => {
120
145
  }
121
146
 
122
147
  case "scroll":
123
- if (!target) return never();
124
- // Already scrolled when the ad arrived — the event will not come again.
125
- if (target.scrollY > 0) {
126
- fire();
127
- return never();
128
- }
129
-
130
- return onFirstScroll(fire, target);
148
+ return target ? onScrolledPast(fire, target, doc) : never();
131
149
 
132
150
  case "exit_intent":
133
151
  return doc ? onExitIntent(fire, doc) : never();
@@ -138,4 +156,4 @@ const watchTrigger = (trigger, onFire, env = {}) => {
138
156
  }
139
157
  };
140
158
 
141
- export { countPageView, PAGE_VIEWS_KEY, watchTrigger };
159
+ export { countPageView, PAGE_VIEWS_KEY, SCROLL_SHARE_PERCENT, watchTrigger };