phoenix_live_view 1.0.2 → 1.0.4

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,11 +1123,27 @@ 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));
1115
- this.el.addEventListener("phx:show-end", () => this.el.focus());
1116
- if (window.getComputedStyle(this.el).display !== "none") {
1117
- 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
+ });
1142
+ if (!this.el.contains(document.activeElement)) {
1143
+ this.el.addEventListener("phx:show-end", () => this.el.focus());
1144
+ if (window.getComputedStyle(this.el).display !== "none") {
1145
+ aria_default.focusFirst(this.el);
1146
+ }
1118
1147
  }
1119
1148
  }
1120
1149
  }
@@ -1260,6 +1289,16 @@ var hooks_default = Hooks;
1260
1289
 
1261
1290
  // js/phoenix_live_view/element_ref.js
1262
1291
  var ElementRef = class {
1292
+ static onUnlock(el, callback) {
1293
+ if (!dom_default.isLocked(el) && !el.closest(`[${PHX_REF_LOCK}]`)) {
1294
+ return callback();
1295
+ }
1296
+ const closestLock = el.closest(`[${PHX_REF_LOCK}]`);
1297
+ const ref = closestLock.closest(`[${PHX_REF_LOCK}]`).getAttribute(PHX_REF_LOCK);
1298
+ closestLock.addEventListener(`phx:undo-lock:${ref}`, () => {
1299
+ callback();
1300
+ }, { once: true });
1301
+ }
1263
1302
  constructor(el) {
1264
1303
  this.el = el;
1265
1304
  this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null;
@@ -1402,7 +1441,7 @@ var DOMPostMorphRestorer = class {
1402
1441
  }
1403
1442
  };
1404
1443
 
1405
- // node_modules/morphdom/dist/morphdom-esm.js
1444
+ // ../node_modules/morphdom/dist/morphdom-esm.js
1406
1445
  var DOCUMENT_FRAGMENT_NODE = 11;
1407
1446
  function morphAttrs(fromNode, toNode) {
1408
1447
  var toNodeAttrs = toNode.attributes;
@@ -1911,37 +1950,7 @@ var morphdom_esm_default = morphdom;
1911
1950
 
1912
1951
  // js/phoenix_live_view/dom_patch.js
1913
1952
  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) {
1953
+ constructor(view, container, id, html, streams, targetCID, opts = {}) {
1945
1954
  this.view = view;
1946
1955
  this.liveSocket = view.liveSocket;
1947
1956
  this.container = container;
@@ -1966,6 +1975,8 @@ var DOMPatch = class {
1966
1975
  afterphxChildAdded: [],
1967
1976
  aftertransitionsDiscarded: []
1968
1977
  };
1978
+ this.withChildren = opts.withChildren || opts.undoRef || false;
1979
+ this.undoRef = opts.undoRef;
1969
1980
  }
1970
1981
  before(kind, callback) {
1971
1982
  this.callbacks[`before${kind}`].push(callback);
@@ -2000,7 +2011,7 @@ var DOMPatch = class {
2000
2011
  let updates = [];
2001
2012
  let appendPrependUpdates = [];
2002
2013
  let externalFormTriggered = null;
2003
- function morph(targetContainer2, source, withChildren = false) {
2014
+ function morph(targetContainer2, source, withChildren = this.withChildren) {
2004
2015
  let morphCallbacks = {
2005
2016
  // normally, we are running with childrenOnly, as the patch HTML for a LV
2006
2017
  // does not include the LV attrs (data-phx-session, etc.)
@@ -2126,7 +2137,7 @@ var DOMPatch = class {
2126
2137
  }
2127
2138
  let isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);
2128
2139
  let focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl);
2129
- if (fromEl.hasAttribute(PHX_REF_SRC)) {
2140
+ if (fromEl.hasAttribute(PHX_REF_SRC) && fromEl.getAttribute(PHX_REF_LOCK) != this.undoRef) {
2130
2141
  if (dom_default.isUploadInput(fromEl)) {
2131
2142
  dom_default.mergeAttrs(fromEl, toEl, { isIgnored: true });
2132
2143
  this.trackBefore("updated", fromEl, toEl);
@@ -2210,6 +2221,7 @@ var DOMPatch = class {
2210
2221
  });
2211
2222
  if (liveSocket.isDebugEnabled()) {
2212
2223
  detectDuplicateIds();
2224
+ detectInvalidStreamInserts(this.streamInserts);
2213
2225
  Array.from(document.querySelectorAll("input[name=id]")).forEach((node) => {
2214
2226
  if (node.form) {
2215
2227
  console.error('Detected an input with name="id" inside a form! This will cause problems when patching the DOM.\n', node);
@@ -2306,7 +2318,7 @@ var DOMPatch = class {
2306
2318
  transitionPendingRemoves() {
2307
2319
  let { pendingRemoves, liveSocket } = this;
2308
2320
  if (pendingRemoves.length > 0) {
2309
- liveSocket.transitionRemoves(pendingRemoves, false, () => {
2321
+ liveSocket.transitionRemoves(pendingRemoves, () => {
2310
2322
  pendingRemoves.forEach((el) => {
2311
2323
  let child = dom_default.firstPhxChild(el);
2312
2324
  if (child) {
@@ -2766,10 +2778,10 @@ var JS = {
2766
2778
  view.liveSocket.pushHistoryPatch(e, href, replace ? "replace" : "push", sourceEl);
2767
2779
  },
2768
2780
  exec_focus(e, eventType, phxEvent, view, sourceEl, el) {
2769
- window.requestAnimationFrame(() => aria_default.attemptFocus(el));
2781
+ aria_default.attemptFocus(el);
2770
2782
  },
2771
2783
  exec_focus_first(e, eventType, phxEvent, view, sourceEl, el) {
2772
- window.requestAnimationFrame(() => aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el));
2784
+ aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el);
2773
2785
  },
2774
2786
  exec_push_focus(e, eventType, phxEvent, view, sourceEl, el) {
2775
2787
  window.requestAnimationFrame(() => focusStack.push(el || sourceEl));
@@ -2875,18 +2887,14 @@ var JS = {
2875
2887
  }
2876
2888
  } else {
2877
2889
  if (this.isVisible(el)) {
2878
- window.requestAnimationFrame(() => {
2879
- el.dispatchEvent(new Event("phx:hide-start"));
2880
- dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = "none");
2881
- el.dispatchEvent(new Event("phx:hide-end"));
2882
- });
2890
+ el.dispatchEvent(new Event("phx:hide-start"));
2891
+ dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = "none");
2892
+ el.dispatchEvent(new Event("phx:hide-end"));
2883
2893
  } else {
2884
- window.requestAnimationFrame(() => {
2885
- el.dispatchEvent(new Event("phx:show-start"));
2886
- let stickyDisplay = display || this.defaultDisplay(el);
2887
- dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = stickyDisplay);
2888
- el.dispatchEvent(new Event("phx:show-end"));
2889
- });
2894
+ el.dispatchEvent(new Event("phx:show-start"));
2895
+ let stickyDisplay = display || this.defaultDisplay(el);
2896
+ dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = stickyDisplay);
2897
+ el.dispatchEvent(new Event("phx:show-end"));
2890
2898
  }
2891
2899
  }
2892
2900
  },
@@ -3544,7 +3552,11 @@ var View = class _View {
3544
3552
  this.formsForRecovery = this.getFormsForRecovery();
3545
3553
  }
3546
3554
  if (this.isMain() && window.history.state === null) {
3547
- this.liveSocket.replaceRootHistory();
3555
+ browser_default.pushState("replace", {
3556
+ type: "patch",
3557
+ id: this.id,
3558
+ position: this.liveSocket.currentHistoryPosition
3559
+ });
3548
3560
  }
3549
3561
  if (liveview_version !== this.liveSocket.version()) {
3550
3562
  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 +3878,9 @@ var View = class _View {
3866
3878
  }
3867
3879
  addHook(el) {
3868
3880
  let hookElId = ViewHook.elementID(el);
3881
+ if (el.getAttribute && !this.ownsElement(el)) {
3882
+ return;
3883
+ }
3869
3884
  if (hookElId && !this.viewHooks[hookElId]) {
3870
3885
  let hook = dom_default.getCustomElHook(el) || logError(`no hook found for custom element: ${el.id}`);
3871
3886
  this.viewHooks[hookElId] = hook;
@@ -3875,9 +3890,6 @@ var View = class _View {
3875
3890
  return;
3876
3891
  } else {
3877
3892
  let hookName = el.getAttribute(`data-phx-${PHX_HOOK}`) || el.getAttribute(this.binding(PHX_HOOK));
3878
- if (hookName && !this.ownsElement(el)) {
3879
- return;
3880
- }
3881
3893
  let callbacks = this.liveSocket.getHookCallbacks(hookName);
3882
3894
  if (callbacks) {
3883
3895
  if (!el.id) {
@@ -3892,9 +3904,10 @@ var View = class _View {
3892
3904
  }
3893
3905
  }
3894
3906
  destroyHook(hook) {
3907
+ const hookId = ViewHook.elementID(hook.el);
3895
3908
  hook.__destroyed();
3896
3909
  hook.__cleanup__();
3897
- delete this.viewHooks[ViewHook.elementID(hook.el)];
3910
+ delete this.viewHooks[hookId];
3898
3911
  }
3899
3912
  applyPendingUpdates() {
3900
3913
  this.pendingDiffs.forEach(({ diff, events }) => this.update(diff, events));
@@ -4144,12 +4157,11 @@ var View = class _View {
4144
4157
  undoElRef(el, ref, phxEvent) {
4145
4158
  let elRef = new ElementRef(el);
4146
4159
  elRef.maybeUndo(ref, phxEvent, (clonedTree) => {
4147
- let hook = this.triggerBeforeUpdateHook(el, clonedTree);
4148
- DOMPatch.patchWithClonedTree(el, clonedTree, this.liveSocket);
4160
+ let patch = new DOMPatch(this, el, this.id, clonedTree, [], null, { undoRef: ref });
4161
+ const phxChildrenAdded = this.performPatch(patch, true);
4149
4162
  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();
4163
+ if (phxChildrenAdded) {
4164
+ this.joinNewChildren();
4153
4165
  }
4154
4166
  });
4155
4167
  }
@@ -4365,15 +4377,17 @@ var View = class _View {
4365
4377
  };
4366
4378
  this.pushWithReply(refGenerator, "event", event).then(({ resp }) => {
4367
4379
  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
- }
4380
+ ElementRef.onUnlock(inputEl, () => {
4381
+ if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) {
4382
+ let [ref, _els] = refGenerator();
4383
+ this.undoRefs(ref, phxEvent, [inputEl.form]);
4384
+ this.uploadFiles(inputEl.form, phxEvent, targetCtx, ref, cid, (_uploads) => {
4385
+ callback && callback(resp);
4386
+ this.triggerAwaitingSubmit(inputEl.form, phxEvent);
4387
+ this.undoRefs(ref, phxEvent);
4388
+ });
4389
+ }
4390
+ });
4377
4391
  } else {
4378
4392
  callback && callback(resp);
4379
4393
  }
@@ -4724,7 +4738,7 @@ var LiveSocket = class {
4724
4738
  }
4725
4739
  // public
4726
4740
  version() {
4727
- return "1.0.2";
4741
+ return "1.0.4";
4728
4742
  }
4729
4743
  isProfileEnabled() {
4730
4744
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
@@ -4921,7 +4935,9 @@ var LiveSocket = class {
4921
4935
  dom_default.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, (rootEl) => {
4922
4936
  if (!this.getRootById(rootEl.id)) {
4923
4937
  let view = this.newRootView(rootEl);
4924
- view.setHref(this.getHref());
4938
+ if (!dom_default.isPhxSticky(rootEl)) {
4939
+ view.setHref(this.getHref());
4940
+ }
4925
4941
  view.join();
4926
4942
  if (rootEl.hasAttribute(PHX_MAIN)) {
4927
4943
  this.main = view;
@@ -4939,20 +4955,21 @@ var LiveSocket = class {
4939
4955
  browser_default.redirect(to, flash);
4940
4956
  }
4941
4957
  replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)) {
4942
- let liveReferer = this.currentLocation.href;
4958
+ const liveReferer = this.currentLocation.href;
4943
4959
  this.outgoingMainEl = this.outgoingMainEl || this.main.el;
4944
- let removeEls = dom_default.all(this.outgoingMainEl, `[${this.binding("remove")}]`);
4945
- let newMainEl = dom_default.cloneNode(this.outgoingMainEl, "");
4960
+ const stickies = dom_default.findPhxSticky(document) || [];
4961
+ const removeEls = dom_default.all(this.outgoingMainEl, `[${this.binding("remove")}]`).filter((el) => !dom_default.isChildOfAny(el, stickies));
4962
+ const newMainEl = dom_default.cloneNode(this.outgoingMainEl, "");
4946
4963
  this.main.showLoader(this.loaderTimeout);
4947
4964
  this.main.destroy();
4948
4965
  this.main = this.newRootView(newMainEl, flash, liveReferer);
4949
4966
  this.main.setRedirect(href);
4950
- this.transitionRemoves(removeEls, true);
4967
+ this.transitionRemoves(removeEls);
4951
4968
  this.main.join((joinCount, onDone) => {
4952
4969
  if (joinCount === 1 && this.commitPendingLink(linkRef)) {
4953
4970
  this.requestDOMUpdate(() => {
4954
4971
  removeEls.forEach((el) => el.remove());
4955
- dom_default.findPhxSticky(document).forEach((el) => newMainEl.appendChild(el));
4972
+ stickies.forEach((el) => newMainEl.appendChild(el));
4956
4973
  this.outgoingMainEl.replaceWith(newMainEl);
4957
4974
  this.outgoingMainEl = null;
4958
4975
  callback && callback(linkRef);
@@ -4961,12 +4978,8 @@ var LiveSocket = class {
4961
4978
  }
4962
4979
  });
4963
4980
  }
4964
- transitionRemoves(elements, skipSticky, callback) {
4981
+ transitionRemoves(elements, callback) {
4965
4982
  let removeAttr = this.binding("remove");
4966
- if (skipSticky) {
4967
- const stickies = dom_default.findPhxSticky(document) || [];
4968
- elements = elements.filter((el) => !dom_default.isChildOfAny(el, stickies));
4969
- }
4970
4983
  let silenceEvents = (e) => {
4971
4984
  e.preventDefault();
4972
4985
  e.stopImmediatePropagation();
@@ -5235,7 +5248,7 @@ var LiveSocket = class {
5235
5248
  if (!this.registerNewLocation(window.location)) {
5236
5249
  return;
5237
5250
  }
5238
- let { type, backType, id, root, scroll, position } = event.state || {};
5251
+ let { type, backType, id, scroll, position } = event.state || {};
5239
5252
  let href = window.location.href;
5240
5253
  let isForward = position > this.currentHistoryPosition;
5241
5254
  type = isForward ? type : backType || type;
@@ -5243,17 +5256,13 @@ var LiveSocket = class {
5243
5256
  this.sessionStorage.setItem(PHX_LV_HISTORY_POSITION, this.currentHistoryPosition.toString());
5244
5257
  dom_default.dispatchEvent(window, "phx:navigate", { detail: { href, patch: type === "patch", pop: true, direction: isForward ? "forward" : "backward" } });
5245
5258
  this.requestDOMUpdate(() => {
5259
+ const callback = () => {
5260
+ this.maybeScroll(scroll);
5261
+ };
5246
5262
  if (this.main.isConnected() && (type === "patch" && id === this.main.id)) {
5247
- this.main.pushLinkPatch(event, href, null, () => {
5248
- this.maybeScroll(scroll);
5249
- });
5263
+ this.main.pushLinkPatch(event, href, null, callback);
5250
5264
  } else {
5251
- this.replaceMain(href, null, () => {
5252
- if (root) {
5253
- this.replaceRootHistory();
5254
- }
5255
- this.maybeScroll(scroll);
5256
- });
5265
+ this.replaceMain(href, null, callback);
5257
5266
  }
5258
5267
  });
5259
5268
  }, false);
@@ -5330,7 +5339,8 @@ var LiveSocket = class {
5330
5339
  this.registerNewLocation(window.location);
5331
5340
  }
5332
5341
  historyRedirect(e, href, linkState, flash, targetEl) {
5333
- if (targetEl && e.isTrusted && e.type !== "popstate") {
5342
+ const clickLoading = targetEl && e.isTrusted && e.type !== "popstate";
5343
+ if (clickLoading) {
5334
5344
  targetEl.classList.add("phx-click-loading");
5335
5345
  }
5336
5346
  if (!this.isConnected() || !this.main.isMain()) {
@@ -5356,19 +5366,13 @@ var LiveSocket = class {
5356
5366
  dom_default.dispatchEvent(window, "phx:navigate", { detail: { href, patch: false, pop: false, direction: "forward" } });
5357
5367
  this.registerNewLocation(window.location);
5358
5368
  }
5369
+ if (clickLoading) {
5370
+ targetEl.classList.remove("phx-click-loading");
5371
+ }
5359
5372
  done();
5360
5373
  });
5361
5374
  });
5362
5375
  }
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
5376
  registerNewLocation(newLocation) {
5373
5377
  let { pathname, search } = this.currentLocation;
5374
5378
  if (pathname + search === newLocation.pathname + newLocation.search) {