| | @@ -1,10 +1,12 @@ |
| 1 | 1 | (function(F/*the fossil object*/){ |
| 2 | 2 | "use strict"; |
| 3 | 3 | /** |
| 4 | 4 | Code for the /filepage app. Requires that the fossil JS |
| 5 | | - bootstrapping is complete and fossil.fetch() has been installed. |
| 5 | + bootstrapping is complete and that several fossil JS APIs have |
| 6 | + been installed: fossil.fetch, fossil.dom, fossil.tabs, |
| 7 | + fossil.storage, fossil.confirmer. |
| 6 | 8 | |
| 7 | 9 | Custom events, handled via fossil.page.addEventListener(): |
| 8 | 10 | |
| 9 | 11 | - Event 'fileedit-file-loaded': passes on information when it |
| 10 | 12 | loads a file, in the form of an object: |
| | @@ -58,21 +60,207 @@ |
| 58 | 60 | */ |
| 59 | 61 | const E = (s)=>document.querySelector(s), |
| 60 | 62 | D = F.dom, |
| 61 | 63 | P = F.page; |
| 62 | 64 | |
| 65 | + P.config = { |
| 66 | + defaultMaxStashSize: 7 |
| 67 | + }; |
| 68 | + |
| 69 | + /** |
| 70 | + $stash is an internal-use-only object for managing "stashed" |
| 71 | + local edits, to help avoid that users accidentally lose content |
| 72 | + by switching tabs or following links or some such. The basic |
| 73 | + theory of operation is... |
| 74 | + |
| 75 | + All "stashed" state is stored using fossil.storage. |
| 76 | + |
| 77 | + - When the current file content is modified by the user, the |
| 78 | + current stathe of the current P.finfo and its the content |
| 79 | + is stashed. For the built-in editor widget, "changes" is |
| 80 | + notified via a 'change' event. For a client-side custom |
| 81 | + widget, the client needs to call P.stashContentChange() when |
| 82 | + their widget triggers the equivalent of a 'change' event. |
| 83 | + |
| 84 | + - For certain non-content updates (as of this writing, only the |
| 85 | + is-executable checkbox), only the P.finfo stash entry is |
| 86 | + updated, not the content (unless the content has not yet been |
| 87 | + stashed, in which case it is also stashed so that the stash |
| 88 | + always has matching pairs of finfo/content). |
| 89 | + |
| 90 | + - When saving, the stashed entry for the previous version is removed |
| 91 | + from the stash. |
| 92 | + |
| 93 | + - When "loading", we use any stashed state for the given |
| 94 | + checkin/file combination. When forcing a re-load of content, |
| 95 | + any stashed entry for that combination is removed from the |
| 96 | + stash. |
| 97 | + |
| 98 | + - Every time P.stashContentChange() updates the stash, it is |
| 99 | + pruned to $stash.prune.defaultMaxCount most-recently-updated |
| 100 | + entries. |
| 101 | + |
| 102 | + - This API often refers to "finfo objects." Those are objects |
| 103 | + with a minimum of {checkin,filename} properties (which must be |
| 104 | + valid), and a combination of those two properties is used as |
| 105 | + basis for the stash keys for any given checkin/filename |
| 106 | + combination. |
| 107 | + |
| 108 | + The structure of the stash is a bit convoluted for efficiency's |
| 109 | + sake: we store a map of file info (finfo) objects separately from |
| 110 | + those files' contents because otherwise we would be required to |
| 111 | + JSONize/de-JSONize the file content when stashing/restoring it, |
| 112 | + and that would be horribly inefficient (meaning "battery-consuming" |
| 113 | + on mobile devices). |
| 114 | + */ |
| 115 | + const $stash = { |
| 116 | + keys: { |
| 117 | + index: F.page.name+':index' |
| 118 | + }, |
| 119 | + /** |
| 120 | + index: { |
| 121 | + "CHECKIN_HASH:FILENAME": {file info w/o content} |
| 122 | + ... |
| 123 | + } |
| 124 | + |
| 125 | + In F.storage we... |
| 126 | + |
| 127 | + - Store this.index under the key this.keys.index. |
| 128 | + |
| 129 | + - Store each file's content under the key |
| 130 | + (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately |
| 131 | + from the index entries to avoid having to JSONize/de-JSONize |
| 132 | + the content. The assumption/hope is that the browser can store |
| 133 | + those records "directly," without any intermediary |
| 134 | + encoding/decoding going on. |
| 135 | + */ |
| 136 | + indexKey: function(finfo){return finfo.checkin+':'+finfo.filename}, |
| 137 | + /** Returns the key for storing content for the given key suffix, |
| 138 | + by prepending P.name to suffix. */ |
| 139 | + contentKey: function(suffix){return P.name+'/'+suffix}, |
| 140 | + /** Returns the index object, fetching it from the stash or creating |
| 141 | + it anew on the first call. */ |
| 142 | + getIndex: function(){ |
| 143 | + if(!this.index) this.index = F.storage.getJSON(this.keys.index,{}); |
| 144 | + return this.index; |
| 145 | + }, |
| 146 | + _fireStashEvent: function(){ |
| 147 | + if(this._disableNextEvent) delete this._disableNextEvent; |
| 148 | + else F.page.dispatchEvent('fileedit-stash-updated', this); |
| 149 | + }, |
| 150 | + /** |
| 151 | + Returns the stashed version, if any, for the given finfo object. |
| 152 | + */ |
| 153 | + getFinfo: function(finfo){ |
| 154 | + const ndx = this.getIndex(); |
| 155 | + return ndx[this.indexKey(finfo)]; |
| 156 | + }, |
| 157 | + /** Serializes this object's index to F.storage. Returns this. */ |
| 158 | + storeIndex: function(){ |
| 159 | + if(this.index) F.storage.setJSON(this.keys.index,this.index); |
| 160 | + return this; |
| 161 | + }, |
| 162 | + /** Updates the stash record for the given finfo |
| 163 | + and (optionally) content. If passed 1 arg, only |
| 164 | + the finfo stash is updated, else both the finfo |
| 165 | + and its contents are (re-)stashed. Returns this. |
| 166 | + */ |
| 167 | + updateFile: function(finfo,content){ |
| 168 | + const ndx = this.getIndex(), |
| 169 | + key = this.indexKey(finfo), |
| 170 | + old = ndx[key]; |
| 171 | + const record = old || (ndx[key]={ |
| 172 | + checkin: finfo.checkin, |
| 173 | + filename: finfo.filename, |
| 174 | + mimetype: finfo.mimetype |
| 175 | + }); |
| 176 | + record.isExe = !!finfo.isExe; |
| 177 | + record.stashTime = new Date().getTime(); |
| 178 | + this.storeIndex(); |
| 179 | + if(arguments.length>1){ |
| 180 | + F.storage.set(this.contentKey(key), content); |
| 181 | + } |
| 182 | + this._fireStashEvent(); |
| 183 | + return this; |
| 184 | + }, |
| 185 | + /** |
| 186 | + Returns the stashed content, if any, for the given finfo |
| 187 | + object. |
| 188 | + */ |
| 189 | + stashedContent: function(finfo){ |
| 190 | + return F.storage.get(this.contentKey(this.indexKey(finfo))); |
| 191 | + }, |
| 192 | + /** Returns true if we have stashed content for the given finfo |
| 193 | + record. */ |
| 194 | + hasStashedContent: function(finfo){ |
| 195 | + return F.storage.contains(this.contentKey(this.indexKey(finfo))); |
| 196 | + }, |
| 197 | + /** Unstashes the given finfo record and its content. |
| 198 | + Returns this. */ |
| 199 | + unstash: function(finfo){ |
| 200 | + const ndx = this.getIndex(), |
| 201 | + key = this.indexKey(finfo); |
| 202 | + delete finfo.stashTime; |
| 203 | + delete ndx[key]; |
| 204 | + F.storage.remove(this.contentKey(key)); |
| 205 | + this.storeIndex(); |
| 206 | + this._fireStashEvent(); |
| 207 | + return this; |
| 208 | + }, |
| 209 | + /** |
| 210 | + Clears all $stash entries from F.storage. Returns this. |
| 211 | + */ |
| 212 | + clear: function(){ |
| 213 | + const ndx = this.getIndex(), |
| 214 | + self = this; |
| 215 | + let count = 0; |
| 216 | + Object.keys(ndx).forEach(function(k){ |
| 217 | + ++count; |
| 218 | + const e = ndx[k]; |
| 219 | + delete ndx[k]; |
| 220 | + F.storage.remove(self.contentKey(k)); |
| 221 | + }); |
| 222 | + F.storage.remove(this.keys.index); |
| 223 | + delete this.index; |
| 224 | + if(count) this._fireStashEvent(); |
| 225 | + return this; |
| 226 | + }, |
| 227 | + /** |
| 228 | + Removes all but the maxCount most-recently-updated stash |
| 229 | + entries, where maxCount defaults to this.prune.defaultMaxCount. |
| 230 | + */ |
| 231 | + prune: function f(maxCount){ |
| 232 | + const ndx = this.getIndex(); |
| 233 | + const li = []; |
| 234 | + if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount; |
| 235 | + Object.keys(ndx).forEach((k)=>li.push(ndx[k])); |
| 236 | + li.sort((l,r)=>l.stashTime - r.stashTime); |
| 237 | + let n = 0; |
| 238 | + while(li.length>maxCount){ |
| 239 | + ++n; |
| 240 | + const e = li.shift(); |
| 241 | + this._disableNextEvent = true; |
| 242 | + this.unstash(e); |
| 243 | + console.warn("Pruned oldest stash entry:",e); |
| 244 | + } |
| 245 | + if(n) this._fireStashEvent(); |
| 246 | + } |
| 247 | + }; |
| 248 | + $stash.prune.defaultMaxCount = P.config.defaultMaxStashSize; |
| 249 | + |
| 63 | 250 | /** |
| 64 | 251 | Widget for the checkin/file selection list. |
| 65 | 252 | */ |
| 66 | | - P.fileSelector = { |
| 253 | + P.fileSelectWidget = { |
| 67 | 254 | e:{ |
| 68 | 255 | container: E('#fileedit-file-selector') |
| 69 | 256 | }, |
| 70 | 257 | finfo: {}, |
| 71 | 258 | cache: { |
| 72 | 259 | checkins: undefined, |
| 73 | | - files:{} |
| 260 | + files:{}, |
| 261 | + branchNames: {} |
| 74 | 262 | }, |
| 75 | 263 | /** |
| 76 | 264 | Fetches the list of leaf checkins from the server and updates |
| 77 | 265 | the UI with that list. |
| 78 | 266 | */ |
| | @@ -93,10 +281,11 @@ |
| 93 | 281 | self.cache.checkins = list; |
| 94 | 282 | D.clearElement(D.enable(self.e.selectCi)); |
| 95 | 283 | let loadThisOne; |
| 96 | 284 | list.forEach(function(o,n){ |
| 97 | 285 | if(!n) loadThisOne = o; |
| 286 | + self.cache.branchNames[F.hashDigits(o.checkin)] = o.branch; |
| 98 | 287 | D.option(self.e.selectCi, o.checkin, |
| 99 | 288 | o.timestamp+' ['+o.branch+']: ' |
| 100 | 289 | +F.hashDigits(o.checkin)); |
| 101 | 290 | }); |
| 102 | 291 | self.loadFiles(loadThisOne ? loadThisOne.checkin : false); |
| | @@ -151,10 +340,20 @@ |
| 151 | 340 | responseType: 'json', |
| 152 | 341 | onload |
| 153 | 342 | }); |
| 154 | 343 | return this; |
| 155 | 344 | }, |
| 345 | + |
| 346 | + /** |
| 347 | + If this object has ever loaded the given checkin version via |
| 348 | + loadLeaves(), this returns the branch name associated with that |
| 349 | + version, else returns undefined; |
| 350 | + */ |
| 351 | + checkinBranchName: function(uuid){ |
| 352 | + return this.cache.branchNames[F.hashDigits(uuid)]; |
| 353 | + }, |
| 354 | + |
| 156 | 355 | /** |
| 157 | 356 | Initializes the checkin/file selector widget. Must only be |
| 158 | 357 | called once. |
| 159 | 358 | */ |
| 160 | 359 | init: function(){ |
| | @@ -206,12 +405,108 @@ |
| 206 | 405 | btnReload.addEventListener( |
| 207 | 406 | 'click', (e)=>this.loadLeaves(), false |
| 208 | 407 | ); |
| 209 | 408 | delete this.init; |
| 210 | 409 | } |
| 211 | | - }/*P.fileSelector*/; |
| 410 | + }/*P.fileSelectWidget*/; |
| 212 | 411 | |
| 412 | + /** |
| 413 | + Widget for listing and selecting $stash entries. |
| 414 | + */ |
| 415 | + P.stashWidget = { |
| 416 | + e:{/*DOM element(s)*/}, |
| 417 | + init: function(domInsertPoint/*insert widget BEFORE this element*/){ |
| 418 | + const flow = D.addClass(D.div(), 'flex-container','flex-column'); |
| 419 | + const wrapper = D.addClass( |
| 420 | + D.attr(D.div(),'id','fileedit-stash-selector'), |
| 421 | + 'input-with-label' |
| 422 | + ); |
| 423 | + const sel = this.e.select = D.select(); |
| 424 | + const btnClear = this.e.btnClear |
| 425 | + = D.addClass(D.button("Clear"),'hidden'); |
| 426 | + D.append(flow, wrapper); |
| 427 | + D.append(wrapper, "Local edits ("+(F.storage.storageImplName())+"):", |
| 428 | + sel, btnClear); |
| 429 | + D.attr(wrapper, "title", [ |
| 430 | + 'Locally "stashed" edits. Timestamps are the last local edit time.', |
| 431 | + 'Only the',P.config.defaultMaxStashSize,'most recent checkin/file', |
| 432 | + 'combinations are retained.', |
| 433 | + 'Committing or reloading a file removes it from this stash.' |
| 434 | + ].join(' ')); |
| 435 | + D.option(D.disable(sel), "(empty)"); |
| 436 | + F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail)); |
| 437 | + F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail)); |
| 438 | + sel.addEventListener('change',function(e){ |
| 439 | + const opt = this.selectedOptions[0]; |
| 440 | + if(opt && opt._finfo) P.loadFile(opt._finfo); |
| 441 | + }); |
| 442 | + F.confirmer(btnClear, { |
| 443 | + confirmText: "REALLY delete ALL local edits?", |
| 444 | + onconfirm: (e)=>P.clearStash().loadFile(/*in case P.finfo() was in the stash*/), |
| 445 | + ticks: 3 |
| 446 | + }); |
| 447 | + if(F.storage.isTransient()){/*Warn if transient storage is in use...*/ |
| 448 | + D.append(flow, D.append(D.addClass(D.div(),'warning'), |
| 449 | + "Warning: persistent storage is not avaible, "+ |
| 450 | + "so uncomitted edits will not survive a page reload.") |
| 451 | + ); |
| 452 | + } |
| 453 | + domInsertPoint.parentNode.insertBefore(flow, domInsertPoint); |
| 454 | + $stash._fireStashEvent(/*update this object with the load-time stash*/); |
| 455 | + delete this.init; |
| 456 | + }, |
| 457 | + /** |
| 458 | + Regenerates the edit selection list. |
| 459 | + */ |
| 460 | + updateList: function f(stasher,theFinfo){ |
| 461 | + if(!f.compare){ |
| 462 | + const cmpBase = (l,r)=>l<r ? -1 : (l===r ? 0 : 1); |
| 463 | + f.compare = function(l,r){ |
| 464 | + const cmp = cmpBase(l.filename, r.filename); |
| 465 | + return cmp ? cmp : cmpBase(l.checkin, r.checkin); |
| 466 | + }; |
| 467 | + f.rxZ = /\.\d+Z$/ /* ms and 'Z' part of date string */; |
| 468 | + const pad=(x)=>(''+x).length>1 ? x : '0'+x; |
| 469 | + f.timestring = function ff(d){ |
| 470 | + return [ |
| 471 | + d.getFullYear(),'-',pad(d.getMonth()+1/*sigh*/),'-',pad(d.getDate()), |
| 472 | + '@',pad(d.getHours()),':',pad(d.getMinutes()) |
| 473 | + ].join(''); |
| 474 | + }; |
| 475 | + } |
| 476 | + const index = stasher.getIndex(), ilist = []; |
| 477 | + Object.keys(index).forEach((finfo)=>{ |
| 478 | + ilist.push(index[finfo]); |
| 479 | + }); |
| 480 | + const self = this; |
| 481 | + D.clearElement(this.e.select); |
| 482 | + if(0===ilist.length){ |
| 483 | + D.addClass(this.e.btnClear, 'hidden'); |
| 484 | + D.option(D.disable(this.e.select),"No local edits"); |
| 485 | + return; |
| 486 | + } |
| 487 | + D.enable(this.e.select); |
| 488 | + D.removeClass(this.e.btnClear, 'hidden'); |
| 489 | + D.disable(D.option(this.e.select,0,"Select a local edit...")); |
| 490 | + const currentFinfo = theFinfo || P.finfo || {}; |
| 491 | + ilist.sort(f.compare).forEach(function(finfo,n){ |
| 492 | + const key = stasher.indexKey(finfo), |
| 493 | + branch = P.fileSelectWidget.checkinBranchName(finfo.checkin); |
| 494 | + const opt = D.option( |
| 495 | + self.e.select, n+1/*value is (almost) irrelevant*/, |
| 496 | + [F.hashDigits(finfo.checkin, 6), branch, |
| 497 | + f.timestring(new Date(finfo.stashTime)), |
| 498 | + false ? finfo.filename : F.shortenFilename(finfo.filename) |
| 499 | + ].join(' ') |
| 500 | + ); |
| 501 | + opt._finfo = finfo; |
| 502 | + if(0===f.compare(currentFinfo, finfo)){ |
| 503 | + D.attr(opt, 'selected', true); |
| 504 | + } |
| 505 | + }); |
| 506 | + } |
| 507 | + }/*P.stashWidget*/; |
| 213 | 508 | |
| 214 | 509 | /** |
| 215 | 510 | Internal workaround to select the current preview mode |
| 216 | 511 | and fire a change event if the value actually changes |
| 217 | 512 | or if forceEvent is truthy. |
| | @@ -227,11 +522,11 @@ |
| 227 | 522 | // Force UI update |
| 228 | 523 | s.dispatchEvent(new Event('change',{target:s})); |
| 229 | 524 | } |
| 230 | 525 | }; |
| 231 | 526 | |
| 232 | | - window.addEventListener("load", function() { |
| 527 | + F.onPageLoad(function() { |
| 233 | 528 | P.base = {tag: E('base')}; |
| 234 | 529 | P.base.originalHref = P.base.tag.href; |
| 235 | 530 | P.tabs = new fossil.TabManager('#fileedit-tabs'); |
| 236 | 531 | P.e = { |
| 237 | 532 | taEditor: E('#fileedit-content-editor'), |
| | @@ -258,11 +553,10 @@ |
| 258 | 553 | preview: E('#fileedit-tab-preview'), |
| 259 | 554 | diff: E('#fileedit-tab-diff'), |
| 260 | 555 | commit: E('#fileedit-tab-commit') |
| 261 | 556 | } |
| 262 | 557 | }; |
| 263 | | - P.fileSelector.init(); |
| 264 | 558 | /* Figure out which comment editor to show by default and |
| 265 | 559 | hide the other one. By default we take the one which does |
| 266 | 560 | not have the 'hidden' CSS class. If neither do, we default |
| 267 | 561 | to single-line mode. */ |
| 268 | 562 | if(D.hasClass(P.e.taCommentSmall, 'hidden')){ |
| | @@ -384,29 +678,13 @@ |
| 384 | 678 | // Clear diff/preview when new content is loaded/set |
| 385 | 679 | 'fileedit-content-replaced', |
| 386 | 680 | ()=>D.clearElement(P.e.diffTarget, P.e.previewTarget) |
| 387 | 681 | ); |
| 388 | 682 | |
| 389 | | - /* Tell the user about which fossil.storage is being used... */ |
| 390 | | - let storageMsg = D.addClass(D.div(),'flex-container','flex-row', |
| 391 | | - 'fileedit-hint'); |
| 392 | | - if(F.storage.isTransient()){ |
| 393 | | - D.append( |
| 394 | | - D.addClass(storageMsg,'warning'), |
| 395 | | - "Warning: persistent storage is not avaible, "+ |
| 396 | | - "so unsaved edits "+ |
| 397 | | - "will not survive a page reload." |
| 398 | | - ); |
| 399 | | - }else{ |
| 400 | | - D.append( |
| 401 | | - storageMsg, |
| 402 | | - "Current storage mechanism for local edits: "+ |
| 403 | | - F.storage.storageImplName() |
| 404 | | - ); |
| 405 | | - } |
| 406 | | - P.e.tabs.content.insertBefore(storageMsg, P.e.tabs.content.lastElementChild); |
| 407 | | - }, false)/*onload event handler*/; |
| 683 | + P.fileSelectWidget.init(); |
| 684 | + P.stashWidget.init(P.e.tabs.content.lastElementChild); |
| 685 | + }/*F.onPageLoad()*/); |
| 408 | 686 | |
| 409 | 687 | /** |
| 410 | 688 | Getter (if called with no args) or setter (if passed an arg) for |
| 411 | 689 | the current file content. |
| 412 | 690 | |
| | @@ -608,13 +886,19 @@ |
| 608 | 886 | it triggers a 'fileedit-file-loaded' event, passing it |
| 609 | 887 | this.finfo. |
| 610 | 888 | */ |
| 611 | 889 | P.loadFile = function(file,rev){ |
| 612 | 890 | if(0===arguments.length){ |
| 891 | + /* Reload from this.finfo */ |
| 613 | 892 | if(!affirmHasFile()) return this; |
| 614 | 893 | file = this.finfo.filename; |
| 615 | 894 | rev = this.finfo.checkin; |
| 895 | + }else if(1===arguments.length){ |
| 896 | + /* Assume finfo-like object */ |
| 897 | + const arg = arguments[0]; |
| 898 | + file = arg.filename; |
| 899 | + rev = arg.checkin; |
| 616 | 900 | } |
| 617 | 901 | const self = this; |
| 618 | 902 | const onload = (r,headers)=>{ |
| 619 | 903 | delete self.finfo; |
| 620 | 904 | self.updateVersion({ |
| | @@ -796,11 +1080,11 @@ |
| 796 | 1080 | self.unstashContent(oldFinfo); |
| 797 | 1081 | delete c.manifest; |
| 798 | 1082 | self.finfo = c; |
| 799 | 1083 | self.e.taComment.value = ''; |
| 800 | 1084 | self.updateVersion(); |
| 801 | | - self.fileSelector.loadLeaves(); |
| 1085 | + self.fileSelectWidget.loadLeaves(); |
| 802 | 1086 | } |
| 803 | 1087 | F.message.apply(F, msg); |
| 804 | 1088 | self.tabs.switchToTab(self.e.tabs.commit); |
| 805 | 1089 | }; |
| 806 | 1090 | } |
| | @@ -843,176 +1127,10 @@ |
| 843 | 1127 | onload: f.onload |
| 844 | 1128 | }); |
| 845 | 1129 | return this; |
| 846 | 1130 | }; |
| 847 | 1131 | |
| 848 | | - /** |
| 849 | | - $stash is an internal-use-only object for managing "stashed" |
| 850 | | - local edits, to help avoid that users accidentally lose content |
| 851 | | - by switching tabs or following links or some such. The basic |
| 852 | | - theory of operation is... |
| 853 | | - |
| 854 | | - All "stashed" state is stored using fossil.storage. |
| 855 | | - |
| 856 | | - - When the current file content is modified by the user, the |
| 857 | | - current stathe of the current P.finfo and its the content |
| 858 | | - is stashed. For the built-in editor widget, "changes" is |
| 859 | | - notified via a 'change' event. For a client-side custom |
| 860 | | - widget, the client needs to call P.stashContentChange() when |
| 861 | | - their widget triggers the equivalent of a 'change' event. |
| 862 | | - |
| 863 | | - - For certain non-content updates (as of this writing, only the |
| 864 | | - is-executable checkbox), only the P.finfo stash entry is |
| 865 | | - updated, not the content (unless the content has not yet been |
| 866 | | - stashed, in which case it is also stashed so that the stash |
| 867 | | - always has matching pairs of finfo/content). |
| 868 | | - |
| 869 | | - - When saving, the stashed entry for the previous version is removed |
| 870 | | - from the stash. |
| 871 | | - |
| 872 | | - - When "loading", we use any stashed state for the given |
| 873 | | - checkin/file combination. When forcing a re-load of content, |
| 874 | | - any stashed entry for that combination is removed from the |
| 875 | | - stash. |
| 876 | | - |
| 877 | | - - Every time P.stashContentChange() updates the stash, it is |
| 878 | | - pruned to $stash.prune.defaultMaxCount most-recently-updated |
| 879 | | - entries. |
| 880 | | - |
| 881 | | - - This API often refers to "finfo objects." Those are objects |
| 882 | | - with a minimum of {checkin,filename} properties (which must be |
| 883 | | - valid), and a combination of those two properties is used as |
| 884 | | - basis for the stash keys for any given checkin/filename |
| 885 | | - combination. |
| 886 | | - |
| 887 | | - The structure of the stash is a bit convoluted for efficiency's |
| 888 | | - sake: we store a map of file info (finfo) objects separately from |
| 889 | | - those files' contents because otherwise we would be required to |
| 890 | | - JSONize/de-JSONize the file content when stashing/restoring it, |
| 891 | | - and that would be horribly inefficient (meaning "battery-consuming" |
| 892 | | - on mobile devices). |
| 893 | | - */ |
| 894 | | - const $stash = { |
| 895 | | - keys: { |
| 896 | | - index: F.page.name+':index' |
| 897 | | - }, |
| 898 | | - /** |
| 899 | | - index: { |
| 900 | | - "CHECKIN_HASH:FILENAME": {file info w/o content} |
| 901 | | - ... |
| 902 | | - } |
| 903 | | - |
| 904 | | - In F.storage we... |
| 905 | | - |
| 906 | | - - Store this.index under the key this.keys.index. |
| 907 | | - |
| 908 | | - - Store each file's content under the key |
| 909 | | - (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately |
| 910 | | - from the index entries to avoid having to JSONize/de-JSONize |
| 911 | | - the content. The assumption/hope is that the browser can store |
| 912 | | - those records "directly," without any intermediary |
| 913 | | - encoding/decoding going on. |
| 914 | | - */ |
| 915 | | - indexKey: function(finfo){return finfo.checkin+':'+finfo.filename}, |
| 916 | | - /** Returns the key for storing content for the given key suffix, |
| 917 | | - by prepending P.name to suffix. */ |
| 918 | | - contentKey: function(suffix){return P.name+'/'+suffix}, |
| 919 | | - /** Returns the index object, fetching it from the stash or creating |
| 920 | | - it anew on the first call. */ |
| 921 | | - getIndex: function(){ |
| 922 | | - if(!this.index) this.index = F.storage.getJSON(this.keys.index,{}); |
| 923 | | - return this.index; |
| 924 | | - }, |
| 925 | | - /** |
| 926 | | - Returns the stashed version, if any, for the given finfo object. |
| 927 | | - */ |
| 928 | | - getFinfo: function(finfo){ |
| 929 | | - const ndx = this.getIndex(); |
| 930 | | - return ndx[this.indexKey(finfo)]; |
| 931 | | - }, |
| 932 | | - /** Serializes this object's index to F.storage. Returns this. */ |
| 933 | | - storeIndex: function(){ |
| 934 | | - if(this.index) F.storage.setJSON(this.keys.index,this.index); |
| 935 | | - return this; |
| 936 | | - }, |
| 937 | | - /** Updates the stash record for the given finfo |
| 938 | | - and (optionally) content. If passed 1 arg, only |
| 939 | | - the finfo stash is updated, else both the finfo |
| 940 | | - and its contents are (re-)stashed. Returns this. |
| 941 | | - */ |
| 942 | | - updateFile: function(finfo,content){ |
| 943 | | - const ndx = this.getIndex(), |
| 944 | | - key = this.indexKey(finfo); |
| 945 | | - const record = ndx[key] || (ndx[key]={ |
| 946 | | - checkin: finfo.checkin, |
| 947 | | - filename: finfo.filename, |
| 948 | | - mimetype: finfo.mimetype |
| 949 | | - }); |
| 950 | | - record.isExe = !!finfo.isExe; |
| 951 | | - record.stashTime = new Date().getTime(); |
| 952 | | - this.storeIndex(); |
| 953 | | - if(arguments.length>1){ |
| 954 | | - F.storage.set(this.contentKey(key), content); |
| 955 | | - } |
| 956 | | - return this; |
| 957 | | - }, |
| 958 | | - /** |
| 959 | | - Returns the stashed content, if any, for the given finfo |
| 960 | | - object. |
| 961 | | - */ |
| 962 | | - stashedContent: function(finfo){ |
| 963 | | - return F.storage.get(this.contentKey(this.indexKey(finfo))); |
| 964 | | - }, |
| 965 | | - /** Returns true if we have stashed content for the given finfo |
| 966 | | - record. */ |
| 967 | | - hasStashedContent: function(finfo){ |
| 968 | | - return F.storage.contains(this.contentKey(this.indexKey(finfo))); |
| 969 | | - }, |
| 970 | | - /** Unstashes the given finfo record and its content. |
| 971 | | - Returns this. */ |
| 972 | | - unstash: function(finfo){ |
| 973 | | - const ndx = this.getIndex(), |
| 974 | | - key = this.indexKey(finfo); |
| 975 | | - delete finfo.stashTime; |
| 976 | | - delete ndx[key]; |
| 977 | | - F.storage.remove(this.contentKey(key)); |
| 978 | | - return this.storeIndex(); |
| 979 | | - }, |
| 980 | | - /** |
| 981 | | - Clears all $stash entries from F.storage. Returns this. |
| 982 | | - */ |
| 983 | | - clear: function(){ |
| 984 | | - const ndx = this.getIndex(), |
| 985 | | - self = this; |
| 986 | | - Object.keys(ndx).forEach(function(k){ |
| 987 | | - const e = ndx[k]; |
| 988 | | - delete ndx[k]; |
| 989 | | - F.storage.remove(self.contentKey(k)); |
| 990 | | - }); |
| 991 | | - F.storage.remove(this.keys.index); |
| 992 | | - delete this.index; |
| 993 | | - return this; |
| 994 | | - }, |
| 995 | | - /** |
| 996 | | - Removes all but the maxCount most-recently-updated stash |
| 997 | | - entries, where maxCount defaults to this.prune.defaultMaxCount. |
| 998 | | - */ |
| 999 | | - prune: function f(maxCount){ |
| 1000 | | - const ndx = this.getIndex(); |
| 1001 | | - const li = []; |
| 1002 | | - if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount; |
| 1003 | | - Object.keys(ndx).forEach((k)=>li.push(ndx[k])); |
| 1004 | | - li.sort((l,r)=>l.stashTime - r.stashTime); |
| 1005 | | - while(li.length>maxCount){ |
| 1006 | | - const e = li.shift(); |
| 1007 | | - this.unstash(e); |
| 1008 | | - console.warn("Pruned oldest stash entry:",e); |
| 1009 | | - } |
| 1010 | | - } |
| 1011 | | - }; |
| 1012 | | - $stash.prune.defaultMaxCount = 7; |
| 1013 | | - |
| 1014 | 1132 | /** |
| 1015 | 1133 | Updates P.finfo for certain state and stashes P.finfo, with the |
| 1016 | 1134 | current content fetched via P.fileContent(). |
| 1017 | 1135 | |
| 1018 | 1136 | If passed truthy AND the stash already has stashed content for |
| | @@ -1070,8 +1188,6 @@ |
| 1070 | 1188 | */ |
| 1071 | 1189 | P.getStashedFinfo = function(finfo){ |
| 1072 | 1190 | return $stash.getFinfo(finfo); |
| 1073 | 1191 | }; |
| 1074 | 1192 | |
| 1075 | | - P.$stash = $stash /*only for testing/debugging */; |
| 1076 | | - |
| 1077 | 1193 | })(window.fossil); |
| 1078 | 1194 | |