cable_ready 5.0.1 → 5.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b867df19bb8d7a78152aa746001d00e593390055cbc7e673123d6e314d950a8a
4
- data.tar.gz: c7c2bf1bbedd33b3af0de55dac46f2239a647357a1d6e671a57f6c3db5ef6e26
3
+ metadata.gz: 77c9ebacb73173f6208a3695b3f9305c70a87620216fc9441d92cc29fb2199af
4
+ data.tar.gz: c41f703f59647be24df6a357f91e0cb848da7a16d31701714f1a648ec1991f6c
5
5
  SHA512:
6
- metadata.gz: f06c1061d2df7fee27d75a9a46714a27762486fe1d4d5dabc4fe6d6fc13a677e240a90f4c74bf4f3a0353a5b1f7d5335493cb90fe65428927236ed3155333c4d
7
- data.tar.gz: c5efaaedebccbcb95f570e5d4ff122de9288edc923af7dd63434e7748ee4f34216552cb20921ac8755dfb2497f5f11cd3383414986ffbfaba0a6dd1150164bb5
6
+ metadata.gz: e741e3a8d4c500ed4ca9dc1a09590f373911f5bd87986e2a9de9fb8d461da06d65fb86d8b940a4007da79d38cb352907a2c09aefd642f2a808d9d511e7d6f230
7
+ data.tar.gz: 4a6b2d908138c58afb074642bf8e27c6f685eeace3ad2b43d182e31b61440fb251e059e7aa7cb508179e8c39c050f892e066f9c7d2c7516460284fda1d66bf87
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cable_ready (5.0.1)
4
+ cable_ready (5.0.2)
5
5
  actionpack (>= 5.2)
6
6
  actionview (>= 5.2)
7
7
  activesupport (>= 5.2)
@@ -203,6 +203,7 @@ GEM
203
203
  PLATFORMS
204
204
  arm64-darwin-22
205
205
  x86_64-darwin-19
206
+ x86_64-darwin-22
206
207
  x86_64-linux
207
208
 
208
209
  DEPENDENCIES
@@ -2,7 +2,7 @@ import morphdom from "morphdom";
2
2
 
3
3
  var name = "cable_ready";
4
4
 
5
- var version = "5.0.1";
5
+ var version = "5.0.3";
6
6
 
7
7
  var description = "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.";
8
8
 
@@ -1061,6 +1061,56 @@ var Log = {
1061
1061
  morphEnd: morphEnd
1062
1062
  };
1063
1063
 
1064
+ class AppearanceObserver {
1065
+ constructor(delegate, element = null) {
1066
+ this.delegate = delegate;
1067
+ this.element = element || delegate;
1068
+ this.started = false;
1069
+ this.intersecting = false;
1070
+ this.intersectionObserver = new IntersectionObserver(this.intersect);
1071
+ }
1072
+ start() {
1073
+ if (!this.started) {
1074
+ this.started = true;
1075
+ this.intersectionObserver.observe(this.element);
1076
+ this.observeVisibility();
1077
+ }
1078
+ }
1079
+ stop() {
1080
+ if (this.started) {
1081
+ this.started = false;
1082
+ this.intersectionObserver.unobserve(this.element);
1083
+ this.unobserveVisibility();
1084
+ }
1085
+ }
1086
+ observeVisibility=() => {
1087
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
1088
+ };
1089
+ unobserveVisibility=() => {
1090
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
1091
+ };
1092
+ intersect=entries => {
1093
+ entries.forEach((entry => {
1094
+ if (entry.target === this.element) {
1095
+ if (entry.isIntersecting && document.visibilityState === "visible") {
1096
+ this.intersecting = true;
1097
+ this.delegate.appearedInViewport();
1098
+ } else {
1099
+ this.intersecting = false;
1100
+ this.delegate.disappearedFromViewport();
1101
+ }
1102
+ }
1103
+ }));
1104
+ };
1105
+ handleVisibilityChange=event => {
1106
+ if (document.visibilityState === "visible" && this.intersecting) {
1107
+ this.delegate.appearedInViewport();
1108
+ } else {
1109
+ this.delegate.disappearedFromViewport();
1110
+ }
1111
+ };
1112
+ }
1113
+
1064
1114
  const template = `\n<style>\n :host {\n display: block;\n }\n</style>\n<slot></slot>\n`;
1065
1115
 
1066
1116
  class UpdatesForElement extends SubscribingElement {
@@ -1075,6 +1125,9 @@ class UpdatesForElement extends SubscribingElement {
1075
1125
  shadowRoot.innerHTML = template;
1076
1126
  this.triggerElementLog = new BoundedQueue(10);
1077
1127
  this.targetElementLog = new BoundedQueue(10);
1128
+ this.appearanceObserver = new AppearanceObserver(this);
1129
+ this.visible = false;
1130
+ this.didTransitionToVisible = false;
1078
1131
  }
1079
1132
  async connectedCallback() {
1080
1133
  if (this.preview) return;
@@ -1085,6 +1138,14 @@ class UpdatesForElement extends SubscribingElement {
1085
1138
  } else {
1086
1139
  console.error("The `cable_ready_updates_for` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
1087
1140
  }
1141
+ if (this.observeAppearance) {
1142
+ this.appearanceObserver.start();
1143
+ }
1144
+ }
1145
+ disconnectedCallback() {
1146
+ if (this.observeAppearance) {
1147
+ this.appearanceObserver.stop();
1148
+ }
1088
1149
  }
1089
1150
  async update(data) {
1090
1151
  this.lastUpdateTimestamp = new Date;
@@ -1095,7 +1156,8 @@ class UpdatesForElement extends SubscribingElement {
1095
1156
  return;
1096
1157
  }
1097
1158
  // first <cable-ready-updates-for> element in the DOM *at any given moment* updates all of the others
1098
- if (blocks[0].element !== this) {
1159
+ // if the element becomes visible though, we have to overrule and load it
1160
+ if (blocks[0].element !== this && !this.didTransitionToVisible) {
1099
1161
  this.triggerElementLog.push(`${(new Date).toLocaleString()}: ${Log.cancel(this.lastUpdateTimestamp, "Update already requested")}`);
1100
1162
  return;
1101
1163
  }
@@ -1121,6 +1183,17 @@ class UpdatesForElement extends SubscribingElement {
1121
1183
  block.process(data, this.html, this.index, this.lastUpdateTimestamp);
1122
1184
  }));
1123
1185
  }
1186
+ appearedInViewport() {
1187
+ if (!this.visible) {
1188
+ // transition from invisible to visible forces update
1189
+ this.didTransitionToVisible = true;
1190
+ this.update({});
1191
+ }
1192
+ this.visible = true;
1193
+ }
1194
+ disappearedFromViewport() {
1195
+ this.visible = false;
1196
+ }
1124
1197
  get query() {
1125
1198
  return `${this.tagName}[identifier="${this.identifier}"]`;
1126
1199
  }
@@ -1130,6 +1203,9 @@ class UpdatesForElement extends SubscribingElement {
1130
1203
  get debounce() {
1131
1204
  return this.hasAttribute("debounce") ? parseInt(this.getAttribute("debounce")) : 20;
1132
1205
  }
1206
+ get observeAppearance() {
1207
+ return this.hasAttribute("observe-appearance");
1208
+ }
1133
1209
  }
1134
1210
 
1135
1211
  class Block {
@@ -1159,6 +1235,7 @@ class Block {
1159
1235
  onBeforeElUpdated: shouldMorph(operation),
1160
1236
  onElUpdated: _ => {
1161
1237
  this.element.removeAttribute("updating");
1238
+ this.element.didTransitionToVisible = false;
1162
1239
  dispatch(this.element, "cable-ready:after-update", operation);
1163
1240
  assignFocus(operation.focusSelector);
1164
1241
  }
@@ -1185,7 +1262,7 @@ class Block {
1185
1262
  }
1186
1263
  shouldUpdate(data) {
1187
1264
  // if everything that could prevent an update is false, update this block
1188
- return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data);
1265
+ return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data) && (!this.observeAppearance || this.visible);
1189
1266
  }
1190
1267
  hasChangesSelectedForUpdate(data) {
1191
1268
  // if there's an only attribute, only update if at least one of the attributes changed is in the allow list
@@ -1205,6 +1282,12 @@ class Block {
1205
1282
  get query() {
1206
1283
  return this.element.query;
1207
1284
  }
1285
+ get visible() {
1286
+ return this.element.visible;
1287
+ }
1288
+ get observeAppearance() {
1289
+ return this.element.observeAppearance;
1290
+ }
1208
1291
  }
1209
1292
 
1210
1293
  const registerInnerUpdates = () => {
@@ -4,7 +4,7 @@
4
4
  })(this, (function(exports, morphdom) {
5
5
  "use strict";
6
6
  var name = "cable_ready";
7
- var version = "5.0.1";
7
+ var version = "5.0.3";
8
8
  var description = "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.";
9
9
  var keywords = [ "ruby", "rails", "websockets", "actioncable", "cable", "ssr", "stimulus_reflex", "client-side", "dom" ];
10
10
  var homepage = "https://cableready.stimulusreflex.com";
@@ -982,6 +982,55 @@
982
982
  morphStart: morphStart,
983
983
  morphEnd: morphEnd
984
984
  };
985
+ class AppearanceObserver {
986
+ constructor(delegate, element = null) {
987
+ this.delegate = delegate;
988
+ this.element = element || delegate;
989
+ this.started = false;
990
+ this.intersecting = false;
991
+ this.intersectionObserver = new IntersectionObserver(this.intersect);
992
+ }
993
+ start() {
994
+ if (!this.started) {
995
+ this.started = true;
996
+ this.intersectionObserver.observe(this.element);
997
+ this.observeVisibility();
998
+ }
999
+ }
1000
+ stop() {
1001
+ if (this.started) {
1002
+ this.started = false;
1003
+ this.intersectionObserver.unobserve(this.element);
1004
+ this.unobserveVisibility();
1005
+ }
1006
+ }
1007
+ observeVisibility=() => {
1008
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
1009
+ };
1010
+ unobserveVisibility=() => {
1011
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
1012
+ };
1013
+ intersect=entries => {
1014
+ entries.forEach((entry => {
1015
+ if (entry.target === this.element) {
1016
+ if (entry.isIntersecting && document.visibilityState === "visible") {
1017
+ this.intersecting = true;
1018
+ this.delegate.appearedInViewport();
1019
+ } else {
1020
+ this.intersecting = false;
1021
+ this.delegate.disappearedFromViewport();
1022
+ }
1023
+ }
1024
+ }));
1025
+ };
1026
+ handleVisibilityChange=event => {
1027
+ if (document.visibilityState === "visible" && this.intersecting) {
1028
+ this.delegate.appearedInViewport();
1029
+ } else {
1030
+ this.delegate.disappearedFromViewport();
1031
+ }
1032
+ };
1033
+ }
985
1034
  const template = `\n<style>\n :host {\n display: block;\n }\n</style>\n<slot></slot>\n`;
986
1035
  class UpdatesForElement extends SubscribingElement {
987
1036
  static get tagName() {
@@ -995,6 +1044,9 @@
995
1044
  shadowRoot.innerHTML = template;
996
1045
  this.triggerElementLog = new BoundedQueue(10);
997
1046
  this.targetElementLog = new BoundedQueue(10);
1047
+ this.appearanceObserver = new AppearanceObserver(this);
1048
+ this.visible = false;
1049
+ this.didTransitionToVisible = false;
998
1050
  }
999
1051
  async connectedCallback() {
1000
1052
  if (this.preview) return;
@@ -1005,6 +1057,14 @@
1005
1057
  } else {
1006
1058
  console.error("The `cable_ready_updates_for` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
1007
1059
  }
1060
+ if (this.observeAppearance) {
1061
+ this.appearanceObserver.start();
1062
+ }
1063
+ }
1064
+ disconnectedCallback() {
1065
+ if (this.observeAppearance) {
1066
+ this.appearanceObserver.stop();
1067
+ }
1008
1068
  }
1009
1069
  async update(data) {
1010
1070
  this.lastUpdateTimestamp = new Date;
@@ -1015,7 +1075,8 @@
1015
1075
  return;
1016
1076
  }
1017
1077
  // first <cable-ready-updates-for> element in the DOM *at any given moment* updates all of the others
1018
- if (blocks[0].element !== this) {
1078
+ // if the element becomes visible though, we have to overrule and load it
1079
+ if (blocks[0].element !== this && !this.didTransitionToVisible) {
1019
1080
  this.triggerElementLog.push(`${(new Date).toLocaleString()}: ${Log.cancel(this.lastUpdateTimestamp, "Update already requested")}`);
1020
1081
  return;
1021
1082
  }
@@ -1041,6 +1102,17 @@
1041
1102
  block.process(data, this.html, this.index, this.lastUpdateTimestamp);
1042
1103
  }));
1043
1104
  }
1105
+ appearedInViewport() {
1106
+ if (!this.visible) {
1107
+ // transition from invisible to visible forces update
1108
+ this.didTransitionToVisible = true;
1109
+ this.update({});
1110
+ }
1111
+ this.visible = true;
1112
+ }
1113
+ disappearedFromViewport() {
1114
+ this.visible = false;
1115
+ }
1044
1116
  get query() {
1045
1117
  return `${this.tagName}[identifier="${this.identifier}"]`;
1046
1118
  }
@@ -1050,6 +1122,9 @@
1050
1122
  get debounce() {
1051
1123
  return this.hasAttribute("debounce") ? parseInt(this.getAttribute("debounce")) : 20;
1052
1124
  }
1125
+ get observeAppearance() {
1126
+ return this.hasAttribute("observe-appearance");
1127
+ }
1053
1128
  }
1054
1129
  class Block {
1055
1130
  constructor(element) {
@@ -1078,6 +1153,7 @@
1078
1153
  onBeforeElUpdated: shouldMorph(operation),
1079
1154
  onElUpdated: _ => {
1080
1155
  this.element.removeAttribute("updating");
1156
+ this.element.didTransitionToVisible = false;
1081
1157
  dispatch(this.element, "cable-ready:after-update", operation);
1082
1158
  assignFocus(operation.focusSelector);
1083
1159
  }
@@ -1104,7 +1180,7 @@
1104
1180
  }
1105
1181
  shouldUpdate(data) {
1106
1182
  // if everything that could prevent an update is false, update this block
1107
- return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data);
1183
+ return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data) && (!this.observeAppearance || this.visible);
1108
1184
  }
1109
1185
  hasChangesSelectedForUpdate(data) {
1110
1186
  // if there's an only attribute, only update if at least one of the attributes changed is in the allow list
@@ -1124,6 +1200,12 @@
1124
1200
  get query() {
1125
1201
  return this.element.query;
1126
1202
  }
1203
+ get visible() {
1204
+ return this.element.visible;
1205
+ }
1206
+ get observeAppearance() {
1207
+ return this.element.observeAppearance;
1208
+ }
1127
1209
  }
1128
1210
  const registerInnerUpdates = () => {
1129
1211
  document.addEventListener("stimulus-reflex:before", (event => {
@@ -27,12 +27,13 @@ module CableReady
27
27
  tag.cable_ready_stream_from(**build_options(*keys, html_options))
28
28
  end
29
29
 
30
- def cable_ready_updates_for(*keys, url: nil, debounce: nil, only: nil, ignore_inner_updates: false, html_options: {}, &block)
30
+ def cable_ready_updates_for(*keys, url: nil, debounce: nil, only: nil, ignore_inner_updates: false, observe_appearance: false, html_options: {}, &block)
31
31
  options = build_options(*keys, html_options)
32
32
  options[:url] = url if url
33
33
  options[:debounce] = debounce if debounce
34
34
  options[:only] = only if only
35
35
  options[:"ignore-inner-updates"] = "" if ignore_inner_updates
36
+ options[:"observe-appearance"] = "" if observe_appearance
36
37
  tag.cable_ready_updates_for(**options) { capture(&block) }
37
38
  end
38
39
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "thread/local"
4
4
 
5
+ require "active_support/core_ext/enumerable"
6
+
5
7
  module CableReady
6
8
  # This class is a thread local singleton: CableReady::Channels.instance
7
9
  # SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started
@@ -4,6 +4,8 @@ require "monitor"
4
4
  require "observer"
5
5
  require "singleton"
6
6
 
7
+ require "active_support/core_ext/numeric/time"
8
+
7
9
  module CableReady
8
10
  # This class is a process level singleton shared by all threads: CableReady::Config.instance
9
11
  class Config
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CableReady
4
- VERSION = "5.0.1"
4
+ VERSION = "5.0.3"
5
5
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cable_ready",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.",
5
5
  "keywords": [
6
6
  "ruby",
data/yarn.lock CHANGED
@@ -4892,20 +4892,25 @@ semver-compare@^1.0.0:
4892
4892
  resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
4893
4893
  integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
4894
4894
 
4895
- semver@6.3.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.0:
4895
+ semver@6.3.0:
4896
4896
  version "6.3.0"
4897
4897
  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
4898
4898
  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
4899
4899
 
4900
4900
  semver@^5.5.0, semver@^5.6.0:
4901
- version "5.7.1"
4902
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
4903
- integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
4901
+ version "5.7.2"
4902
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
4903
+ integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
4904
+
4905
+ semver@^6.0.0, semver@^6.1.2, semver@^6.3.0:
4906
+ version "6.3.1"
4907
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
4908
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
4904
4909
 
4905
4910
  semver@^7.3.4:
4906
- version "7.3.8"
4907
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
4908
- integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
4911
+ version "7.5.4"
4912
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
4913
+ integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
4909
4914
  dependencies:
4910
4915
  lru-cache "^6.0.0"
4911
4916
 
@@ -5728,9 +5733,9 @@ which@^2.0.1:
5728
5733
  isexe "^2.0.0"
5729
5734
 
5730
5735
  word-wrap@~1.2.3:
5731
- version "1.2.3"
5732
- resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
5733
- integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
5736
+ version "1.2.4"
5737
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f"
5738
+ integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==
5734
5739
 
5735
5740
  wordwrap@^1.0.0:
5736
5741
  version "1.0.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cable_ready
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 5.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Hopkins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-07 00:00:00.000000000 Z
11
+ date: 2023-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -303,7 +303,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
303
303
  - !ruby/object:Gem::Version
304
304
  version: '0'
305
305
  requirements: []
306
- rubygems_version: 3.4.1
306
+ rubygems_version: 3.4.19
307
307
  signing_key:
308
308
  specification_version: 4
309
309
  summary: Out-of-Band Server Triggered DOM Operations