| | @@ -19,10 +19,11 @@ |
| 19 | 19 | */ |
| 20 | 20 | class Attacher { |
| 21 | 21 | #opt; |
| 22 | 22 | #rows = []; |
| 23 | 23 | #e = Object.create(null); |
| 24 | + #events = new EventTarget(); |
| 24 | 25 | |
| 25 | 26 | /** |
| 26 | 27 | Options: |
| 27 | 28 | |
| 28 | 29 | opt.container: DOM element to wrap this object in. |
| | @@ -34,13 +35,32 @@ |
| 34 | 35 | defaults to "some sensible value". |
| 35 | 36 | |
| 36 | 37 | opt.startWith[=0]: if >0 then that many file selection widgets |
| 37 | 38 | are automatically activated, as if the user had tapped the Add |
| 38 | 39 | button that many times. |
| 40 | + |
| 41 | + opt.listener = {add: func, remove: func, populate: func}: if |
| 42 | + these are functions they are registered as listeners for |
| 43 | + 'entry-added', 'entry-removed', and/or 'entry-populated' |
| 44 | + events, described below. |
| 45 | + |
| 46 | + Events: |
| 47 | + |
| 48 | + This class fires CustomEvents for certain changes: |
| 49 | + |
| 50 | + 'entry-added' and 'entry-removed' trigger when an attachment |
| 51 | + entry row is added/removed. Its event.detail is {attacher: |
| 52 | + this, row: object}. |
| 53 | + |
| 54 | + 'entry-populated' is triggered when a visible entry gets |
| 55 | + content attached to it. |
| 56 | + |
| 57 | + The public structure of the row object passed to each is |
| 58 | + currently TBD. |
| 39 | 59 | */ |
| 40 | 60 | constructor(opt){ |
| 41 | | - this.#opt = opt = Object.assign(Object.create(null),{ |
| 61 | + this.#opt = opt = F.nu({ |
| 42 | 62 | addButtonLabel: false, |
| 43 | 63 | startWith: 0, |
| 44 | 64 | limit: 7 |
| 45 | 65 | }, opt); |
| 46 | 66 | const eBtnAdd = this.#e.btnAdd = D.addClass( |
| | @@ -47,45 +67,87 @@ |
| 47 | 67 | D.button(this.#opt.addButtonLabel || 'Add attachment', |
| 48 | 68 | ()=>this.#addRow()), |
| 49 | 69 | 'attach-add-button' |
| 50 | 70 | ); |
| 51 | 71 | eBtnAdd.type = 'button'; |
| 72 | + const eControls = this.#e.controls = |
| 73 | + D.addClass(D.div(), 'attach-controls'); |
| 74 | + eControls.append(eBtnAdd); |
| 52 | 75 | this.#e.list = D.addClass(D.div(), 'attach-container'); |
| 53 | 76 | opt.container.appendChild(this.#e.list); |
| 54 | | - this.#e.list.appendChild(eBtnAdd); |
| 77 | + this.#e.list.appendChild(eControls); |
| 78 | + if( opt.listener ){ |
| 79 | + if( opt.listener.add instanceof Function ){ |
| 80 | + this.addEventListener('entry-added', opt.listener.add); |
| 81 | + } |
| 82 | + if( opt.listener.remove instanceof Function ){ |
| 83 | + this.addEventListener('entry-removed', opt.listener.remove); |
| 84 | + } |
| 85 | + if( opt.listener.populate instanceof Function ){ |
| 86 | + this.addEventListener('entry-populated', opt.listener.populate); |
| 87 | + } |
| 88 | + } |
| 55 | 89 | if( opt.startWith > 0 ){ |
| 56 | 90 | for(let i = 0; i < opt.startWith; ++i ){ |
| 57 | 91 | this.#addRow(); |
| 58 | 92 | } |
| 93 | + }else{ |
| 94 | + this.#updateControls(); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + addEventListener(...args){ |
| 99 | + return this.#events.addEventListener(...args); |
| 100 | + } |
| 101 | + |
| 102 | + removeEventListener(...args){ |
| 103 | + return this.#events.removeEventListener(...args); |
| 104 | + } |
| 105 | + |
| 106 | + get isPopulated(){ |
| 107 | + for(let r of this.#rows){ |
| 108 | + if( r.file ) return true; |
| 59 | 109 | } |
| 110 | + return false; |
| 111 | + } |
| 112 | + |
| 113 | + get controlsElement(){ |
| 114 | + return this.#e.controls; |
| 60 | 115 | } |
| 61 | 116 | |
| 62 | 117 | #removeRow(rowObj){ |
| 63 | 118 | rowObj.eRow.remove(); |
| 64 | 119 | this.#rows = this.#rows.filter(v=>v!==rowObj); |
| 65 | | - this.#updateBtnAdd(); |
| 120 | + this.#updateControls(); |
| 121 | + this.#events.dispatchEvent( |
| 122 | + new CustomEvent('entry-removed',{ |
| 123 | + detail: F.nu({ |
| 124 | + row: rowObj, |
| 125 | + attacher: this |
| 126 | + }) |
| 127 | + }) |
| 128 | + ); |
| 66 | 129 | if( 0===this.#rows.length |
| 67 | | - && 1===this.#opt.limit |
| 68 | 130 | && 1===this.#opt.startWith ){ |
| 69 | 131 | /* Intended primarily for /addattach. */ |
| 70 | 132 | this.#addRow(); |
| 71 | 133 | } |
| 72 | 134 | } |
| 73 | 135 | |
| 74 | 136 | /** |
| 75 | 137 | Hide or show the Add button, as appropriate. |
| 76 | 138 | */ |
| 77 | | - #updateBtnAdd(){ |
| 139 | + #updateControls(){ |
| 78 | 140 | const b = this.#e.btnAdd; |
| 79 | 141 | if( this.#opt.limit>0 && this.#rows.length >= this.#opt.limit ){ |
| 80 | 142 | b.classList.add('hidden'); |
| 81 | 143 | //b.setAttribute('disabled',''); |
| 82 | 144 | //F.toast.warning("Attachment form limit reached."); |
| 83 | 145 | }else{ |
| 84 | 146 | b.classList.remove('hidden'); |
| 85 | 147 | //b.removeAttribute('disabled'); |
| 86 | | - this.#e.list.append(b/*move to the end*/); |
| 148 | + this.#e.list.append(this.#e.controls/*move to the end*/); |
| 87 | 149 | } |
| 88 | 150 | } |
| 89 | 151 | |
| 90 | 152 | #addRow(){ |
| 91 | 153 | const id = ++idCounter; |
| | @@ -166,11 +228,19 @@ |
| 166 | 228 | rowObj.eInfo = eInfo; |
| 167 | 229 | rowObj.eDesc = eDesc; |
| 168 | 230 | rowObj.eRow = eRow; |
| 169 | 231 | this.#e.list.append(eRow); |
| 170 | 232 | this.#rows.push( rowObj ); |
| 171 | | - this.#updateBtnAdd(); |
| 233 | + this.#updateControls(); |
| 234 | + this.#events.dispatchEvent( |
| 235 | + new CustomEvent('entry-added',{ |
| 236 | + detail: F.nu({ |
| 237 | + row: rowObj, |
| 238 | + attacher: this |
| 239 | + }) |
| 240 | + }) |
| 241 | + ); |
| 172 | 242 | if( 0 ){ |
| 173 | 243 | /* To allow immediate ctrl-v, we need a trick... |
| 174 | 244 | But don't do this because it will interfere with, e.g., |
| 175 | 245 | the forum editor. */ |
| 176 | 246 | D.attr(eRow, 'tabindex', '-1'); |
| | @@ -217,13 +287,24 @@ |
| 217 | 287 | }else if( file.size < 1000000 ){ |
| 218 | 288 | szLbl = (file.size / 1024).toFixed(2)+' KB'; |
| 219 | 289 | }else{ |
| 220 | 290 | szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB'; |
| 221 | 291 | } |
| 222 | | - rowObj.eInfo.textContent = `${lbl} (${szLbl}, ${rowObj.mimeType})`; |
| 292 | + D.append( |
| 293 | + D.clearElement(rowObj.eInfo), |
| 294 | + lbl, D.br(), szLbl, ' ', rowObj.mimeType || '' |
| 295 | + ); |
| 223 | 296 | rowObj.eDropzone.classList.add('populated'); |
| 224 | 297 | rowObj.eDesc.classList.remove('hidden'); |
| 298 | + this.#events.dispatchEvent( |
| 299 | + new CustomEvent('entry-populated',{ |
| 300 | + detail: F.nu({ |
| 301 | + row: rowObj, |
| 302 | + attacher: this |
| 303 | + }) |
| 304 | + }) |
| 305 | + ); |
| 225 | 306 | } |
| 226 | 307 | |
| 227 | 308 | /** |
| 228 | 309 | Returns an array of objects describing the currently-selected |
| 229 | 310 | attachments. |
| | @@ -232,11 +313,11 @@ |
| 232 | 313 | const rv = []; |
| 233 | 314 | for(let r of this.#rows){ |
| 234 | 315 | if( !r.eDropzone?.classList?.contains?.('populated') ){ |
| 235 | 316 | continue; |
| 236 | 317 | } |
| 237 | | - rv.push(Object.assign(Object.create(null),{ |
| 318 | + rv.push(F.nu({ |
| 238 | 319 | name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`, |
| 239 | 320 | content: r.file, |
| 240 | 321 | description: r.eDesc?.value || '', |
| 241 | 322 | mimeType: r.mimeType |
| 242 | 323 | })); |
| | @@ -266,9 +347,42 @@ |
| 266 | 347 | } |
| 267 | 348 | } |
| 268 | 349 | return i; |
| 269 | 350 | } |
| 270 | 351 | }/*Attacher*/; |
| 271 | | - |
| 272 | 352 | F.Attacher = Attacher; |
| 353 | + |
| 354 | + if( document.body.classList.contains('cpage-attachaddV2') ){ |
| 355 | + const eFormDiv = document.querySelector('#attachadd-form-wrapper'); |
| 356 | + const eBtnSubmit = D.button("Submit"); |
| 357 | + eBtnSubmit.type = 'button'; |
| 358 | + const updateBtnSubmit = (attacher)=>{ |
| 359 | + if( attacher.isPopulated ){ |
| 360 | + eBtnSubmit.removeAttribute('disabled'); |
| 361 | + }else{ |
| 362 | + eBtnSubmit.setAttribute('disabled', ''); |
| 363 | + } |
| 364 | + }; |
| 365 | + const cbAdd = (ev)=>{ |
| 366 | + const a = ev.detail.attacher; |
| 367 | + updateBtnSubmit(a); |
| 368 | + }; |
| 369 | + const cbRm = (ev)=>{ |
| 370 | + const a = ev.detail.attacher; |
| 371 | + updateBtnSubmit(a); |
| 372 | + }; |
| 373 | + const cbPopulated = (ev)=>{ |
| 374 | + const a = ev.detail.attacher; |
| 375 | + updateBtnSubmit(a); |
| 376 | + }; |
| 377 | + const cbSubmit = (ev)=>{ |
| 378 | + }; |
| 379 | + eBtnSubmit.addEventListener('click', cbSubmit, false); |
| 380 | + const att = new Attacher({ |
| 381 | + container: eFormDiv, |
| 382 | + startWith: 1, |
| 383 | + listener: {add: cbAdd, remove: cbRm, populate: cbPopulated} |
| 384 | + }); |
| 385 | + att.controlsElement.append(eBtnSubmit); |
| 386 | + }/* /attachaddV2 */ |
| 273 | 387 | |
| 274 | 388 | })(window.fossil); |
| 275 | 389 | |