phoenix_live_view 0.20.9 → 0.20.11

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.
@@ -70,7 +70,7 @@ export default class LiveUploader {
70
70
  static trackFiles(inputEl, files, dataTransfer){
71
71
  if(inputEl.getAttribute("multiple") !== null){
72
72
  let newFiles = files.filter(file => !this.activeFiles(inputEl).find(f => Object.is(f, file)))
73
- DOM.putPrivate(inputEl, "files", this.activeFiles(inputEl).concat(newFiles))
73
+ DOM.updatePrivate(inputEl, "files", [], (existing) => existing.concat(newFiles))
74
74
  inputEl.value = null
75
75
  } else {
76
76
  // Reset inputEl files to align output with programmatic changes (i.e. drag and drop)
@@ -102,28 +102,36 @@ export default class LiveUploader {
102
102
  }
103
103
 
104
104
  constructor(inputEl, view, onComplete){
105
+ this.autoUpload = DOM.isAutoUpload(inputEl)
105
106
  this.view = view
106
107
  this.onComplete = onComplete
107
108
  this._entries =
108
109
  Array.from(LiveUploader.filesAwaitingPreflight(inputEl) || [])
109
- .map(file => new UploadEntry(inputEl, file, view))
110
+ .map(file => new UploadEntry(inputEl, file, view, this.autoUpload))
110
111
 
111
- // prevent sending duplicate preflight requests
112
+ // prevent sending duplicate preflight requests
112
113
  LiveUploader.markPreflightInProgress(this._entries)
113
114
 
114
115
  this.numEntriesInProgress = this._entries.length
115
116
  }
116
117
 
118
+ isAutoUpload(){ return this.autoUpload }
119
+
117
120
  entries(){ return this._entries }
118
121
 
119
122
  initAdapterUpload(resp, onError, liveSocket){
120
123
  this._entries =
121
124
  this._entries.map(entry => {
122
- entry.zipPostFlight(resp)
123
- entry.onDone(() => {
125
+ if(entry.isCancelled()){
124
126
  this.numEntriesInProgress--
125
127
  if(this.numEntriesInProgress === 0){ this.onComplete() }
126
- })
128
+ } else {
129
+ entry.zipPostFlight(resp)
130
+ entry.onDone(() => {
131
+ this.numEntriesInProgress--
132
+ if(this.numEntriesInProgress === 0){ this.onComplete() }
133
+ })
134
+ }
127
135
  return entry
128
136
  })
129
137
 
@@ -10,15 +10,13 @@ import {
10
10
  } from "./utils"
11
11
 
12
12
  import LiveUploader from "./live_uploader"
13
- import DOM from "./dom"
14
13
 
15
14
  export default class UploadEntry {
16
15
  static isActive(fileEl, file){
17
16
  let isNew = file._phxRef === undefined
18
- let isPreflightInProgress = UploadEntry.isPreflightInProgress(file)
19
17
  let activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(",")
20
18
  let isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0
21
- return file.size > 0 && (isNew || isActive || !isPreflightInProgress)
19
+ return file.size > 0 && (isNew || isActive)
22
20
  }
23
21
 
24
22
  static isPreflighted(fileEl, file){
@@ -35,7 +33,7 @@ export default class UploadEntry {
35
33
  file._preflightInProgress = true
36
34
  }
37
35
 
38
- constructor(fileEl, file, view){
36
+ constructor(fileEl, file, view, autoUpload){
39
37
  this.ref = LiveUploader.genFileRef(file)
40
38
  this.fileEl = fileEl
41
39
  this.file = file
@@ -45,9 +43,10 @@ export default class UploadEntry {
45
43
  this._isDone = false
46
44
  this._progress = 0
47
45
  this._lastProgressSent = -1
48
- this._onDone = function (){ }
46
+ this._onDone = function(){ }
49
47
  this._onElUpdated = this.onElUpdated.bind(this)
50
48
  this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated)
49
+ this.autoUpload = autoUpload
51
50
  }
52
51
 
53
52
  metadata(){ return this.meta }
@@ -70,7 +69,10 @@ export default class UploadEntry {
70
69
  }
71
70
  }
72
71
 
72
+ isCancelled(){ return this._isCancelled }
73
+
73
74
  cancel(){
75
+ this.file._preflightInProgress = false
74
76
  this._isCancelled = true
75
77
  this._isDone = true
76
78
  this._onDone()
@@ -81,9 +83,11 @@ export default class UploadEntry {
81
83
  error(reason = "failed"){
82
84
  this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated)
83
85
  this.view.pushFileProgress(this.fileEl, this.ref, {error: reason})
84
- if(!DOM.isAutoUpload(this.fileEl)){ LiveUploader.clearFiles(this.fileEl) }
86
+ if(!this.isAutoUpload()){ LiveUploader.clearFiles(this.fileEl) }
85
87
  }
86
88
 
89
+ isAutoUpload(){ return this.autoUpload }
90
+
87
91
  //private
88
92
 
89
93
  onDone(callback){
@@ -95,7 +99,10 @@ export default class UploadEntry {
95
99
 
96
100
  onElUpdated(){
97
101
  let activeRefs = this.fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(",")
98
- if(activeRefs.indexOf(this.ref) === -1){ this.cancel() }
102
+ if(activeRefs.indexOf(this.ref) === -1){
103
+ LiveUploader.untrackFile(this.fileEl, this.file)
104
+ this.cancel()
105
+ }
99
106
  }
100
107
 
101
108
  toPreflightPayload(){
@@ -120,6 +120,7 @@ export default class View {
120
120
  this.childJoins = 0
121
121
  this.loaderTimer = null
122
122
  this.pendingDiffs = []
123
+ this.pendingForms = new Set()
123
124
  this.redirect = false
124
125
  this.href = null
125
126
  this.joinCount = this.parent ? this.parent.joinCount - 1 : 0
@@ -282,12 +283,16 @@ export default class View {
282
283
  this.rendered = new Rendered(this.id, diff)
283
284
  let [html, streams] = this.renderContainer(null, "join")
284
285
  this.dropPendingRefs()
285
- let forms = this.formsForRecovery(html)
286
+ let forms = this.formsForRecovery(html).filter(([form, newForm, newCid]) => {
287
+ return !this.pendingForms.has(form.id)
288
+ })
286
289
  this.joinCount++
287
290
 
288
291
  if(forms.length > 0){
289
292
  forms.forEach(([form, newForm, newCid], i) => {
293
+ this.pendingForms.add(form.id)
290
294
  this.pushFormRecovery(form, newCid, resp => {
295
+ this.pendingForms.delete(form.id)
291
296
  if(i === forms.length - 1){
292
297
  this.onJoinComplete(resp, html, streams, events)
293
298
  }
@@ -307,6 +312,9 @@ export default class View {
307
312
  }
308
313
 
309
314
  onJoinComplete({live_patch}, html, streams, events){
315
+ // we can clear pending form recoveries now that we've joined.
316
+ // They either all resolved or were abandoned
317
+ this.pendingForms.clear()
310
318
  // In order to provide a better experience, we want to join
311
319
  // all LiveViews first and only then apply their patches.
312
320
  if(this.joinCount > 1 || (this.parent && !this.parent.isJoinPending())){
@@ -1029,7 +1037,12 @@ export default class View {
1029
1037
  } else if(LiveUploader.inputsAwaitingPreflight(formEl).length > 0){
1030
1038
  let [ref, els] = refGenerator()
1031
1039
  let proxyRefGen = () => [ref, els, opts]
1032
- this.uploadFiles(formEl, targetCtx, ref, cid, (_uploads) => {
1040
+ this.uploadFiles(formEl, targetCtx, ref, cid, (uploads) => {
1041
+ // if we still having pending preflights it means we have invalid entries
1042
+ // and the phx-submit cannot be completed
1043
+ if(LiveUploader.inputsAwaitingPreflight(formEl).length > 0){
1044
+ return this.undoRefs(ref)
1045
+ }
1033
1046
  let meta = this.extractMeta(formEl)
1034
1047
  let formData = serializeForm(formEl, {submitter, ...meta})
1035
1048
  this.pushWithReply(proxyRefGen, "event", {
@@ -1065,7 +1078,7 @@ export default class View {
1065
1078
 
1066
1079
  let entries = uploader.entries().map(entry => entry.toPreflightPayload())
1067
1080
 
1068
- if (entries.length === 0) {
1081
+ if(entries.length === 0) {
1069
1082
  numFileInputsInProgress--
1070
1083
  return
1071
1084
  }
@@ -1080,10 +1093,21 @@ export default class View {
1080
1093
 
1081
1094
  this.pushWithReply(null, "allow_upload", payload, resp => {
1082
1095
  this.log("upload", () => ["got preflight response", resp])
1083
- if(resp.error){
1096
+ // the preflight will reject entries beyond the max entries
1097
+ // so we error and cancel entries on the client that are missing from the response
1098
+ uploader.entries().forEach(entry => {
1099
+ if(resp.entries && !resp.entries[entry.ref]){
1100
+ this.handleFailedEntryPreflight(entry.ref, "failed preflight", uploader)
1101
+ }
1102
+ })
1103
+ // for auto uploas, we may have an empty entries response from the server
1104
+ // for form submits that contain invalid entries
1105
+ if(resp.error || Object.keys(resp.entries).length === 0){
1084
1106
  this.undoRefs(ref)
1085
- let [entry_ref, reason] = resp.error
1086
- this.log("upload", () => [`error for entry ${entry_ref}`, reason])
1107
+ let errors = resp.error || []
1108
+ errors.map(([entry_ref, reason]) => {
1109
+ this.handleFailedEntryPreflight(entry_ref, reason, uploader)
1110
+ })
1087
1111
  } else {
1088
1112
  let onError = (callback) => {
1089
1113
  this.channel.onError(() => {
@@ -1096,6 +1120,17 @@ export default class View {
1096
1120
  })
1097
1121
  }
1098
1122
 
1123
+ handleFailedEntryPreflight(uploadRef, reason, uploader){
1124
+ if(uploader.isAutoUpload()){
1125
+ // uploadRef may be top level upload config ref or entry ref
1126
+ let entry = uploader.entries().find(entry => entry.ref === uploadRef.toString())
1127
+ if(entry){ entry.cancel() }
1128
+ } else {
1129
+ uploader.entries().map(entry => entry.cancel())
1130
+ }
1131
+ this.log("upload", () => [`error for entry ${uploadRef}`, reason])
1132
+ }
1133
+
1099
1134
  dispatchUploads(targetCtx, name, filesOrBlobs){
1100
1135
  let targetElement = this.targetCtxElement(targetCtx) || this.el
1101
1136
  let inputs = DOM.findUploadInputs(targetElement).filter(el => el.name === name)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "0.20.9",
3
+ "version": "0.20.11",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "repository": {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "0.20.9",
3
+ "version": "0.20.11",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",
@@ -1124,10 +1124,9 @@ var dom_default = DOM;
1124
1124
  var UploadEntry = class {
1125
1125
  static isActive(fileEl, file) {
1126
1126
  let isNew = file._phxRef === void 0;
1127
- let isPreflightInProgress = UploadEntry.isPreflightInProgress(file);
1128
1127
  let activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(",");
1129
1128
  let isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;
1130
- return file.size > 0 && (isNew || isActive || !isPreflightInProgress);
1129
+ return file.size > 0 && (isNew || isActive);
1131
1130
  }
1132
1131
  static isPreflighted(fileEl, file) {
1133
1132
  let preflightedRefs = fileEl.getAttribute(PHX_PREFLIGHTED_REFS).split(",");
@@ -1140,7 +1139,7 @@ var UploadEntry = class {
1140
1139
  static markPreflightInProgress(file) {
1141
1140
  file._preflightInProgress = true;
1142
1141
  }
1143
- constructor(fileEl, file, view) {
1142
+ constructor(fileEl, file, view, autoUpload) {
1144
1143
  this.ref = LiveUploader.genFileRef(file);
1145
1144
  this.fileEl = fileEl;
1146
1145
  this.file = file;
@@ -1154,6 +1153,7 @@ var UploadEntry = class {
1154
1153
  };
1155
1154
  this._onElUpdated = this.onElUpdated.bind(this);
1156
1155
  this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);
1156
+ this.autoUpload = autoUpload;
1157
1157
  }
1158
1158
  metadata() {
1159
1159
  return this.meta;
@@ -1175,7 +1175,11 @@ var UploadEntry = class {
1175
1175
  }
1176
1176
  }
1177
1177
  }
1178
+ isCancelled() {
1179
+ return this._isCancelled;
1180
+ }
1178
1181
  cancel() {
1182
+ this.file._preflightInProgress = false;
1179
1183
  this._isCancelled = true;
1180
1184
  this._isDone = true;
1181
1185
  this._onDone();
@@ -1186,10 +1190,13 @@ var UploadEntry = class {
1186
1190
  error(reason = "failed") {
1187
1191
  this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);
1188
1192
  this.view.pushFileProgress(this.fileEl, this.ref, { error: reason });
1189
- if (!dom_default.isAutoUpload(this.fileEl)) {
1193
+ if (!this.isAutoUpload()) {
1190
1194
  LiveUploader.clearFiles(this.fileEl);
1191
1195
  }
1192
1196
  }
1197
+ isAutoUpload() {
1198
+ return this.autoUpload;
1199
+ }
1193
1200
  onDone(callback) {
1194
1201
  this._onDone = () => {
1195
1202
  this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);
@@ -1199,6 +1206,7 @@ var UploadEntry = class {
1199
1206
  onElUpdated() {
1200
1207
  let activeRefs = this.fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(",");
1201
1208
  if (activeRefs.indexOf(this.ref) === -1) {
1209
+ LiveUploader.untrackFile(this.fileEl, this.file);
1202
1210
  this.cancel();
1203
1211
  }
1204
1212
  }
@@ -1285,7 +1293,7 @@ var LiveUploader = class {
1285
1293
  static trackFiles(inputEl, files, dataTransfer) {
1286
1294
  if (inputEl.getAttribute("multiple") !== null) {
1287
1295
  let newFiles = files.filter((file) => !this.activeFiles(inputEl).find((f) => Object.is(f, file)));
1288
- dom_default.putPrivate(inputEl, "files", this.activeFiles(inputEl).concat(newFiles));
1296
+ dom_default.updatePrivate(inputEl, "files", [], (existing) => existing.concat(newFiles));
1289
1297
  inputEl.value = null;
1290
1298
  } else {
1291
1299
  if (dataTransfer && dataTransfer.files.length > 0) {
@@ -1312,24 +1320,35 @@ var LiveUploader = class {
1312
1320
  entries.forEach((entry) => UploadEntry.markPreflightInProgress(entry.file));
1313
1321
  }
1314
1322
  constructor(inputEl, view, onComplete) {
1323
+ this.autoUpload = dom_default.isAutoUpload(inputEl);
1315
1324
  this.view = view;
1316
1325
  this.onComplete = onComplete;
1317
- this._entries = Array.from(LiveUploader.filesAwaitingPreflight(inputEl) || []).map((file) => new UploadEntry(inputEl, file, view));
1326
+ this._entries = Array.from(LiveUploader.filesAwaitingPreflight(inputEl) || []).map((file) => new UploadEntry(inputEl, file, view, this.autoUpload));
1318
1327
  LiveUploader.markPreflightInProgress(this._entries);
1319
1328
  this.numEntriesInProgress = this._entries.length;
1320
1329
  }
1330
+ isAutoUpload() {
1331
+ return this.autoUpload;
1332
+ }
1321
1333
  entries() {
1322
1334
  return this._entries;
1323
1335
  }
1324
1336
  initAdapterUpload(resp, onError, liveSocket) {
1325
1337
  this._entries = this._entries.map((entry) => {
1326
- entry.zipPostFlight(resp);
1327
- entry.onDone(() => {
1338
+ if (entry.isCancelled()) {
1328
1339
  this.numEntriesInProgress--;
1329
1340
  if (this.numEntriesInProgress === 0) {
1330
1341
  this.onComplete();
1331
1342
  }
1332
- });
1343
+ } else {
1344
+ entry.zipPostFlight(resp);
1345
+ entry.onDone(() => {
1346
+ this.numEntriesInProgress--;
1347
+ if (this.numEntriesInProgress === 0) {
1348
+ this.onComplete();
1349
+ }
1350
+ });
1351
+ }
1333
1352
  return entry;
1334
1353
  });
1335
1354
  let groupedEntries = this._entries.reduce((acc, entry) => {
@@ -2920,6 +2939,7 @@ var View = class {
2920
2939
  this.childJoins = 0;
2921
2940
  this.loaderTimer = null;
2922
2941
  this.pendingDiffs = [];
2942
+ this.pendingForms = new Set();
2923
2943
  this.redirect = false;
2924
2944
  this.href = null;
2925
2945
  this.joinCount = this.parent ? this.parent.joinCount - 1 : 0;
@@ -3072,11 +3092,15 @@ var View = class {
3072
3092
  this.rendered = new Rendered(this.id, diff);
3073
3093
  let [html, streams] = this.renderContainer(null, "join");
3074
3094
  this.dropPendingRefs();
3075
- let forms = this.formsForRecovery(html);
3095
+ let forms = this.formsForRecovery(html).filter(([form, newForm, newCid]) => {
3096
+ return !this.pendingForms.has(form.id);
3097
+ });
3076
3098
  this.joinCount++;
3077
3099
  if (forms.length > 0) {
3078
3100
  forms.forEach(([form, newForm, newCid], i) => {
3101
+ this.pendingForms.add(form.id);
3079
3102
  this.pushFormRecovery(form, newCid, (resp2) => {
3103
+ this.pendingForms.delete(form.id);
3080
3104
  if (i === forms.length - 1) {
3081
3105
  this.onJoinComplete(resp2, html, streams, events);
3082
3106
  }
@@ -3094,6 +3118,7 @@ var View = class {
3094
3118
  });
3095
3119
  }
3096
3120
  onJoinComplete({ live_patch }, html, streams, events) {
3121
+ this.pendingForms.clear();
3097
3122
  if (this.joinCount > 1 || this.parent && !this.parent.isJoinPending()) {
3098
3123
  return this.applyJoinPatch(live_patch, html, streams, events);
3099
3124
  }
@@ -3813,7 +3838,10 @@ var View = class {
3813
3838
  } else if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {
3814
3839
  let [ref, els] = refGenerator();
3815
3840
  let proxyRefGen = () => [ref, els, opts];
3816
- this.uploadFiles(formEl, targetCtx, ref, cid, (_uploads) => {
3841
+ this.uploadFiles(formEl, targetCtx, ref, cid, (uploads) => {
3842
+ if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {
3843
+ return this.undoRefs(ref);
3844
+ }
3817
3845
  let meta = this.extractMeta(formEl);
3818
3846
  let formData = serializeForm(formEl, { submitter, ...meta });
3819
3847
  this.pushWithReply(proxyRefGen, "event", {
@@ -3858,10 +3886,17 @@ var View = class {
3858
3886
  this.log("upload", () => ["sending preflight request", payload]);
3859
3887
  this.pushWithReply(null, "allow_upload", payload, (resp) => {
3860
3888
  this.log("upload", () => ["got preflight response", resp]);
3861
- if (resp.error) {
3889
+ uploader.entries().forEach((entry) => {
3890
+ if (resp.entries && !resp.entries[entry.ref]) {
3891
+ this.handleFailedEntryPreflight(entry.ref, "failed preflight", uploader);
3892
+ }
3893
+ });
3894
+ if (resp.error || Object.keys(resp.entries).length === 0) {
3862
3895
  this.undoRefs(ref);
3863
- let [entry_ref, reason] = resp.error;
3864
- this.log("upload", () => [`error for entry ${entry_ref}`, reason]);
3896
+ let errors = resp.error || [];
3897
+ errors.map(([entry_ref, reason]) => {
3898
+ this.handleFailedEntryPreflight(entry_ref, reason, uploader);
3899
+ });
3865
3900
  } else {
3866
3901
  let onError = (callback) => {
3867
3902
  this.channel.onError(() => {
@@ -3875,6 +3910,17 @@ var View = class {
3875
3910
  });
3876
3911
  });
3877
3912
  }
3913
+ handleFailedEntryPreflight(uploadRef, reason, uploader) {
3914
+ if (uploader.isAutoUpload()) {
3915
+ let entry = uploader.entries().find((entry2) => entry2.ref === uploadRef.toString());
3916
+ if (entry) {
3917
+ entry.cancel();
3918
+ }
3919
+ } else {
3920
+ uploader.entries().map((entry) => entry.cancel());
3921
+ }
3922
+ this.log("upload", () => [`error for entry ${uploadRef}`, reason]);
3923
+ }
3878
3924
  dispatchUploads(targetCtx, name, filesOrBlobs) {
3879
3925
  let targetElement = this.targetCtxElement(targetCtx) || this.el;
3880
3926
  let inputs = dom_default.findUploadInputs(targetElement).filter((el) => el.name === name);