phoenix_live_view 1.0.2 → 1.0.3

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.
@@ -165,6 +165,16 @@ function detectDuplicateIds() {
165
165
  }
166
166
  }
167
167
  }
168
+ function detectInvalidStreamInserts(inserts) {
169
+ const errors = /* @__PURE__ */ new Set();
170
+ Object.keys(inserts).forEach((id) => {
171
+ const streamEl = document.getElementById(id);
172
+ if (streamEl && streamEl.parentElement && streamEl.parentElement.getAttribute("phx-update") !== "stream") {
173
+ errors.add(`The stream container with id "${streamEl.parentElement.id}" is missing the phx-update="stream" attribute. Ensure it is set for streams to work properly.`);
174
+ }
175
+ });
176
+ errors.forEach((error) => console.error(error));
177
+ }
168
178
  var debug = (view, kind, msg, obj) => {
169
179
  if (view.liveSocket.isDebugEnabled()) {
170
180
  console.log(`${view.id} ${kind}: ${msg} - `, obj);
@@ -391,7 +401,7 @@ var DOM = {
391
401
  cids.forEach((cid) => {
392
402
  this.filterWithinSameLiveView(this.all(node, `[${PHX_COMPONENT}="${cid}"]`), node).forEach((parent) => {
393
403
  parentCids.add(cid);
394
- this.all(parent, `[${PHX_COMPONENT}]`).map((el) => parseInt(el.getAttribute(PHX_COMPONENT))).forEach((childCID) => childrenCids.add(childCID));
404
+ this.filterWithinSameLiveView(this.all(parent, `[${PHX_COMPONENT}]`), parent).map((el) => parseInt(el.getAttribute(PHX_COMPONENT))).forEach((childCID) => childrenCids.add(childCID));
395
405
  });
396
406
  });
397
407
  childrenCids.forEach((childCid) => parentCids.delete(childCid));
@@ -768,6 +778,9 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
768
778
  return;
769
779
  }
770
780
  ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op));
781
+ },
782
+ isLocked(el) {
783
+ return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK);
771
784
  }
772
785
  };
773
786
  var dom_default = DOM;
@@ -1110,8 +1123,22 @@ var Hooks = {
1110
1123
  mounted() {
1111
1124
  this.focusStart = this.el.firstElementChild;
1112
1125
  this.focusEnd = this.el.lastElementChild;
1113
- this.focusStart.addEventListener("focus", () => aria_default.focusLast(this.el));
1114
- this.focusEnd.addEventListener("focus", () => aria_default.focusFirst(this.el));
1126
+ this.focusStart.addEventListener("focus", (e) => {
1127
+ if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {
1128
+ const nextFocus = e.target.nextElementSibling;
1129
+ aria_default.attemptFocus(nextFocus) || aria_default.focusFirst(nextFocus);
1130
+ } else {
1131
+ aria_default.focusLast(this.el);
1132
+ }
1133
+ });
1134
+ this.focusEnd.addEventListener("focus", (e) => {
1135
+ if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {
1136
+ const nextFocus = e.target.previousElementSibling;
1137
+ aria_default.attemptFocus(nextFocus) || aria_default.focusLast(nextFocus);
1138
+ } else {
1139
+ aria_default.focusFirst(this.el);
1140
+ }
1141
+ });
1115
1142
  this.el.addEventListener("phx:show-end", () => this.el.focus());
1116
1143
  if (window.getComputedStyle(this.el).display !== "none") {
1117
1144
  aria_default.focusFirst(this.el);
@@ -1260,6 +1287,16 @@ var hooks_default = Hooks;
1260
1287
 
1261
1288
  // js/phoenix_live_view/element_ref.js
1262
1289
  var ElementRef = class {
1290
+ static onUnlock(el, callback) {
1291
+ if (!dom_default.isLocked(el) && !el.closest(`[${PHX_REF_LOCK}]`)) {
1292
+ return callback();
1293
+ }
1294
+ const closestLock = el.closest(`[${PHX_REF_LOCK}]`);
1295
+ const ref = closestLock.closest(`[${PHX_REF_LOCK}]`).getAttribute(PHX_REF_LOCK);
1296
+ closestLock.addEventListener(`phx:undo-lock:${ref}`, () => {
1297
+ callback();
1298
+ }, { once: true });
1299
+ }
1263
1300
  constructor(el) {
1264
1301
  this.el = el;
1265
1302
  this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null;
@@ -1911,37 +1948,7 @@ var morphdom_esm_default = morphdom;
1911
1948
 
1912
1949
  // js/phoenix_live_view/dom_patch.js
1913
1950
  var DOMPatch = class {
1914
- static patchWithClonedTree(container, clonedTree, liveSocket) {
1915
- let focused = liveSocket.getActiveElement();
1916
- let { selectionStart, selectionEnd } = focused && dom_default.hasSelectionRange(focused) ? focused : {};
1917
- let phxUpdate = liveSocket.binding(PHX_UPDATE);
1918
- let externalFormTriggered = null;
1919
- morphdom_esm_default(container, clonedTree, {
1920
- childrenOnly: false,
1921
- onBeforeElUpdated: (fromEl, toEl) => {
1922
- dom_default.syncPendingAttrs(fromEl, toEl);
1923
- if (!container.isSameNode(fromEl) && fromEl.hasAttribute(PHX_REF_LOCK)) {
1924
- return false;
1925
- }
1926
- if (dom_default.isIgnored(fromEl, phxUpdate)) {
1927
- return false;
1928
- }
1929
- if (focused && focused.isSameNode(fromEl) && dom_default.isFormInput(fromEl)) {
1930
- dom_default.mergeFocusedInput(fromEl, toEl);
1931
- return false;
1932
- }
1933
- if (dom_default.isNowTriggerFormExternal(toEl, liveSocket.binding(PHX_TRIGGER_ACTION))) {
1934
- externalFormTriggered = toEl;
1935
- }
1936
- }
1937
- });
1938
- if (externalFormTriggered) {
1939
- liveSocket.unload();
1940
- Object.getPrototypeOf(externalFormTriggered).submit.call(externalFormTriggered);
1941
- }
1942
- liveSocket.silenceEvents(() => dom_default.restoreFocus(focused, selectionStart, selectionEnd));
1943
- }
1944
- constructor(view, container, id, html, streams, targetCID) {
1951
+ constructor(view, container, id, html, streams, targetCID, opts = {}) {
1945
1952
  this.view = view;
1946
1953
  this.liveSocket = view.liveSocket;
1947
1954
  this.container = container;
@@ -1966,6 +1973,8 @@ var DOMPatch = class {
1966
1973
  afterphxChildAdded: [],
1967
1974
  aftertransitionsDiscarded: []
1968
1975
  };
1976
+ this.withChildren = opts.withChildren || opts.undoRef || false;
1977
+ this.undoRef = opts.undoRef;
1969
1978
  }
1970
1979
  before(kind, callback) {
1971
1980
  this.callbacks[`before${kind}`].push(callback);
@@ -2000,7 +2009,7 @@ var DOMPatch = class {
2000
2009
  let updates = [];
2001
2010
  let appendPrependUpdates = [];
2002
2011
  let externalFormTriggered = null;
2003
- function morph(targetContainer2, source, withChildren = false) {
2012
+ function morph(targetContainer2, source, withChildren = this.withChildren) {
2004
2013
  let morphCallbacks = {
2005
2014
  // normally, we are running with childrenOnly, as the patch HTML for a LV
2006
2015
  // does not include the LV attrs (data-phx-session, etc.)
@@ -2126,7 +2135,7 @@ var DOMPatch = class {
2126
2135
  }
2127
2136
  let isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);
2128
2137
  let focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl);
2129
- if (fromEl.hasAttribute(PHX_REF_SRC)) {
2138
+ if (fromEl.hasAttribute(PHX_REF_SRC) && fromEl.getAttribute(PHX_REF_LOCK) != this.undoRef) {
2130
2139
  if (dom_default.isUploadInput(fromEl)) {
2131
2140
  dom_default.mergeAttrs(fromEl, toEl, { isIgnored: true });
2132
2141
  this.trackBefore("updated", fromEl, toEl);
@@ -2210,6 +2219,7 @@ var DOMPatch = class {
2210
2219
  });
2211
2220
  if (liveSocket.isDebugEnabled()) {
2212
2221
  detectDuplicateIds();
2222
+ detectInvalidStreamInserts(this.streamInserts);
2213
2223
  Array.from(document.querySelectorAll("input[name=id]")).forEach((node) => {
2214
2224
  if (node.form) {
2215
2225
  console.error('Detected an input with name="id" inside a form! This will cause problems when patching the DOM.\n', node);
@@ -3544,7 +3554,11 @@ var View = class _View {
3544
3554
  this.formsForRecovery = this.getFormsForRecovery();
3545
3555
  }
3546
3556
  if (this.isMain() && window.history.state === null) {
3547
- this.liveSocket.replaceRootHistory();
3557
+ browser_default.pushState("replace", {
3558
+ type: "patch",
3559
+ id: this.id,
3560
+ position: this.liveSocket.currentHistoryPosition
3561
+ });
3548
3562
  }
3549
3563
  if (liveview_version !== this.liveSocket.version()) {
3550
3564
  console.error(`LiveView asset version mismatch. JavaScript version ${this.liveSocket.version()} vs. server ${liveview_version}. To avoid issues, please ensure that your assets use the same version as the server.`);
@@ -3866,6 +3880,9 @@ var View = class _View {
3866
3880
  }
3867
3881
  addHook(el) {
3868
3882
  let hookElId = ViewHook.elementID(el);
3883
+ if (el.getAttribute && !this.ownsElement(el)) {
3884
+ return;
3885
+ }
3869
3886
  if (hookElId && !this.viewHooks[hookElId]) {
3870
3887
  let hook = dom_default.getCustomElHook(el) || logError(`no hook found for custom element: ${el.id}`);
3871
3888
  this.viewHooks[hookElId] = hook;
@@ -3875,9 +3892,6 @@ var View = class _View {
3875
3892
  return;
3876
3893
  } else {
3877
3894
  let hookName = el.getAttribute(`data-phx-${PHX_HOOK}`) || el.getAttribute(this.binding(PHX_HOOK));
3878
- if (hookName && !this.ownsElement(el)) {
3879
- return;
3880
- }
3881
3895
  let callbacks = this.liveSocket.getHookCallbacks(hookName);
3882
3896
  if (callbacks) {
3883
3897
  if (!el.id) {
@@ -3892,9 +3906,10 @@ var View = class _View {
3892
3906
  }
3893
3907
  }
3894
3908
  destroyHook(hook) {
3909
+ const hookId = ViewHook.elementID(hook.el);
3895
3910
  hook.__destroyed();
3896
3911
  hook.__cleanup__();
3897
- delete this.viewHooks[ViewHook.elementID(hook.el)];
3912
+ delete this.viewHooks[hookId];
3898
3913
  }
3899
3914
  applyPendingUpdates() {
3900
3915
  this.pendingDiffs.forEach(({ diff, events }) => this.update(diff, events));
@@ -4144,12 +4159,11 @@ var View = class _View {
4144
4159
  undoElRef(el, ref, phxEvent) {
4145
4160
  let elRef = new ElementRef(el);
4146
4161
  elRef.maybeUndo(ref, phxEvent, (clonedTree) => {
4147
- let hook = this.triggerBeforeUpdateHook(el, clonedTree);
4148
- DOMPatch.patchWithClonedTree(el, clonedTree, this.liveSocket);
4162
+ let patch = new DOMPatch(this, el, this.id, clonedTree, [], null, { undoRef: ref });
4163
+ const phxChildrenAdded = this.performPatch(patch, true);
4149
4164
  dom_default.all(el, `[${PHX_REF_SRC}="${this.refSrc()}"]`, (child) => this.undoElRef(child, ref, phxEvent));
4150
- this.execNewMounted(el);
4151
- if (hook) {
4152
- hook.__updated();
4165
+ if (phxChildrenAdded) {
4166
+ this.joinNewChildren();
4153
4167
  }
4154
4168
  });
4155
4169
  }
@@ -4365,15 +4379,17 @@ var View = class _View {
4365
4379
  };
4366
4380
  this.pushWithReply(refGenerator, "event", event).then(({ resp }) => {
4367
4381
  if (dom_default.isUploadInput(inputEl) && dom_default.isAutoUpload(inputEl)) {
4368
- if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) {
4369
- let [ref, _els] = refGenerator();
4370
- this.undoRefs(ref, phxEvent, [inputEl.form]);
4371
- this.uploadFiles(inputEl.form, phxEvent, targetCtx, ref, cid, (_uploads) => {
4372
- callback && callback(resp);
4373
- this.triggerAwaitingSubmit(inputEl.form, phxEvent);
4374
- this.undoRefs(ref, phxEvent);
4375
- });
4376
- }
4382
+ ElementRef.onUnlock(inputEl, () => {
4383
+ if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) {
4384
+ let [ref, _els] = refGenerator();
4385
+ this.undoRefs(ref, phxEvent, [inputEl.form]);
4386
+ this.uploadFiles(inputEl.form, phxEvent, targetCtx, ref, cid, (_uploads) => {
4387
+ callback && callback(resp);
4388
+ this.triggerAwaitingSubmit(inputEl.form, phxEvent);
4389
+ this.undoRefs(ref, phxEvent);
4390
+ });
4391
+ }
4392
+ });
4377
4393
  } else {
4378
4394
  callback && callback(resp);
4379
4395
  }
@@ -4724,7 +4740,7 @@ var LiveSocket = class {
4724
4740
  }
4725
4741
  // public
4726
4742
  version() {
4727
- return "1.0.2";
4743
+ return "1.0.3";
4728
4744
  }
4729
4745
  isProfileEnabled() {
4730
4746
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
@@ -4921,7 +4937,9 @@ var LiveSocket = class {
4921
4937
  dom_default.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, (rootEl) => {
4922
4938
  if (!this.getRootById(rootEl.id)) {
4923
4939
  let view = this.newRootView(rootEl);
4924
- view.setHref(this.getHref());
4940
+ if (!dom_default.isPhxSticky(rootEl)) {
4941
+ view.setHref(this.getHref());
4942
+ }
4925
4943
  view.join();
4926
4944
  if (rootEl.hasAttribute(PHX_MAIN)) {
4927
4945
  this.main = view;
@@ -5235,7 +5253,7 @@ var LiveSocket = class {
5235
5253
  if (!this.registerNewLocation(window.location)) {
5236
5254
  return;
5237
5255
  }
5238
- let { type, backType, id, root, scroll, position } = event.state || {};
5256
+ let { type, backType, id, scroll, position } = event.state || {};
5239
5257
  let href = window.location.href;
5240
5258
  let isForward = position > this.currentHistoryPosition;
5241
5259
  type = isForward ? type : backType || type;
@@ -5243,17 +5261,13 @@ var LiveSocket = class {
5243
5261
  this.sessionStorage.setItem(PHX_LV_HISTORY_POSITION, this.currentHistoryPosition.toString());
5244
5262
  dom_default.dispatchEvent(window, "phx:navigate", { detail: { href, patch: type === "patch", pop: true, direction: isForward ? "forward" : "backward" } });
5245
5263
  this.requestDOMUpdate(() => {
5264
+ const callback = () => {
5265
+ this.maybeScroll(scroll);
5266
+ };
5246
5267
  if (this.main.isConnected() && (type === "patch" && id === this.main.id)) {
5247
- this.main.pushLinkPatch(event, href, null, () => {
5248
- this.maybeScroll(scroll);
5249
- });
5268
+ this.main.pushLinkPatch(event, href, null, callback);
5250
5269
  } else {
5251
- this.replaceMain(href, null, () => {
5252
- if (root) {
5253
- this.replaceRootHistory();
5254
- }
5255
- this.maybeScroll(scroll);
5256
- });
5270
+ this.replaceMain(href, null, callback);
5257
5271
  }
5258
5272
  });
5259
5273
  }, false);
@@ -5360,15 +5374,6 @@ var LiveSocket = class {
5360
5374
  });
5361
5375
  });
5362
5376
  }
5363
- replaceRootHistory() {
5364
- browser_default.pushState("replace", {
5365
- root: true,
5366
- type: "patch",
5367
- id: this.main.id,
5368
- position: this.currentHistoryPosition
5369
- // Preserve current position
5370
- });
5371
- }
5372
5377
  registerNewLocation(newLocation) {
5373
5378
  let { pathname, search } = this.currentLocation;
5374
5379
  if (pathname + search === newLocation.pathname + newLocation.search) {