| | @@ -25,34 +25,42 @@ |
| 25 | 25 | #tabs; |
| 26 | 26 | /* Elements to disable while an XHR is pending. */ |
| 27 | 27 | #toDisable = []; |
| 28 | 28 | /* DOM element of the current active tab. */ |
| 29 | 29 | #activeTab; |
| 30 | + /* Extra input[type=hidden] fields imported from fossil's |
| 31 | + static page generation. */ |
| 32 | + #extraFields; |
| 30 | 33 | |
| 34 | + /** |
| 35 | + */ |
| 31 | 36 | constructor(opt){ |
| 32 | 37 | opt = this.#opt = F.nu({ |
| 33 | 38 | // todo: defaults once we determine the options |
| 34 | 39 | // replyTo: hash |
| 35 | 40 | // edit: hash |
| 36 | 41 | }, opt); |
| 42 | + opt.isNewThread = !opt.replyTo && !opt.edit; |
| 37 | 43 | const e = this.#e = F.nu({ |
| 38 | 44 | mimetype: F.nu(), |
| 39 | 45 | button: F.nu() |
| 40 | 46 | }); |
| 41 | 47 | const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor'); |
| 42 | 48 | D.clearElement(wrapper); |
| 43 | 49 | if( !opt.inReplyTo ){ |
| 44 | 50 | e.titleBar = D.addClass(D.div(),'titlebar'); |
| 45 | 51 | e.title = D.addClass(D.input('text'), 'title'); |
| 52 | + e.title.setAttribute('maxlength', 125); |
| 46 | 53 | e.titleBar.append( |
| 47 | 54 | D.append(D.span(), "Title:"), |
| 48 | 55 | e.title |
| 49 | 56 | ); |
| 50 | 57 | wrapper.append(e.titleBar); |
| 51 | 58 | } |
| 52 | 59 | e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper'); |
| 53 | 60 | e.mimetype.select = D.addClass(D.select(), 'mimetype-select'); |
| 61 | + this.#toDisable.push(e.mimetype.select); |
| 54 | 62 | e.mimetype.label = D.span(); |
| 55 | 63 | e.mimetype.label.append( |
| 56 | 64 | D.a(F.repoUrl('markup_help'), 'Markup style'), |
| 57 | 65 | ':' |
| 58 | 66 | ); |
| | @@ -67,14 +75,18 @@ |
| 67 | 75 | if( !i++ ) o.setAttribute('selected', ''); |
| 68 | 76 | } |
| 69 | 77 | |
| 70 | 78 | e.button.preview = D.button("Preview", e=>this.#preview()); |
| 71 | 79 | e.button.submit = D.button("Submit"); |
| 72 | | - F.confirmer(e.button.submit, { |
| 73 | | - confirmText: "Confirm submit...", |
| 74 | | - onconfirm: ()=>this.#submit() |
| 75 | | - }); |
| 80 | + if( 1 ){ |
| 81 | + F.confirmer(e.button.submit, { |
| 82 | + confirmText: "Confirm submit...", |
| 83 | + onconfirm: ()=>this.#submit() |
| 84 | + }); |
| 85 | + }else{ |
| 86 | + e.button.submit.addEventListener('click', ()=>this.#submit()); |
| 87 | + } |
| 76 | 88 | e.button.submit.setAttribute('disabled', ''); |
| 77 | 89 | e.buttons = D.addClass(D.div(), 'buttons'); |
| 78 | 90 | wrapper.append(e.buttons); |
| 79 | 91 | |
| 80 | 92 | e.err = D.addClass(D.div(), 'error', 'hidden'); |
| | @@ -86,32 +98,33 @@ |
| 86 | 98 | e.tabs = D.attr( |
| 87 | 99 | D.addClass(D.div(), 'tab-container'), |
| 88 | 100 | 'id', idPrefix+'-tabs' |
| 89 | 101 | ); |
| 90 | 102 | this.#tabs = new F.TabManager(e.tabs); |
| 103 | + this.#tabs.addEventListener('before-switch-to', (ev)=>{ |
| 104 | + this.#activeTab = ev.detail; |
| 105 | + if( e.preview === this.#activeTab ){ |
| 106 | + this.#e.button.preview.click(); |
| 107 | + } |
| 108 | + }); |
| 91 | 109 | wrapper.append( e.tabs ); |
| 92 | 110 | |
| 93 | 111 | e.tabEdit = D.div(); |
| 94 | 112 | e.tabEdit.classList.add('editor-wrapper'); |
| 95 | 113 | e.editor = D.attr( |
| 96 | 114 | D.addClass(D.textarea(), 'editor'), |
| 97 | 115 | 'placeholder', |
| 98 | | - 'Your content...' |
| 116 | + 'Your message to other forum-goers...' |
| 99 | 117 | ); |
| 100 | 118 | e.tabEdit.append(e.editor); |
| 101 | 119 | e.tabEdit.dataset.tabLabel = 'Edit'; |
| 102 | 120 | this.#tabs.addTab( e.tabEdit ); |
| 121 | + this.#tabs.switchToTab( e.tabEdit ); |
| 103 | 122 | |
| 104 | 123 | e.preview = D.addClass(D.div(), 'preview'); |
| 105 | 124 | e.preview.dataset.tabLabel = 'Preview'; |
| 106 | 125 | this.#tabs.addTab( e.preview ); |
| 107 | | - this.#tabs.addEventListener('before-switch-to', (ev)=>{ |
| 108 | | - this.#activeTab = ev.detail; |
| 109 | | - if( e.preview === this.#activeTab ){ |
| 110 | | - this.#e.button.preview.click(); |
| 111 | | - } |
| 112 | | - }); |
| 113 | 126 | } |
| 114 | 127 | |
| 115 | 128 | if( F.user.enableDebug ){ |
| 116 | 129 | e.debug = D.addClass(D.div(), 'debug'); |
| 117 | 130 | e.debug.dataset.tabLabel = 'Debug'; |
| | @@ -147,11 +160,10 @@ |
| 147 | 160 | |
| 148 | 161 | get widget(){ |
| 149 | 162 | return this.#e.widget; |
| 150 | 163 | } |
| 151 | 164 | |
| 152 | | - |
| 153 | 165 | /** |
| 154 | 166 | Reports an error by appending each argument to the error widget |
| 155 | 167 | and unhiding it. If passed no arugments, it clears and hides |
| 156 | 168 | the error widget. |
| 157 | 169 | */ |
| | @@ -164,14 +176,48 @@ |
| 164 | 176 | }else{ |
| 165 | 177 | e.classList.add('hidden'); |
| 166 | 178 | } |
| 167 | 179 | } |
| 168 | 180 | |
| 169 | | - async #fetchPreview(){ |
| 181 | + /** |
| 182 | + Adds a list of input[type=hidden] form fields to this object, |
| 183 | + imported from the server-generated HTML. This is used for collecting, |
| 184 | + e.g., the CSRF token. |
| 185 | + */ |
| 186 | + addHiddenFields(list){ |
| 187 | + this.#extraFields ??= []; |
| 188 | + for( const f of list ){ |
| 189 | + if( 'title'===f.name && this.#opt.isNewThread ){ |
| 190 | + this.#e.title.value = f.value; |
| 191 | + }else{ |
| 192 | + this.#extraFields.push(f); |
| 193 | + } |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + async #fetchPreview(content){ |
| 170 | 198 | /* TODO: fetch preview */ |
| 171 | | - this.#isWaiting = false; |
| 172 | | - D.enable(this.#toDisable); |
| 199 | + const e = this.#e; |
| 200 | + const fd = new FormData; |
| 201 | + for(const f of this.#extraFields){ |
| 202 | + fd.append(f.name, f.value); |
| 203 | + } |
| 204 | + fd.append('mimetype', e.mimetype.select.value); |
| 205 | + fd.append('content', content); |
| 206 | + return window |
| 207 | + .fetch(F.repoUrl('wikiajax/preview'), { |
| 208 | + method: 'POST', |
| 209 | + body: fd |
| 210 | + }) |
| 211 | + .then(r=>r.text()) |
| 212 | + .then(t=>{ |
| 213 | + if( /^\{.*}$/.test(t) ){ |
| 214 | + const o = JSON.parse(t); |
| 215 | + throw new Error(o.error); |
| 216 | + } |
| 217 | + return t; |
| 218 | + }); |
| 173 | 219 | } |
| 174 | 220 | |
| 175 | 221 | async #preview(){ |
| 176 | 222 | if( this.#isWaiting ) return; |
| 177 | 223 | const e = this.#e; |
| | @@ -179,28 +225,49 @@ |
| 179 | 225 | this.#tabs.switchToTab(e.preview); |
| 180 | 226 | /* Will recurse into here */ |
| 181 | 227 | return; |
| 182 | 228 | } |
| 183 | 229 | this.#isWaiting = true; |
| 230 | + D.clearElement(e.preview); |
| 231 | + const content = e.editor.value.trim(); |
| 232 | + if( !content ){ |
| 233 | + return; |
| 234 | + } |
| 184 | 235 | D.disable(this.#toDisable, e.button.submit); |
| 185 | 236 | e.preview.textContent = "Fetching preview..."; |
| 186 | | - this.#fetchPreview() |
| 187 | | - .then(()=>{ |
| 188 | | - e.preview.textContent = "TODO: actually fetch the preview "+Date.now(); |
| 237 | + this.#fetchPreview(content) |
| 238 | + .then((c)=>{ |
| 239 | + e.preview.innerHTML = c; |
| 189 | 240 | D.enable(this.#toDisable, e.button.submit); |
| 190 | 241 | }) |
| 191 | | - .catch(e=>{ |
| 192 | | - this.reportError(e.message); |
| 242 | + .catch(err=>{ |
| 243 | + e.preview.textContent = "Error fetching preview: "+err.message; |
| 244 | + this.reportError(err.message); |
| 245 | + }) |
| 246 | + .finally(()=>{ |
| 247 | + this.#isWaiting = false; |
| 193 | 248 | D.enable(this.#toDisable); |
| 249 | + console.warn("finally()!"); |
| 194 | 250 | }); |
| 195 | 251 | } |
| 252 | + |
| 253 | + #validate(){ |
| 254 | + let v = this.#e.title.value.trim(); |
| 255 | + if( !v ){ |
| 256 | + this.reportError("A non-empty title is required."); |
| 257 | + return; |
| 258 | + } |
| 259 | + return true; |
| 260 | + } |
| 196 | 261 | |
| 197 | 262 | #submit(){ |
| 198 | 263 | if( this.#isWaiting ) return; |
| 264 | + if( !this.#validate() ) return; |
| 199 | 265 | this.#isWaiting = true; |
| 200 | 266 | const e = this.#e; |
| 201 | 267 | D.disable(e.button.submit); |
| 268 | + this.reportError("Submit is TODO."); |
| 202 | 269 | /* |
| 203 | 270 | TODO: save it, set #isWaiting=false, then handle error or |
| 204 | 271 | redirect to the post (if this is a new post) or, if replying |
| 205 | 272 | inline, replace this object with a static rendering from the |
| 206 | 273 | response. |
| | @@ -391,7 +458,18 @@ |
| 391 | 458 | }); |
| 392 | 459 | }); |
| 393 | 460 | }); |
| 394 | 461 | } |
| 395 | 462 | |
| 463 | + const eForumNew = document.body.classList.contains('cpage-forumnew') |
| 464 | + ? document.querySelector('#forumnew-placeholder') |
| 465 | + : null; |
| 466 | + if( eForumNew ){ |
| 467 | + /* /forumnew */ |
| 468 | + const fpe = new fossil.ForumPostEditor({}); |
| 469 | + fpe.addHiddenFields( eForumNew.querySelectorAll('input[type=hidden]') ); |
| 470 | + eForumNew.parentElement.insertBefore(fpe.widget, eForumNew); |
| 471 | + eForumNew.remove(); |
| 472 | + fossil.page.fpe = fpe /* for testing via the console */; |
| 473 | + }/*eForumNew*/ |
| 396 | 474 | })/*F.onPageLoad callback*/; |
| 397 | 475 | })(window.fossil); |
| 398 | 476 | |