Fossil SCM
Have /forumnew stash edits in local/sessionStorage and restore its state if it's reloaded. It clears the stash on a successful submit (which is still TBD). Process pikchrs in the preview.
Commit
adcbe4496ba86172d5139ac4fc051bea932f3b09748b24d69c2b43256e79a6a7
Parent
3addcc0abdf6b8e…
2 files changed
+1
-1
+81
-10
+1
-1
| --- src/forum.c | ||
| +++ src/forum.c | ||
| @@ -1483,11 +1483,11 @@ | ||
| 1483 | 1483 | ** to all forum-related pages. It does not include page-specific |
| 1484 | 1484 | ** code (e.g. "forum.js"). |
| 1485 | 1485 | */ |
| 1486 | 1486 | static void forum_emit_js(void){ |
| 1487 | 1487 | builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer", |
| 1488 | - "attach", "tabs", NULL); | |
| 1488 | + "attach", "tabs", "storage", NULL); | |
| 1489 | 1489 | builtin_request_js("fossil.page.forumpost.js"); |
| 1490 | 1490 | } |
| 1491 | 1491 | |
| 1492 | 1492 | /* |
| 1493 | 1493 | ** WEBPAGE: forumpost |
| 1494 | 1494 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -1483,11 +1483,11 @@ | |
| 1483 | ** to all forum-related pages. It does not include page-specific |
| 1484 | ** code (e.g. "forum.js"). |
| 1485 | */ |
| 1486 | static void forum_emit_js(void){ |
| 1487 | builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer", |
| 1488 | "attach", "tabs", NULL); |
| 1489 | builtin_request_js("fossil.page.forumpost.js"); |
| 1490 | } |
| 1491 | |
| 1492 | /* |
| 1493 | ** WEBPAGE: forumpost |
| 1494 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -1483,11 +1483,11 @@ | |
| 1483 | ** to all forum-related pages. It does not include page-specific |
| 1484 | ** code (e.g. "forum.js"). |
| 1485 | */ |
| 1486 | static void forum_emit_js(void){ |
| 1487 | builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer", |
| 1488 | "attach", "tabs", "storage", NULL); |
| 1489 | builtin_request_js("fossil.page.forumpost.js"); |
| 1490 | } |
| 1491 | |
| 1492 | /* |
| 1493 | ** WEBPAGE: forumpost |
| 1494 |
+81
-10
| --- src/fossil.page.forumpost.js | ||
| +++ src/fossil.page.forumpost.js | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | Code for the forum family of pages. Requires fossil.X where X is |
| 3 | - (copybutton, pikchr, confirmer, attach, tabs). | |
| 3 | + (copybutton, pikchr, confirmer, attach, tabs, storage). | |
| 4 | 4 | */ |
| 5 | 5 | (function(F/*the fossil object*/){ |
| 6 | 6 | "use strict"; |
| 7 | 7 | /* JS code for /forumpost and friends. Requires fossil.dom |
| 8 | 8 | and can optionally use fossil.pikchr. */ |
| @@ -30,18 +30,26 @@ | ||
| 30 | 30 | /* Extra input[type=hidden] fields imported from fossil's |
| 31 | 31 | static page generation. */ |
| 32 | 32 | #extraFields; |
| 33 | 33 | |
| 34 | 34 | /** |
| 35 | - */ | |
| 35 | + Options: | |
| 36 | + | |
| 37 | + opt.draftKey[string=undefined]: if set then this object's state | |
| 38 | + will be stored in fossil.storage when the relevant input fields | |
| 39 | + lose focus. If old state is found, the form is pre-populated | |
| 40 | + from it. The state is cleared on a successful submit. | |
| 41 | + */ | |
| 36 | 42 | constructor(opt){ |
| 37 | 43 | opt = this.#opt = F.nu({ |
| 38 | 44 | // todo: defaults once we determine the options |
| 39 | 45 | // replyTo: hash |
| 40 | 46 | // edit: hash |
| 47 | + draftKey: undefined | |
| 41 | 48 | }, opt); |
| 42 | 49 | opt.isNewThread = !opt.replyTo && !opt.edit; |
| 50 | + if( !opt.draftKey) opt.draftKey = ''; | |
| 43 | 51 | const e = this.#e = F.nu({ |
| 44 | 52 | mimetype: F.nu(), |
| 45 | 53 | button: F.nu() |
| 46 | 54 | }); |
| 47 | 55 | const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor'); |
| @@ -52,10 +60,17 @@ | ||
| 52 | 60 | e.title.setAttribute('maxlength', 125); |
| 53 | 61 | e.titleBar.append( |
| 54 | 62 | D.append(D.span(), "Title:"), |
| 55 | 63 | e.title |
| 56 | 64 | ); |
| 65 | + if( opt.draftKey ){ | |
| 66 | + const key = opt.draftKey+'.title'; | |
| 67 | + e.title.addEventListener('blur', ()=>{ | |
| 68 | + F.storage.set(key, e.title.value) | |
| 69 | + }); | |
| 70 | + e.title.value = F.storage.get(key,''); | |
| 71 | + } | |
| 57 | 72 | wrapper.append(e.titleBar); |
| 58 | 73 | } |
| 59 | 74 | e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper'); |
| 60 | 75 | e.mimetype.select = D.addClass(D.select(), 'mimetype-select'); |
| 61 | 76 | this.#toDisable.push(e.mimetype.select); |
| @@ -92,11 +107,11 @@ | ||
| 92 | 107 | e.err = D.addClass(D.div(), 'error', 'hidden'); |
| 93 | 108 | wrapper.append(e.err); |
| 94 | 109 | e.err.addEventListener('dblclick',()=>this.reportError()); |
| 95 | 110 | |
| 96 | 111 | const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */; |
| 97 | - { /* Tabs... */ | |
| 112 | + { /* Main tabs... */ | |
| 98 | 113 | e.tabs = D.attr( |
| 99 | 114 | D.addClass(D.div(), 'tab-container'), |
| 100 | 115 | 'id', idPrefix+'-tabs' |
| 101 | 116 | ); |
| 102 | 117 | this.#tabs = new F.TabManager(e.tabs); |
| @@ -117,11 +132,17 @@ | ||
| 117 | 132 | ); |
| 118 | 133 | e.tabEdit.append(e.editor); |
| 119 | 134 | e.tabEdit.dataset.tabLabel = 'Edit'; |
| 120 | 135 | this.#tabs.addTab( e.tabEdit ); |
| 121 | 136 | this.#tabs.switchToTab( e.tabEdit ); |
| 122 | - | |
| 137 | + if( opt.draftKey ){ | |
| 138 | + const key = opt.draftKey+'.content'; | |
| 139 | + this.editorContent = F.storage.get(key,''); | |
| 140 | + e.editor.addEventListener( | |
| 141 | + 'blur', ()=>F.storage.set(key, this.editorContent) | |
| 142 | + ); | |
| 143 | + } | |
| 123 | 144 | e.preview = D.addClass(D.div(), 'preview'); |
| 124 | 145 | e.preview.dataset.tabLabel = 'Preview'; |
| 125 | 146 | this.#tabs.addTab( e.preview ); |
| 126 | 147 | } |
| 127 | 148 | |
| @@ -154,15 +175,43 @@ | ||
| 154 | 175 | /* Reminder: we don't currently have a way to disable/enable |
| 155 | 176 | an Attacher's controls. */ |
| 156 | 177 | } |
| 157 | 178 | e.buttons.append(e.button.preview, e.button.submit); |
| 158 | 179 | this.#toDisable.push(e.button.preview); |
| 180 | + | |
| 181 | + if( opt.hiddenFields ){ | |
| 182 | + this.addHiddenFields( opt.hiddenFields ); | |
| 183 | + delete opt.hiddenFields; | |
| 184 | + } | |
| 185 | + | |
| 159 | 186 | }/*constructor*/ |
| 160 | 187 | |
| 188 | + /** This widget's top-most DOM element. */ | |
| 161 | 189 | get widget(){ |
| 162 | 190 | return this.#e.widget; |
| 163 | 191 | } |
| 192 | + | |
| 193 | + get editorContent(){ | |
| 194 | + /* We wrap access to the editor's contents in a getter/setter so | |
| 195 | + that we can eventually add optional use of a contenteditable | |
| 196 | + edit field, as those are generally more comfortable. The code | |
| 197 | + for that is in fossil.page.chat.js. */ | |
| 198 | + return this.#e.editor.value; | |
| 199 | + } | |
| 200 | + | |
| 201 | + set editorContent(v){ | |
| 202 | + this.#e.editor.value = v; | |
| 203 | + } | |
| 204 | + | |
| 205 | + /** Clears any draft state. */ | |
| 206 | + clearDraft(){ | |
| 207 | + const k = this.#opt.draftKey; | |
| 208 | + if( k ){ | |
| 209 | + F.storage.remove(k+'.content'); | |
| 210 | + F.storage.remove(k+'.title'); | |
| 211 | + } | |
| 212 | + } | |
| 164 | 213 | |
| 165 | 214 | /** |
| 166 | 215 | Reports an error by appending each argument to the error widget |
| 167 | 216 | and unhiding it. If passed no arugments, it clears and hides |
| 168 | 217 | the error widget. |
| @@ -185,25 +234,31 @@ | ||
| 185 | 234 | */ |
| 186 | 235 | addHiddenFields(list){ |
| 187 | 236 | this.#extraFields ??= []; |
| 188 | 237 | for( const f of list ){ |
| 189 | 238 | if( 'title'===f.name && this.#opt.isNewThread ){ |
| 190 | - this.#e.title.value = f.value; | |
| 239 | + if( !this.#e.title.value ){ | |
| 240 | + this.#e.title.value = f.value; | |
| 241 | + } | |
| 191 | 242 | }else{ |
| 192 | 243 | this.#extraFields.push(f); |
| 193 | 244 | } |
| 194 | 245 | } |
| 195 | 246 | } |
| 247 | + | |
| 248 | + get mimetype(){ | |
| 249 | + return e.mimetype.select.value; | |
| 250 | + } | |
| 196 | 251 | |
| 197 | 252 | async #fetchPreview(content){ |
| 198 | 253 | /* TODO: fetch preview */ |
| 199 | 254 | const e = this.#e; |
| 200 | 255 | const fd = new FormData; |
| 201 | 256 | for(const f of this.#extraFields){ |
| 202 | 257 | fd.append(f.name, f.value); |
| 203 | 258 | } |
| 204 | - fd.append('mimetype', e.mimetype.select.value); | |
| 259 | + fd.append('mimetype', this.mimetype); | |
| 205 | 260 | fd.append('content', content); |
| 206 | 261 | return window |
| 207 | 262 | .fetch(F.repoUrl('wikiajax/preview'), { |
| 208 | 263 | method: 'POST', |
| 209 | 264 | body: fd |
| @@ -215,10 +270,20 @@ | ||
| 215 | 270 | throw new Error(o.error); |
| 216 | 271 | } |
| 217 | 272 | return t; |
| 218 | 273 | }); |
| 219 | 274 | } |
| 275 | + | |
| 276 | + setContent(rawHtml){ | |
| 277 | + const preview = this.#e.preview; | |
| 278 | + previe.innerHTML = c; | |
| 279 | + if(F.pikchr && 'text/x-markdown'===this.mimetype){ | |
| 280 | + F.pikchr.addSrcView( | |
| 281 | + preview.querySelectorAll('svg.pikchr') | |
| 282 | + ); | |
| 283 | + } | |
| 284 | + } | |
| 220 | 285 | |
| 221 | 286 | async #preview(){ |
| 222 | 287 | if( this.#isWaiting ) return; |
| 223 | 288 | const e = this.#e; |
| 224 | 289 | if( e.preview !== this.#activeTab ){ |
| @@ -226,19 +291,19 @@ | ||
| 226 | 291 | /* Will recurse into here */ |
| 227 | 292 | return; |
| 228 | 293 | } |
| 229 | 294 | this.#isWaiting = true; |
| 230 | 295 | D.clearElement(e.preview); |
| 231 | - const content = e.editor.value.trim(); | |
| 296 | + const content = this.editorContent.trim(); | |
| 232 | 297 | if( !content ){ |
| 233 | 298 | return; |
| 234 | 299 | } |
| 235 | 300 | D.disable(this.#toDisable, e.button.submit); |
| 236 | 301 | e.preview.textContent = "Fetching preview..."; |
| 237 | 302 | this.#fetchPreview(content) |
| 238 | 303 | .then((c)=>{ |
| 239 | - e.preview.innerHTML = c; | |
| 304 | + this.setContent(c); | |
| 240 | 305 | D.enable(this.#toDisable, e.button.submit); |
| 241 | 306 | }) |
| 242 | 307 | .catch(err=>{ |
| 243 | 308 | e.preview.textContent = "Error fetching preview: "+err.message; |
| 244 | 309 | this.reportError(err.message); |
| @@ -264,10 +329,14 @@ | ||
| 264 | 329 | if( !this.#validate() ) return; |
| 265 | 330 | this.#isWaiting = true; |
| 266 | 331 | const e = this.#e; |
| 267 | 332 | D.disable(e.button.submit); |
| 268 | 333 | this.reportError("Submit is TODO."); |
| 334 | + if( opt.draftKey ){ | |
| 335 | + F.storage.remove(opt.draftKey+'.content'); | |
| 336 | + F.storage.remove(opt.draftKey+'.title'); | |
| 337 | + } | |
| 269 | 338 | /* |
| 270 | 339 | TODO: save it, set #isWaiting=false, then handle error or |
| 271 | 340 | redirect to the post (if this is a new post) or, if replying |
| 272 | 341 | inline, replace this object with a static rendering from the |
| 273 | 342 | response. |
| @@ -463,13 +532,15 @@ | ||
| 463 | 532 | const eForumNew = document.body.classList.contains('cpage-forumnew') |
| 464 | 533 | ? document.querySelector('#forumnew-placeholder') |
| 465 | 534 | : null; |
| 466 | 535 | if( eForumNew ){ |
| 467 | 536 | /* /forumnew */ |
| 468 | - const fpe = new fossil.ForumPostEditor({}); | |
| 469 | - fpe.addHiddenFields( eForumNew.querySelectorAll('input[type=hidden]') ); | |
| 537 | + const fpe = new fossil.ForumPostEditor({ | |
| 538 | + draftKey: 'forumnew', | |
| 539 | + hiddenFields: eForumNew.querySelectorAll('input[type=hidden]') | |
| 540 | + }); | |
| 470 | 541 | eForumNew.parentElement.insertBefore(fpe.widget, eForumNew); |
| 471 | 542 | eForumNew.remove(); |
| 472 | 543 | fossil.page.fpe = fpe /* for testing via the console */; |
| 473 | 544 | }/*eForumNew*/ |
| 474 | 545 | })/*F.onPageLoad callback*/; |
| 475 | 546 | })(window.fossil); |
| 476 | 547 |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -1,8 +1,8 @@ | |
| 1 | /** |
| 2 | Code for the forum family of pages. Requires fossil.X where X is |
| 3 | (copybutton, pikchr, confirmer, attach, tabs). |
| 4 | */ |
| 5 | (function(F/*the fossil object*/){ |
| 6 | "use strict"; |
| 7 | /* JS code for /forumpost and friends. Requires fossil.dom |
| 8 | and can optionally use fossil.pikchr. */ |
| @@ -30,18 +30,26 @@ | |
| 30 | /* Extra input[type=hidden] fields imported from fossil's |
| 31 | static page generation. */ |
| 32 | #extraFields; |
| 33 | |
| 34 | /** |
| 35 | */ |
| 36 | constructor(opt){ |
| 37 | opt = this.#opt = F.nu({ |
| 38 | // todo: defaults once we determine the options |
| 39 | // replyTo: hash |
| 40 | // edit: hash |
| 41 | }, opt); |
| 42 | opt.isNewThread = !opt.replyTo && !opt.edit; |
| 43 | const e = this.#e = F.nu({ |
| 44 | mimetype: F.nu(), |
| 45 | button: F.nu() |
| 46 | }); |
| 47 | const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor'); |
| @@ -52,10 +60,17 @@ | |
| 52 | e.title.setAttribute('maxlength', 125); |
| 53 | e.titleBar.append( |
| 54 | D.append(D.span(), "Title:"), |
| 55 | e.title |
| 56 | ); |
| 57 | wrapper.append(e.titleBar); |
| 58 | } |
| 59 | e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper'); |
| 60 | e.mimetype.select = D.addClass(D.select(), 'mimetype-select'); |
| 61 | this.#toDisable.push(e.mimetype.select); |
| @@ -92,11 +107,11 @@ | |
| 92 | e.err = D.addClass(D.div(), 'error', 'hidden'); |
| 93 | wrapper.append(e.err); |
| 94 | e.err.addEventListener('dblclick',()=>this.reportError()); |
| 95 | |
| 96 | const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */; |
| 97 | { /* Tabs... */ |
| 98 | e.tabs = D.attr( |
| 99 | D.addClass(D.div(), 'tab-container'), |
| 100 | 'id', idPrefix+'-tabs' |
| 101 | ); |
| 102 | this.#tabs = new F.TabManager(e.tabs); |
| @@ -117,11 +132,17 @@ | |
| 117 | ); |
| 118 | e.tabEdit.append(e.editor); |
| 119 | e.tabEdit.dataset.tabLabel = 'Edit'; |
| 120 | this.#tabs.addTab( e.tabEdit ); |
| 121 | this.#tabs.switchToTab( e.tabEdit ); |
| 122 | |
| 123 | e.preview = D.addClass(D.div(), 'preview'); |
| 124 | e.preview.dataset.tabLabel = 'Preview'; |
| 125 | this.#tabs.addTab( e.preview ); |
| 126 | } |
| 127 | |
| @@ -154,15 +175,43 @@ | |
| 154 | /* Reminder: we don't currently have a way to disable/enable |
| 155 | an Attacher's controls. */ |
| 156 | } |
| 157 | e.buttons.append(e.button.preview, e.button.submit); |
| 158 | this.#toDisable.push(e.button.preview); |
| 159 | }/*constructor*/ |
| 160 | |
| 161 | get widget(){ |
| 162 | return this.#e.widget; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | Reports an error by appending each argument to the error widget |
| 167 | and unhiding it. If passed no arugments, it clears and hides |
| 168 | the error widget. |
| @@ -185,25 +234,31 @@ | |
| 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){ |
| 198 | /* TODO: fetch preview */ |
| 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 |
| @@ -215,10 +270,20 @@ | |
| 215 | throw new Error(o.error); |
| 216 | } |
| 217 | return t; |
| 218 | }); |
| 219 | } |
| 220 | |
| 221 | async #preview(){ |
| 222 | if( this.#isWaiting ) return; |
| 223 | const e = this.#e; |
| 224 | if( e.preview !== this.#activeTab ){ |
| @@ -226,19 +291,19 @@ | |
| 226 | /* Will recurse into here */ |
| 227 | return; |
| 228 | } |
| 229 | this.#isWaiting = true; |
| 230 | D.clearElement(e.preview); |
| 231 | const content = e.editor.value.trim(); |
| 232 | if( !content ){ |
| 233 | return; |
| 234 | } |
| 235 | D.disable(this.#toDisable, e.button.submit); |
| 236 | e.preview.textContent = "Fetching preview..."; |
| 237 | this.#fetchPreview(content) |
| 238 | .then((c)=>{ |
| 239 | e.preview.innerHTML = c; |
| 240 | D.enable(this.#toDisable, e.button.submit); |
| 241 | }) |
| 242 | .catch(err=>{ |
| 243 | e.preview.textContent = "Error fetching preview: "+err.message; |
| 244 | this.reportError(err.message); |
| @@ -264,10 +329,14 @@ | |
| 264 | if( !this.#validate() ) return; |
| 265 | this.#isWaiting = true; |
| 266 | const e = this.#e; |
| 267 | D.disable(e.button.submit); |
| 268 | this.reportError("Submit is TODO."); |
| 269 | /* |
| 270 | TODO: save it, set #isWaiting=false, then handle error or |
| 271 | redirect to the post (if this is a new post) or, if replying |
| 272 | inline, replace this object with a static rendering from the |
| 273 | response. |
| @@ -463,13 +532,15 @@ | |
| 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*/ |
| 474 | })/*F.onPageLoad callback*/; |
| 475 | })(window.fossil); |
| 476 |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -1,8 +1,8 @@ | |
| 1 | /** |
| 2 | Code for the forum family of pages. Requires fossil.X where X is |
| 3 | (copybutton, pikchr, confirmer, attach, tabs, storage). |
| 4 | */ |
| 5 | (function(F/*the fossil object*/){ |
| 6 | "use strict"; |
| 7 | /* JS code for /forumpost and friends. Requires fossil.dom |
| 8 | and can optionally use fossil.pikchr. */ |
| @@ -30,18 +30,26 @@ | |
| 30 | /* Extra input[type=hidden] fields imported from fossil's |
| 31 | static page generation. */ |
| 32 | #extraFields; |
| 33 | |
| 34 | /** |
| 35 | Options: |
| 36 | |
| 37 | opt.draftKey[string=undefined]: if set then this object's state |
| 38 | will be stored in fossil.storage when the relevant input fields |
| 39 | lose focus. If old state is found, the form is pre-populated |
| 40 | from it. The state is cleared on a successful submit. |
| 41 | */ |
| 42 | constructor(opt){ |
| 43 | opt = this.#opt = F.nu({ |
| 44 | // todo: defaults once we determine the options |
| 45 | // replyTo: hash |
| 46 | // edit: hash |
| 47 | draftKey: undefined |
| 48 | }, opt); |
| 49 | opt.isNewThread = !opt.replyTo && !opt.edit; |
| 50 | if( !opt.draftKey) opt.draftKey = ''; |
| 51 | const e = this.#e = F.nu({ |
| 52 | mimetype: F.nu(), |
| 53 | button: F.nu() |
| 54 | }); |
| 55 | const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor'); |
| @@ -52,10 +60,17 @@ | |
| 60 | e.title.setAttribute('maxlength', 125); |
| 61 | e.titleBar.append( |
| 62 | D.append(D.span(), "Title:"), |
| 63 | e.title |
| 64 | ); |
| 65 | if( opt.draftKey ){ |
| 66 | const key = opt.draftKey+'.title'; |
| 67 | e.title.addEventListener('blur', ()=>{ |
| 68 | F.storage.set(key, e.title.value) |
| 69 | }); |
| 70 | e.title.value = F.storage.get(key,''); |
| 71 | } |
| 72 | wrapper.append(e.titleBar); |
| 73 | } |
| 74 | e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper'); |
| 75 | e.mimetype.select = D.addClass(D.select(), 'mimetype-select'); |
| 76 | this.#toDisable.push(e.mimetype.select); |
| @@ -92,11 +107,11 @@ | |
| 107 | e.err = D.addClass(D.div(), 'error', 'hidden'); |
| 108 | wrapper.append(e.err); |
| 109 | e.err.addEventListener('dblclick',()=>this.reportError()); |
| 110 | |
| 111 | const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */; |
| 112 | { /* Main tabs... */ |
| 113 | e.tabs = D.attr( |
| 114 | D.addClass(D.div(), 'tab-container'), |
| 115 | 'id', idPrefix+'-tabs' |
| 116 | ); |
| 117 | this.#tabs = new F.TabManager(e.tabs); |
| @@ -117,11 +132,17 @@ | |
| 132 | ); |
| 133 | e.tabEdit.append(e.editor); |
| 134 | e.tabEdit.dataset.tabLabel = 'Edit'; |
| 135 | this.#tabs.addTab( e.tabEdit ); |
| 136 | this.#tabs.switchToTab( e.tabEdit ); |
| 137 | if( opt.draftKey ){ |
| 138 | const key = opt.draftKey+'.content'; |
| 139 | this.editorContent = F.storage.get(key,''); |
| 140 | e.editor.addEventListener( |
| 141 | 'blur', ()=>F.storage.set(key, this.editorContent) |
| 142 | ); |
| 143 | } |
| 144 | e.preview = D.addClass(D.div(), 'preview'); |
| 145 | e.preview.dataset.tabLabel = 'Preview'; |
| 146 | this.#tabs.addTab( e.preview ); |
| 147 | } |
| 148 | |
| @@ -154,15 +175,43 @@ | |
| 175 | /* Reminder: we don't currently have a way to disable/enable |
| 176 | an Attacher's controls. */ |
| 177 | } |
| 178 | e.buttons.append(e.button.preview, e.button.submit); |
| 179 | this.#toDisable.push(e.button.preview); |
| 180 | |
| 181 | if( opt.hiddenFields ){ |
| 182 | this.addHiddenFields( opt.hiddenFields ); |
| 183 | delete opt.hiddenFields; |
| 184 | } |
| 185 | |
| 186 | }/*constructor*/ |
| 187 | |
| 188 | /** This widget's top-most DOM element. */ |
| 189 | get widget(){ |
| 190 | return this.#e.widget; |
| 191 | } |
| 192 | |
| 193 | get editorContent(){ |
| 194 | /* We wrap access to the editor's contents in a getter/setter so |
| 195 | that we can eventually add optional use of a contenteditable |
| 196 | edit field, as those are generally more comfortable. The code |
| 197 | for that is in fossil.page.chat.js. */ |
| 198 | return this.#e.editor.value; |
| 199 | } |
| 200 | |
| 201 | set editorContent(v){ |
| 202 | this.#e.editor.value = v; |
| 203 | } |
| 204 | |
| 205 | /** Clears any draft state. */ |
| 206 | clearDraft(){ |
| 207 | const k = this.#opt.draftKey; |
| 208 | if( k ){ |
| 209 | F.storage.remove(k+'.content'); |
| 210 | F.storage.remove(k+'.title'); |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | /** |
| 215 | Reports an error by appending each argument to the error widget |
| 216 | and unhiding it. If passed no arugments, it clears and hides |
| 217 | the error widget. |
| @@ -185,25 +234,31 @@ | |
| 234 | */ |
| 235 | addHiddenFields(list){ |
| 236 | this.#extraFields ??= []; |
| 237 | for( const f of list ){ |
| 238 | if( 'title'===f.name && this.#opt.isNewThread ){ |
| 239 | if( !this.#e.title.value ){ |
| 240 | this.#e.title.value = f.value; |
| 241 | } |
| 242 | }else{ |
| 243 | this.#extraFields.push(f); |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | get mimetype(){ |
| 249 | return e.mimetype.select.value; |
| 250 | } |
| 251 | |
| 252 | async #fetchPreview(content){ |
| 253 | /* TODO: fetch preview */ |
| 254 | const e = this.#e; |
| 255 | const fd = new FormData; |
| 256 | for(const f of this.#extraFields){ |
| 257 | fd.append(f.name, f.value); |
| 258 | } |
| 259 | fd.append('mimetype', this.mimetype); |
| 260 | fd.append('content', content); |
| 261 | return window |
| 262 | .fetch(F.repoUrl('wikiajax/preview'), { |
| 263 | method: 'POST', |
| 264 | body: fd |
| @@ -215,10 +270,20 @@ | |
| 270 | throw new Error(o.error); |
| 271 | } |
| 272 | return t; |
| 273 | }); |
| 274 | } |
| 275 | |
| 276 | setContent(rawHtml){ |
| 277 | const preview = this.#e.preview; |
| 278 | previe.innerHTML = c; |
| 279 | if(F.pikchr && 'text/x-markdown'===this.mimetype){ |
| 280 | F.pikchr.addSrcView( |
| 281 | preview.querySelectorAll('svg.pikchr') |
| 282 | ); |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | async #preview(){ |
| 287 | if( this.#isWaiting ) return; |
| 288 | const e = this.#e; |
| 289 | if( e.preview !== this.#activeTab ){ |
| @@ -226,19 +291,19 @@ | |
| 291 | /* Will recurse into here */ |
| 292 | return; |
| 293 | } |
| 294 | this.#isWaiting = true; |
| 295 | D.clearElement(e.preview); |
| 296 | const content = this.editorContent.trim(); |
| 297 | if( !content ){ |
| 298 | return; |
| 299 | } |
| 300 | D.disable(this.#toDisable, e.button.submit); |
| 301 | e.preview.textContent = "Fetching preview..."; |
| 302 | this.#fetchPreview(content) |
| 303 | .then((c)=>{ |
| 304 | this.setContent(c); |
| 305 | D.enable(this.#toDisable, e.button.submit); |
| 306 | }) |
| 307 | .catch(err=>{ |
| 308 | e.preview.textContent = "Error fetching preview: "+err.message; |
| 309 | this.reportError(err.message); |
| @@ -264,10 +329,14 @@ | |
| 329 | if( !this.#validate() ) return; |
| 330 | this.#isWaiting = true; |
| 331 | const e = this.#e; |
| 332 | D.disable(e.button.submit); |
| 333 | this.reportError("Submit is TODO."); |
| 334 | if( opt.draftKey ){ |
| 335 | F.storage.remove(opt.draftKey+'.content'); |
| 336 | F.storage.remove(opt.draftKey+'.title'); |
| 337 | } |
| 338 | /* |
| 339 | TODO: save it, set #isWaiting=false, then handle error or |
| 340 | redirect to the post (if this is a new post) or, if replying |
| 341 | inline, replace this object with a static rendering from the |
| 342 | response. |
| @@ -463,13 +532,15 @@ | |
| 532 | const eForumNew = document.body.classList.contains('cpage-forumnew') |
| 533 | ? document.querySelector('#forumnew-placeholder') |
| 534 | : null; |
| 535 | if( eForumNew ){ |
| 536 | /* /forumnew */ |
| 537 | const fpe = new fossil.ForumPostEditor({ |
| 538 | draftKey: 'forumnew', |
| 539 | hiddenFields: eForumNew.querySelectorAll('input[type=hidden]') |
| 540 | }); |
| 541 | eForumNew.parentElement.insertBefore(fpe.widget, eForumNew); |
| 542 | eForumNew.remove(); |
| 543 | fossil.page.fpe = fpe /* for testing via the console */; |
| 544 | }/*eForumNew*/ |
| 545 | })/*F.onPageLoad callback*/; |
| 546 | })(window.fossil); |
| 547 |