phoenix_live_view 0.20.0 → 0.20.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.
@@ -69,6 +69,7 @@ var LiveView = (() => {
69
69
  var PHX_ACTIVE_ENTRY_REFS = "data-phx-active-refs";
70
70
  var PHX_LIVE_FILE_UPDATED = "phx:live-file:updated";
71
71
  var PHX_SKIP = "data-phx-skip";
72
+ var PHX_MAGIC_ID = "data-phx-id";
72
73
  var PHX_PRUNE = "data-phx-prune";
73
74
  var PHX_PAGE_LOADING = "page-loading";
74
75
  var PHX_CONNECTED_CLASS = "phx-connected";
@@ -123,6 +124,7 @@ var LiveView = (() => {
123
124
  };
124
125
  var DYNAMICS = "d";
125
126
  var STATIC = "s";
127
+ var ROOT = "r";
126
128
  var COMPONENTS = "c";
127
129
  var EVENTS = "e";
128
130
  var REPLY = "r";
@@ -145,6 +147,7 @@ var LiveView = (() => {
145
147
  if (this.errored) {
146
148
  return;
147
149
  }
150
+ this.uploadChannel.leave();
148
151
  this.errored = true;
149
152
  clearTimeout(this.chunkTimer);
150
153
  this.entry.error(reason);
@@ -564,12 +567,17 @@ var LiveView = (() => {
564
567
  el.setAttribute("data-phx-hook", "Phoenix.InfiniteScroll");
565
568
  }
566
569
  },
567
- maybeHideFeedback(container, input, phxFeedbackFor) {
568
- if (!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))) {
569
- let feedbacks = [input.name];
570
- if (input.name.endsWith("[]")) {
571
- feedbacks.push(input.name.slice(0, -2));
570
+ maybeHideFeedback(container, inputs, phxFeedbackFor) {
571
+ let feedbacks = [];
572
+ inputs.forEach((input) => {
573
+ if (!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))) {
574
+ feedbacks.push(input.name);
575
+ if (input.name.endsWith("[]")) {
576
+ feedbacks.push(input.name.slice(0, -2));
577
+ }
572
578
  }
579
+ });
580
+ if (feedbacks.length > 0) {
573
581
  let selector = feedbacks.map((f) => `[${phxFeedbackFor}="${f}"]`).join(", ");
574
582
  DOM.all(container, selector, (el) => el.classList.add(PHX_NO_FEEDBACK_CLASS));
575
583
  }
@@ -856,7 +864,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
856
864
  relative_path: this.file.webkitRelativePath,
857
865
  size: this.file.size,
858
866
  type: this.file.type,
859
- ref: this.ref
867
+ ref: this.ref,
868
+ meta: typeof this.file.meta === "function" ? this.file.meta() : void 0
860
869
  };
861
870
  }
862
871
  uploader(uploaders) {
@@ -913,6 +922,9 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
913
922
  entry.relative_path = file.webkitRelativePath;
914
923
  entry.type = file.type;
915
924
  entry.size = file.size;
925
+ if (typeof file.meta === "function") {
926
+ entry.meta = file.meta();
927
+ }
916
928
  fileData[uploadRef].push(entry);
917
929
  });
918
930
  return fileData;
@@ -1002,7 +1014,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
1002
1014
  return classes.find((name) => instance instanceof name);
1003
1015
  },
1004
1016
  isFocusable(el, interactiveOnly) {
1005
- return el instanceof HTMLAnchorElement && el.rel !== "ignore" || el instanceof HTMLAreaElement && el.href !== void 0 || !el.disabled && this.anyOf(el, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLButtonElement]) || el instanceof HTMLIFrameElement || (el.tabIndex > 0 || !interactiveOnly && el.tabIndex === 0 && el.getAttribute("tabindex") !== null && el.getAttribute("aria-hidden") !== "true");
1017
+ return el instanceof HTMLAnchorElement && el.rel !== "ignore" || el instanceof HTMLAreaElement && el.href !== void 0 || !el.disabled && this.anyOf(el, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLButtonElement]) || el instanceof HTMLIFrameElement || (el.tabIndex > 0 || !interactiveOnly && el.getAttribute("tabindex") !== null && el.getAttribute("aria-hidden") !== "true");
1006
1018
  },
1007
1019
  attemptFocus(el, interactiveOnly) {
1008
1020
  if (this.isFocusable(el, interactiveOnly)) {
@@ -1578,7 +1590,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
1578
1590
  }
1579
1591
  }
1580
1592
  function morphChildren(fromEl, toEl) {
1581
- var skipFrom = skipFromChildren(fromEl);
1593
+ var skipFrom = skipFromChildren(fromEl, toEl);
1582
1594
  var curToNodeChild = toEl.firstChild;
1583
1595
  var curFromNodeChild = fromEl.firstChild;
1584
1596
  var curToNodeKey;
@@ -1781,7 +1793,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
1781
1793
  el.setAttribute(PHX_PRUNE, "");
1782
1794
  });
1783
1795
  }
1784
- perform() {
1796
+ perform(isJoinPatch) {
1785
1797
  let { view, liveSocket, container, html } = this;
1786
1798
  let targetContainer = this.isCIDPatch() ? this.targetCIDContainer(html) : container;
1787
1799
  if (this.isCIDPatch() && !targetContainer) {
@@ -1800,19 +1812,18 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
1800
1812
  let updates = [];
1801
1813
  let appendPrependUpdates = [];
1802
1814
  let externalFormTriggered = null;
1803
- let diffHTML = liveSocket.time("premorph container prep", () => {
1804
- return this.buildDiffHTML(container, html, phxUpdate, targetContainer);
1805
- });
1806
1815
  this.trackBefore("added", container);
1807
1816
  this.trackBefore("updated", container, container);
1808
1817
  liveSocket.time("morphdom", () => {
1809
1818
  this.streams.forEach(([ref, inserts, deleteIds, reset]) => {
1810
1819
  Object.entries(inserts).forEach(([key, [streamAt, limit]]) => {
1811
- this.streamInserts[key] = { ref, streamAt, limit };
1820
+ this.streamInserts[key] = { ref, streamAt, limit, resetKept: false };
1812
1821
  });
1813
1822
  if (reset !== void 0) {
1814
1823
  dom_default.all(container, `[${PHX_STREAM_REF}="${ref}"]`, (child) => {
1815
- if (!inserts[child.id]) {
1824
+ if (inserts[child.id]) {
1825
+ this.streamInserts[child.id].resetKept = true;
1826
+ } else {
1816
1827
  this.removeStreamChildElement(child);
1817
1828
  }
1818
1829
  });
@@ -1824,10 +1835,16 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
1824
1835
  }
1825
1836
  });
1826
1837
  });
1827
- morphdom_esm_default(targetContainer, diffHTML, {
1838
+ morphdom_esm_default(targetContainer, html, {
1828
1839
  childrenOnly: targetContainer.getAttribute(PHX_COMPONENT) === null,
1829
1840
  getNodeKey: (node) => {
1830
- return dom_default.isPhxDestroyed(node) ? null : node.id;
1841
+ if (dom_default.isPhxDestroyed(node)) {
1842
+ return null;
1843
+ }
1844
+ if (isJoinPatch) {
1845
+ return node.id;
1846
+ }
1847
+ return node.id || node.getAttribute && node.getAttribute(PHX_MAGIC_ID);
1831
1848
  },
1832
1849
  skipFromChildren: (from) => {
1833
1850
  return from.getAttribute(phxUpdate) === PHX_STREAM;
@@ -1884,6 +1901,25 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
1884
1901
  }
1885
1902
  added.push(el);
1886
1903
  },
1904
+ onBeforeElChildrenUpdated: (fromEl, toEl) => {
1905
+ if (fromEl.getAttribute(phxUpdate) === PHX_STREAM) {
1906
+ let toIds = Array.from(toEl.children).map((child) => child.id);
1907
+ Array.from(fromEl.children).filter((child) => {
1908
+ let { resetKept } = this.getStreamInsert(child);
1909
+ return resetKept;
1910
+ }).sort((a, b) => {
1911
+ let aIdx = toIds.indexOf(a.id);
1912
+ let bIdx = toIds.indexOf(b.id);
1913
+ if (aIdx === bIdx) {
1914
+ return 0;
1915
+ } else if (aIdx < bIdx) {
1916
+ return -1;
1917
+ } else {
1918
+ return 1;
1919
+ }
1920
+ }).forEach((child) => fromEl.appendChild(child));
1921
+ }
1922
+ },
1887
1923
  onNodeDiscarded: (el) => this.onNodeDiscarded(el),
1888
1924
  onBeforeNodeDiscarded: (el) => {
1889
1925
  if (el.getAttribute && el.getAttribute(PHX_PRUNE) !== null) {
@@ -1977,9 +2013,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
1977
2013
  appendPrependUpdates.forEach((update) => update.perform());
1978
2014
  });
1979
2015
  }
1980
- trackedInputs.forEach((input) => {
1981
- dom_default.maybeHideFeedback(targetContainer, input, phxFeedbackFor);
1982
- });
2016
+ dom_default.maybeHideFeedback(targetContainer, trackedInputs, phxFeedbackFor);
1983
2017
  liveSocket.silenceEvents(() => dom_default.restoreFocus(focused, selectionStart, selectionEnd));
1984
2018
  dom_default.dispatchEvent(document, "phx:update");
1985
2019
  added.forEach((el) => this.trackAfter("added", el));
@@ -2058,7 +2092,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2058
2092
  return this.cidPatch;
2059
2093
  }
2060
2094
  skipCIDSibling(el) {
2061
- return el.nodeType === Node.ELEMENT_NODE && el.getAttribute(PHX_SKIP) !== null;
2095
+ return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);
2062
2096
  }
2063
2097
  targetCIDContainer(html) {
2064
2098
  if (!this.isCIDPatch()) {
@@ -2071,35 +2105,125 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2071
2105
  return first && first.parentNode;
2072
2106
  }
2073
2107
  }
2074
- buildDiffHTML(container, html, phxUpdate, targetContainer) {
2075
- let isCIDPatch = this.isCIDPatch();
2076
- let isCIDWithSingleRoot = isCIDPatch && targetContainer.getAttribute(PHX_COMPONENT) === this.targetCID.toString();
2077
- if (!isCIDPatch || isCIDWithSingleRoot) {
2078
- return html;
2079
- } else {
2080
- let diffContainer = null;
2081
- let template = document.createElement("template");
2082
- diffContainer = dom_default.cloneNode(targetContainer);
2083
- let [firstComponent, ...rest] = dom_default.findComponentNodeList(diffContainer, this.targetCID);
2084
- template.innerHTML = html;
2085
- rest.forEach((el) => el.remove());
2086
- Array.from(diffContainer.childNodes).forEach((child) => {
2087
- if (child.id && child.nodeType === Node.ELEMENT_NODE && child.getAttribute(PHX_COMPONENT) !== this.targetCID.toString()) {
2088
- child.setAttribute(PHX_SKIP, "");
2089
- child.innerHTML = "";
2090
- }
2091
- });
2092
- Array.from(template.content.childNodes).forEach((el) => diffContainer.insertBefore(el, firstComponent));
2093
- firstComponent.remove();
2094
- return diffContainer.outerHTML;
2095
- }
2096
- }
2097
2108
  indexOf(parent, child) {
2098
2109
  return Array.from(parent.children).indexOf(child);
2099
2110
  }
2100
2111
  };
2101
2112
 
2102
2113
  // js/phoenix_live_view/rendered.js
2114
+ var VOID_TAGS = new Set([
2115
+ "area",
2116
+ "base",
2117
+ "br",
2118
+ "col",
2119
+ "command",
2120
+ "embed",
2121
+ "hr",
2122
+ "img",
2123
+ "input",
2124
+ "keygen",
2125
+ "link",
2126
+ "meta",
2127
+ "param",
2128
+ "source",
2129
+ "track",
2130
+ "wbr"
2131
+ ]);
2132
+ var endingTagNameChars = new Set([">", "/", " ", "\n", " ", "\r"]);
2133
+ var quoteChars = new Set(["'", '"']);
2134
+ var modifyRoot = (html, attrs, clearInnerHTML) => {
2135
+ let i = 0;
2136
+ let insideComment = false;
2137
+ let beforeTag, afterTag, tag, tagNameEndsAt, id, newHTML;
2138
+ while (i < html.length) {
2139
+ let char = html.charAt(i);
2140
+ if (insideComment) {
2141
+ if (char === "-" && html.slice(i, i + 3) === "-->") {
2142
+ insideComment = false;
2143
+ i += 3;
2144
+ } else {
2145
+ i++;
2146
+ }
2147
+ } else if (char === "<" && html.slice(i, i + 4) === "<!--") {
2148
+ insideComment = true;
2149
+ i += 4;
2150
+ } else if (char === "<") {
2151
+ beforeTag = html.slice(0, i);
2152
+ let iAtOpen = i;
2153
+ i++;
2154
+ for (i; i < html.length; i++) {
2155
+ if (endingTagNameChars.has(html.charAt(i))) {
2156
+ break;
2157
+ }
2158
+ }
2159
+ tagNameEndsAt = i;
2160
+ tag = html.slice(iAtOpen + 1, tagNameEndsAt);
2161
+ for (i; i < html.length; i++) {
2162
+ if (html.charAt(i) === ">") {
2163
+ break;
2164
+ }
2165
+ if (html.charAt(i) === "=") {
2166
+ let isId = html.slice(i - 3, i) === " id";
2167
+ i++;
2168
+ let char2 = html.charAt(i);
2169
+ if (quoteChars.has(char2)) {
2170
+ let attrStartsAt = i;
2171
+ i++;
2172
+ for (i; i < html.length; i++) {
2173
+ if (html.charAt(i) === char2) {
2174
+ break;
2175
+ }
2176
+ }
2177
+ if (isId) {
2178
+ id = html.slice(attrStartsAt + 1, i);
2179
+ break;
2180
+ }
2181
+ }
2182
+ }
2183
+ }
2184
+ break;
2185
+ } else {
2186
+ i++;
2187
+ }
2188
+ }
2189
+ if (!tag) {
2190
+ throw new Error(`malformed html ${html}`);
2191
+ }
2192
+ let closeAt = html.length - 1;
2193
+ insideComment = false;
2194
+ while (closeAt >= beforeTag.length + tag.length) {
2195
+ let char = html.charAt(closeAt);
2196
+ if (insideComment) {
2197
+ if (char === "-" && html.slice(closeAt - 3, closeAt) === "<!-") {
2198
+ insideComment = false;
2199
+ closeAt -= 4;
2200
+ } else {
2201
+ closeAt -= 1;
2202
+ }
2203
+ } else if (char === ">" && html.slice(closeAt - 2, closeAt) === "--") {
2204
+ insideComment = true;
2205
+ closeAt -= 3;
2206
+ } else if (char === ">") {
2207
+ break;
2208
+ } else {
2209
+ closeAt -= 1;
2210
+ }
2211
+ }
2212
+ afterTag = html.slice(closeAt + 1, html.length);
2213
+ let attrsStr = Object.keys(attrs).map((attr) => attrs[attr] === true ? attr : `${attr}="${attrs[attr]}"`).join(" ");
2214
+ if (clearInnerHTML) {
2215
+ let idAttrStr = id ? ` id="${id}"` : "";
2216
+ if (VOID_TAGS.has(tag)) {
2217
+ newHTML = `<${tag}${idAttrStr}${attrsStr === "" ? "" : " "}${attrsStr}/>`;
2218
+ } else {
2219
+ newHTML = `<${tag}${idAttrStr}${attrsStr === "" ? "" : " "}${attrsStr}></${tag}>`;
2220
+ }
2221
+ } else {
2222
+ let rest = html.slice(tagNameEndsAt, closeAt + 1);
2223
+ newHTML = `<${tag}${attrsStr === "" ? "" : " "}${attrsStr}${rest}`;
2224
+ }
2225
+ return [newHTML, beforeTag, afterTag];
2226
+ };
2103
2227
  var Rendered = class {
2104
2228
  static extract(diff) {
2105
2229
  let { [REPLY]: reply, [EVENTS]: events, [TITLE]: title } = diff;
@@ -2111,19 +2235,20 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2111
2235
  constructor(viewId, rendered) {
2112
2236
  this.viewId = viewId;
2113
2237
  this.rendered = {};
2238
+ this.magicId = 0;
2114
2239
  this.mergeDiff(rendered);
2115
2240
  }
2116
2241
  parentViewId() {
2117
2242
  return this.viewId;
2118
2243
  }
2119
2244
  toString(onlyCids) {
2120
- let [str, streams] = this.recursiveToString(this.rendered, this.rendered[COMPONENTS], onlyCids);
2245
+ let [str, streams] = this.recursiveToString(this.rendered, this.rendered[COMPONENTS], onlyCids, true, {});
2121
2246
  return [str, streams];
2122
2247
  }
2123
- recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids) {
2248
+ recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids, changeTracking, rootAttrs) {
2124
2249
  onlyCids = onlyCids ? new Set(onlyCids) : null;
2125
2250
  let output = { buffer: "", components, onlyCids, streams: new Set() };
2126
- this.toOutputBuffer(rendered, null, output);
2251
+ this.toOutputBuffer(rendered, null, output, changeTracking, rootAttrs);
2127
2252
  return [output.buffer, output.streams];
2128
2253
  }
2129
2254
  componentCIDs(diff) {
@@ -2168,10 +2293,10 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2168
2293
  tdiff = oldc[-scid];
2169
2294
  }
2170
2295
  stat = tdiff[STATIC];
2171
- ndiff = this.cloneMerge(tdiff, cdiff);
2296
+ ndiff = this.cloneMerge(tdiff, cdiff, true);
2172
2297
  ndiff[STATIC] = stat;
2173
2298
  } else {
2174
- ndiff = cdiff[STATIC] !== void 0 ? cdiff : this.cloneMerge(oldc[cid] || {}, cdiff);
2299
+ ndiff = cdiff[STATIC] !== void 0 || oldc[cid] === void 0 ? cdiff : this.cloneMerge(oldc[cid], cdiff, false);
2175
2300
  }
2176
2301
  cache[cid] = ndiff;
2177
2302
  return ndiff;
@@ -2196,21 +2321,31 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2196
2321
  target[key] = val;
2197
2322
  }
2198
2323
  }
2324
+ if (target[ROOT]) {
2325
+ target.newRender = true;
2326
+ }
2199
2327
  }
2200
- cloneMerge(target, source) {
2328
+ cloneMerge(target, source, pruneMagicId) {
2201
2329
  let merged = __spreadValues(__spreadValues({}, target), source);
2202
2330
  for (let key in merged) {
2203
2331
  let val = source[key];
2204
2332
  let targetVal = target[key];
2205
2333
  if (isObject(val) && val[STATIC] === void 0 && isObject(targetVal)) {
2206
- merged[key] = this.cloneMerge(targetVal, val);
2334
+ merged[key] = this.cloneMerge(targetVal, val, pruneMagicId);
2207
2335
  }
2208
2336
  }
2337
+ if (pruneMagicId) {
2338
+ delete merged.magicId;
2339
+ delete merged.newRender;
2340
+ } else if (target[ROOT]) {
2341
+ merged.newRender = true;
2342
+ }
2209
2343
  return merged;
2210
2344
  }
2211
2345
  componentToString(cid) {
2212
- let [str, streams] = this.recursiveCIDToString(this.rendered[COMPONENTS], cid, null, false);
2213
- return [str, streams];
2346
+ let [str, streams] = this.recursiveCIDToString(this.rendered[COMPONENTS], cid, null);
2347
+ let [strippedHTML, _before, _after] = modifyRoot(str, {});
2348
+ return [strippedHTML, streams];
2214
2349
  }
2215
2350
  pruneCIDs(cids) {
2216
2351
  cids.forEach((cid) => delete this.rendered[COMPONENTS][cid]);
@@ -2228,17 +2363,46 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2228
2363
  return part;
2229
2364
  }
2230
2365
  }
2231
- toOutputBuffer(rendered, templates, output) {
2366
+ nextMagicID() {
2367
+ this.magicId++;
2368
+ return `${this.parentViewId()}-${this.magicId}`;
2369
+ }
2370
+ toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}) {
2232
2371
  if (rendered[DYNAMICS]) {
2233
2372
  return this.comprehensionToBuffer(rendered, templates, output);
2234
2373
  }
2235
2374
  let { [STATIC]: statics } = rendered;
2236
2375
  statics = this.templateStatic(statics, templates);
2376
+ let isRoot = rendered[ROOT];
2377
+ let prevBuffer = output.buffer;
2378
+ if (isRoot) {
2379
+ output.buffer = "";
2380
+ }
2381
+ if (changeTracking && isRoot && !rendered.magicId) {
2382
+ rendered.newRender = true;
2383
+ rendered.magicId = this.nextMagicID();
2384
+ }
2237
2385
  output.buffer += statics[0];
2238
2386
  for (let i = 1; i < statics.length; i++) {
2239
- this.dynamicToBuffer(rendered[i - 1], templates, output);
2387
+ this.dynamicToBuffer(rendered[i - 1], templates, output, changeTracking);
2240
2388
  output.buffer += statics[i];
2241
2389
  }
2390
+ if (isRoot) {
2391
+ let skip = false;
2392
+ let attrs;
2393
+ if (changeTracking || Object.keys(rootAttrs).length > 0) {
2394
+ skip = !rendered.newRender;
2395
+ attrs = __spreadValues({ [PHX_MAGIC_ID]: rendered.magicId }, rootAttrs);
2396
+ } else {
2397
+ attrs = rootAttrs;
2398
+ }
2399
+ if (skip) {
2400
+ attrs[PHX_SKIP] = true;
2401
+ }
2402
+ let [newRoot, commentBefore, commentAfter] = modifyRoot(output.buffer, attrs, skip);
2403
+ rendered.newRender = false;
2404
+ output.buffer = prevBuffer + commentBefore + newRoot + commentAfter;
2405
+ }
2242
2406
  }
2243
2407
  comprehensionToBuffer(rendered, templates, output) {
2244
2408
  let { [DYNAMICS]: dynamics, [STATIC]: statics, [STREAM]: stream } = rendered;
@@ -2249,7 +2413,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2249
2413
  let dynamic = dynamics[d];
2250
2414
  output.buffer += statics[0];
2251
2415
  for (let i = 1; i < statics.length; i++) {
2252
- this.dynamicToBuffer(dynamic[i - 1], compTemplates, output);
2416
+ let changeTracking = false;
2417
+ this.dynamicToBuffer(dynamic[i - 1], compTemplates, output, changeTracking);
2253
2418
  output.buffer += statics[i];
2254
2419
  }
2255
2420
  }
@@ -2259,74 +2424,26 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
2259
2424
  output.streams.add(stream);
2260
2425
  }
2261
2426
  }
2262
- dynamicToBuffer(rendered, templates, output) {
2427
+ dynamicToBuffer(rendered, templates, output, changeTracking) {
2263
2428
  if (typeof rendered === "number") {
2264
2429
  let [str, streams] = this.recursiveCIDToString(output.components, rendered, output.onlyCids);
2265
2430
  output.buffer += str;
2266
2431
  output.streams = new Set([...output.streams, ...streams]);
2267
2432
  } else if (isObject(rendered)) {
2268
- this.toOutputBuffer(rendered, templates, output);
2433
+ this.toOutputBuffer(rendered, templates, output, changeTracking, {});
2269
2434
  } else {
2270
2435
  output.buffer += rendered;
2271
2436
  }
2272
2437
  }
2273
- recursiveCIDToString(components, cid, onlyCids, allowRootComments = true) {
2438
+ recursiveCIDToString(components, cid, onlyCids) {
2274
2439
  let component = components[cid] || logError(`no component for CID ${cid}`, components);
2275
- let template = document.createElement("template");
2276
- let [html, streams] = this.recursiveToString(component, components, onlyCids);
2277
- template.innerHTML = html;
2278
- let container = template.content;
2440
+ let attrs = { [PHX_COMPONENT]: cid };
2279
2441
  let skip = onlyCids && !onlyCids.has(cid);
2280
- let [hasChildNodes, hasChildComponents] = Array.from(container.childNodes).reduce(([hasNodes, hasComponents], child, i) => {
2281
- if (child.nodeType === Node.ELEMENT_NODE) {
2282
- if (child.getAttribute(PHX_COMPONENT)) {
2283
- return [hasNodes, true];
2284
- }
2285
- child.setAttribute(PHX_COMPONENT, cid);
2286
- if (!child.id) {
2287
- child.id = `${this.parentViewId()}-${cid}-${i}`;
2288
- }
2289
- if (skip) {
2290
- child.setAttribute(PHX_SKIP, "");
2291
- child.innerHTML = "";
2292
- }
2293
- return [true, hasComponents];
2294
- } else if (child.nodeType === Node.COMMENT_NODE) {
2295
- if (!allowRootComments) {
2296
- child.remove();
2297
- }
2298
- return [hasNodes, hasComponents];
2299
- } else {
2300
- if (child.nodeValue.trim() !== "") {
2301
- logError(`only HTML element tags are allowed at the root of components.
2302
-
2303
- got: "${child.nodeValue.trim()}"
2304
-
2305
- within:
2306
- `, template.innerHTML.trim());
2307
- child.replaceWith(this.createSpan(child.nodeValue, cid));
2308
- return [true, hasComponents];
2309
- } else {
2310
- child.remove();
2311
- return [hasNodes, hasComponents];
2312
- }
2313
- }
2314
- }, [false, false]);
2315
- if (!hasChildNodes && !hasChildComponents) {
2316
- logError("expected at least one HTML element tag inside a component, but the component is empty:\n", template.innerHTML.trim());
2317
- return [this.createSpan("", cid).outerHTML, streams];
2318
- } else if (!hasChildNodes && hasChildComponents) {
2319
- logError("expected at least one HTML element tag directly inside a component, but only subcomponents were found. A component must render at least one HTML tag directly inside itself.", template.innerHTML.trim());
2320
- return [template.innerHTML, streams];
2321
- } else {
2322
- return [template.innerHTML, streams];
2323
- }
2324
- }
2325
- createSpan(text, cid) {
2326
- let span = document.createElement("span");
2327
- span.innerText = text;
2328
- span.setAttribute(PHX_COMPONENT, cid);
2329
- return span;
2442
+ component.newRender = !skip;
2443
+ component.magicId = `${this.parentViewId()}-c-${cid}`;
2444
+ let changeTracking = true;
2445
+ let [html, streams] = this.recursiveToString(component, components, onlyCids, changeTracking, attrs);
2446
+ return [html, streams];
2330
2447
  }
2331
2448
  };
2332
2449
 
@@ -2395,10 +2512,12 @@ within:
2395
2512
  this.__listeners.delete(callbackRef);
2396
2513
  }
2397
2514
  upload(name, files) {
2398
- return this.__view.dispatchUploads(name, files);
2515
+ return this.__view.dispatchUploads(null, name, files);
2399
2516
  }
2400
2517
  uploadTo(phxTarget, name, files) {
2401
- return this.__view.withinTargets(phxTarget, (view) => view.dispatchUploads(name, files));
2518
+ return this.__view.withinTargets(phxTarget, (view, targetCtx) => {
2519
+ view.dispatchUploads(targetCtx, name, files);
2520
+ });
2402
2521
  }
2403
2522
  __cleanup__() {
2404
2523
  this.__listeners.forEach((callbackRef) => this.removeHandleEvent(callbackRef));
@@ -2424,6 +2543,10 @@ within:
2424
2543
  isVisible(el) {
2425
2544
  return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0);
2426
2545
  },
2546
+ isInViewport(el) {
2547
+ const rect = el.getBoundingClientRect();
2548
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
2549
+ },
2427
2550
  exec_exec(eventType, phxEvent, view, sourceEl, el, [attr, to]) {
2428
2551
  let nodes = to ? dom_default.all(document, to) : [sourceEl];
2429
2552
  nodes.forEach((node) => {
@@ -2682,9 +2805,10 @@ within:
2682
2805
  this.children = this.parent ? null : {};
2683
2806
  this.root.children[this.id] = {};
2684
2807
  this.channel = this.liveSocket.channel(`lv:${this.id}`, () => {
2808
+ let url = this.href && this.expandURL(this.href);
2685
2809
  return {
2686
- redirect: this.redirect ? this.href : void 0,
2687
- url: this.redirect ? void 0 : this.href || void 0,
2810
+ redirect: this.redirect ? url : void 0,
2811
+ url: this.redirect ? void 0 : url || void 0,
2688
2812
  params: this.connectParams(liveReferer),
2689
2813
  session: this.getSession(),
2690
2814
  static: this.getStatic(),
@@ -2882,7 +3006,7 @@ within:
2882
3006
  this.attachTrueDocEl();
2883
3007
  let patch = new DOMPatch(this, this.el, this.id, html, streams, null);
2884
3008
  patch.markPrunableContentForRemoval();
2885
- this.performPatch(patch, false);
3009
+ this.performPatch(patch, false, true);
2886
3010
  this.joinNewChildren();
2887
3011
  this.execNewMounted();
2888
3012
  this.joinPending = false;
@@ -2921,7 +3045,7 @@ within:
2921
3045
  newHook.__mounted();
2922
3046
  }
2923
3047
  }
2924
- performPatch(patch, pruneCids) {
3048
+ performPatch(patch, pruneCids, isJoinPatch = false) {
2925
3049
  let removedEls = [];
2926
3050
  let phxChildrenAdded = false;
2927
3051
  let updatedHookIds = new Set();
@@ -2956,7 +3080,7 @@ within:
2956
3080
  }
2957
3081
  });
2958
3082
  patch.after("transitionsDiscarded", (els) => this.afterElementsRemoved(els, pruneCids));
2959
- patch.perform();
3083
+ patch.perform(isJoinPatch);
2960
3084
  this.afterElementsRemoved(removedEls, pruneCids);
2961
3085
  return phxChildrenAdded;
2962
3086
  }
@@ -3361,7 +3485,7 @@ within:
3361
3485
  if (isCid(targetCtx)) {
3362
3486
  return targetCtx;
3363
3487
  }
3364
- let cidOrSelector = target.getAttribute(this.binding("target"));
3488
+ let cidOrSelector = opts.target || target.getAttribute(this.binding("target"));
3365
3489
  if (isCid(cidOrSelector)) {
3366
3490
  return parseInt(cidOrSelector);
3367
3491
  } else if (targetCtx && (cidOrSelector !== null || opts.target)) {
@@ -3445,7 +3569,7 @@ within:
3445
3569
  }
3446
3570
  pushInput(inputEl, targetCtx, forceCid, phxEvent, opts, callback) {
3447
3571
  let uploads;
3448
- let cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx);
3572
+ let cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx, opts);
3449
3573
  let refGenerator = () => this.putRef([inputEl, inputEl.form], "change", opts);
3450
3574
  let formData;
3451
3575
  let meta = this.extractMeta(inputEl.form);
@@ -3541,7 +3665,7 @@ within:
3541
3665
  let cid = this.targetComponentID(formEl, targetCtx);
3542
3666
  if (LiveUploader.hasUploadsInProgress(formEl)) {
3543
3667
  let [ref, _els] = refGenerator();
3544
- let push = () => this.pushFormSubmit(formEl, submitter, targetCtx, phxEvent, opts, onReply);
3668
+ let push = () => this.pushFormSubmit(formEl, targetCtx, phxEvent, submitter, opts, onReply);
3545
3669
  return this.scheduleSubmit(formEl, ref, opts, push);
3546
3670
  } else if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {
3547
3671
  let [ref, els] = refGenerator();
@@ -3605,8 +3729,9 @@ within:
3605
3729
  });
3606
3730
  });
3607
3731
  }
3608
- dispatchUploads(name, filesOrBlobs) {
3609
- let inputs = dom_default.findUploadInputs(this.el).filter((el) => el.name === name);
3732
+ dispatchUploads(targetCtx, name, filesOrBlobs) {
3733
+ let targetElement = this.targetCtxElement(targetCtx) || this.el;
3734
+ let inputs = dom_default.findUploadInputs(targetElement).filter((el) => el.name === name);
3610
3735
  if (inputs.length === 0) {
3611
3736
  logError(`no live file inputs found matching the name "${name}"`);
3612
3737
  } else if (inputs.length > 1) {
@@ -3615,6 +3740,16 @@ within:
3615
3740
  dom_default.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, { detail: { files: filesOrBlobs } });
3616
3741
  }
3617
3742
  }
3743
+ targetCtxElement(targetCtx) {
3744
+ if (isCid(targetCtx)) {
3745
+ let [target] = dom_default.findComponentNodeList(this.el, targetCtx);
3746
+ return target;
3747
+ } else if (targetCtx) {
3748
+ return targetCtx;
3749
+ } else {
3750
+ return null;
3751
+ }
3752
+ }
3618
3753
  pushFormRecovery(form, newCid, callback) {
3619
3754
  this.liveSocket.withinOwners(form, (view, targetCtx) => {
3620
3755
  let phxChange = this.binding("change");
@@ -4121,7 +4256,7 @@ within:
4121
4256
  if (!dead) {
4122
4257
  this.bindForms();
4123
4258
  }
4124
- this.bind({ keyup: "keyup", keydown: "keydown" }, (e, type, view, targetEl, phxEvent, eventTarget) => {
4259
+ this.bind({ keyup: "keyup", keydown: "keydown" }, (e, type, view, targetEl, phxEvent, phxTarget) => {
4125
4260
  let matchKey = targetEl.getAttribute(this.binding(PHX_KEY));
4126
4261
  let pressedKey = e.key && e.key.toLowerCase();
4127
4262
  if (matchKey && matchKey.toLowerCase() !== pressedKey) {
@@ -4130,13 +4265,13 @@ within:
4130
4265
  let data = __spreadValues({ key: e.key }, this.eventMeta(type, e, targetEl));
4131
4266
  js_default.exec(type, phxEvent, view, targetEl, ["push", { data }]);
4132
4267
  });
4133
- this.bind({ blur: "focusout", focus: "focusin" }, (e, type, view, targetEl, phxEvent, eventTarget) => {
4134
- if (!eventTarget) {
4268
+ this.bind({ blur: "focusout", focus: "focusin" }, (e, type, view, targetEl, phxEvent, phxTarget) => {
4269
+ if (!phxTarget) {
4135
4270
  let data = __spreadValues({ key: e.key }, this.eventMeta(type, e, targetEl));
4136
4271
  js_default.exec(type, phxEvent, view, targetEl, ["push", { data }]);
4137
4272
  }
4138
4273
  });
4139
- this.bind({ blur: "blur", focus: "focus" }, (e, type, view, targetEl, targetCtx, phxEvent, phxTarget) => {
4274
+ this.bind({ blur: "blur", focus: "focus" }, (e, type, view, targetEl, phxEvent, phxTarget) => {
4140
4275
  if (phxTarget === "window") {
4141
4276
  let data = this.eventMeta(type, e, targetEl);
4142
4277
  js_default.exec(type, phxEvent, view, targetEl, ["push", { data }]);
@@ -4217,7 +4352,7 @@ within:
4217
4352
  }
4218
4353
  }
4219
4354
  bindClicks() {
4220
- window.addEventListener("click", (e) => this.clickStartedAtTarget = e.target);
4355
+ window.addEventListener("mousedown", (e) => this.clickStartedAtTarget = e.target);
4221
4356
  this.bindClick("click", "click", false);
4222
4357
  this.bindClick("mousedown", "capture-click", true);
4223
4358
  }
@@ -4259,7 +4394,7 @@ within:
4259
4394
  if (!(el.isSameNode(clickStartedAt) || el.contains(clickStartedAt))) {
4260
4395
  this.withinOwners(e.target, (view) => {
4261
4396
  let phxEvent = el.getAttribute(phxClickAway);
4262
- if (js_default.isVisible(el)) {
4397
+ if (js_default.isVisible(el) && js_default.isInViewport(el)) {
4263
4398
  js_default.exec("click", phxEvent, view, el, ["push", { data: this.eventMeta("click", e, e.target) }]);
4264
4399
  }
4265
4400
  });
@@ -4308,7 +4443,7 @@ within:
4308
4443
  if (!type || !this.isConnected() || !this.main || dom_default.wantsNewTab(e)) {
4309
4444
  return;
4310
4445
  }
4311
- let href = target.href;
4446
+ let href = target.href instanceof SVGAnimatedString ? target.href.baseVal : target.href;
4312
4447
  let linkState = target.getAttribute(PHX_LINK_STATE);
4313
4448
  e.preventDefault();
4314
4449
  e.stopImmediatePropagation();