cable_ready 5.0.1 → 5.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b867df19bb8d7a78152aa746001d00e593390055cbc7e673123d6e314d950a8a
4
- data.tar.gz: c7c2bf1bbedd33b3af0de55dac46f2239a647357a1d6e671a57f6c3db5ef6e26
3
+ metadata.gz: 740e012630c79e657256a99d309e96704f2dfc7080d056edcd68c23ab18aebb2
4
+ data.tar.gz: 356b0038be58993b208f1128465d46a0ed4bf0cf858038e2a44b86275b8447f7
5
5
  SHA512:
6
- metadata.gz: f06c1061d2df7fee27d75a9a46714a27762486fe1d4d5dabc4fe6d6fc13a677e240a90f4c74bf4f3a0353a5b1f7d5335493cb90fe65428927236ed3155333c4d
7
- data.tar.gz: c5efaaedebccbcb95f570e5d4ff122de9288edc923af7dd63434e7748ee4f34216552cb20921ac8755dfb2497f5f11cd3383414986ffbfaba0a6dd1150164bb5
6
+ metadata.gz: eea5efc0262818516332c01f9e6ef98fc935c0f56e0f0a73f7ccf43c88f8774d7ccc9fd6fef4acf2f83ca77a446735a836f55a23d1391fbcc5c8811b667c5d3c
7
+ data.tar.gz: 0b199e3a2950c6103e2099a194973db49ac89f53a8698693ef12767665f20497bec903336a9f197f93b006120e35e14645ad37c18b9b2ecedb87d2eee8aae7d8
data/Gemfile.lock CHANGED
@@ -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
@@ -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 = () => {
@@ -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.2"
5
5
  end
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.2
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-09 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