| | @@ -27,11 +27,11 @@ |
| 27 | 27 | |
| 28 | 28 | The fossil.page.wikiContent() method gets or sets the current |
| 29 | 29 | file content for the page. |
| 30 | 30 | |
| 31 | 31 | - Event 'wiki-saved': is fired when a commit completes, |
| 32 | | - passing on the same info as fileedit-file-loaded. |
| 32 | + passing on the same info as wiki-page-loaded. |
| 33 | 33 | |
| 34 | 34 | - Event 'wiki-content-replaced': when the editor's content is |
| 35 | 35 | replaced, as opposed to it being edited via user |
| 36 | 36 | interaction. This normally happens via selecting a file to |
| 37 | 37 | load. The event detail is the fossil.page object, not the current |
| | @@ -63,17 +63,14 @@ |
| 63 | 63 | ); |
| 64 | 64 | */ |
| 65 | 65 | const E = (s)=>document.querySelector(s), |
| 66 | 66 | D = F.dom, |
| 67 | 67 | P = F.page; |
| 68 | | - |
| 69 | 68 | P.config = { |
| 70 | | - /* Symbolic markers to denote certain edit state. */ |
| 71 | | - editStateMarkers: { |
| 72 | | - isNew: '[+]', |
| 73 | | - isModified: '[*]' |
| 74 | | - } |
| 69 | + /* Max number of locally-edited pages to stash, after which we |
| 70 | + drop the least-recently used. */ |
| 71 | + defaultMaxStashSize: 10 |
| 75 | 72 | }; |
| 76 | 73 | |
| 77 | 74 | /** |
| 78 | 75 | $stash is an internal-use-only object for managing "stashed" |
| 79 | 76 | local edits, to help avoid that users accidentally lose content |
| | @@ -269,11 +266,11 @@ |
| 269 | 266 | } |
| 270 | 267 | }; |
| 271 | 268 | |
| 272 | 269 | /** Internal helper to get an edit status indicator for the given winfo object. */ |
| 273 | 270 | const getEditMarker = function f(winfo, textOnly){ |
| 274 | | - const esm = P.config.editStateMarkers; |
| 271 | + const esm = F.config.editStateMarkers; |
| 275 | 272 | if(1===winfo){ /* force is-new */ |
| 276 | 273 | return textOnly ? esm.isNew : |
| 277 | 274 | D.addClass(D.append(D.span(),esm.isNew), 'is-new'); |
| 278 | 275 | }else if(2===winfo){ /* force is-modified */ |
| 279 | 276 | return textOnly ? esm.isModified : |
| | @@ -569,14 +566,120 @@ |
| 569 | 566 | btn.addEventListener('click', ()=>this.loadList(), false); |
| 570 | 567 | this.loadList(); |
| 571 | 568 | const onSelect = (e)=>P.loadPage(e.target.value); |
| 572 | 569 | sel.addEventListener('change', onSelect, false); |
| 573 | 570 | sel.addEventListener('dblclick', onSelect, false); |
| 574 | | - F.page.addEventListener('wiki-stash-updated', ()=>this._refreshStashMarks()); |
| 571 | + F.page.addEventListener('wiki-stash-updated', ()=>{ |
| 572 | + if(P.winfo) this._refreshStashMarks(); |
| 573 | + else this._rebuildList(); |
| 574 | + }); |
| 575 | + delete this.init; |
| 576 | + } |
| 577 | + }; |
| 578 | + |
| 579 | + /** |
| 580 | + Widget for listing and selecting $stash entries. |
| 581 | + */ |
| 582 | + P.stashWidget = { |
| 583 | + e:{/*DOM element(s)*/}, |
| 584 | + init: function(domInsertPoint/*insert widget BEFORE this element*/){ |
| 585 | + const wrapper = D.addClass( |
| 586 | + D.attr(D.div(),'id','wikiedit-stash-selector'), |
| 587 | + 'input-with-label' |
| 588 | + ); |
| 589 | + const sel = this.e.select = D.select(); |
| 590 | + const btnClear = this.e.btnClear |
| 591 | + = D.addClass(D.button("Clear"),'hidden'); |
| 592 | + D.append(wrapper, "Local edits (", |
| 593 | + D.append(D.code(), |
| 594 | + F.storage.storageImplName()), |
| 595 | + "):", |
| 596 | + sel, btnClear); |
| 597 | + D.attr(wrapper, "title", [ |
| 598 | + 'Locally-edited wiki pages. Timestamps are the last local edit time.', |
| 599 | + 'Only the',P.config.defaultMaxStashSize,'most recent pages', |
| 600 | + 'are retained. Saving or reloading a file removes it from this list.' |
| 601 | + ].join(' ')); |
| 602 | + D.option(D.disable(sel), "(empty)"); |
| 603 | + P.addEventListener('wiki-stash-updated',(e)=>this.updateList(e.detail)); |
| 604 | + P.addEventListener('wiki-page-loaded',(e)=>this.updateList($stash, e.detail)); |
| 605 | + sel.addEventListener('change',function(e){ |
| 606 | + const opt = this.selectedOptions[0]; |
| 607 | + if(opt && opt._winfo) P.loadPage(opt._winfo); |
| 608 | + }); |
| 609 | + F.confirmer(btnClear, { |
| 610 | + confirmText: "REALLY delete ALL local edits?", |
| 611 | + onconfirm: (e)=>P.clearStash(), |
| 612 | + ticks: F.config.confirmerButtonTicks |
| 613 | + }); |
| 614 | + if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/ |
| 615 | + D.append(wrapper, D.append( |
| 616 | + D.addClass(D.span(),'warning'), |
| 617 | + "Warning: persistent storage is not available, "+ |
| 618 | + "so uncomitted edits will not survive a page reload." |
| 619 | + )); |
| 620 | + } |
| 621 | + domInsertPoint.parentNode.insertBefore(wrapper, domInsertPoint); |
| 622 | + $stash._fireStashEvent(/*read the page-load-time stash*/); |
| 575 | 623 | delete this.init; |
| 624 | + }, |
| 625 | + /** |
| 626 | + Regenerates the edit selection list. |
| 627 | + */ |
| 628 | + updateList: function f(stasher,theWinfo){ |
| 629 | + console.debug("updateList()",arguments); |
| 630 | + if(!f.compare){ |
| 631 | + const cmpBase = (l,r)=>l<r ? -1 : (l===r ? 0 : 1); |
| 632 | + f.compare = (l,r)=>cmpBase(l.name, r.name); |
| 633 | + f.rxZ = /\.\d+Z$/ /* ms and 'Z' part of date string */; |
| 634 | + const pad=(x)=>(''+x).length>1 ? x : '0'+x; |
| 635 | + f.timestring = function ff(d){ |
| 636 | + return [ |
| 637 | + d.getFullYear(),'-',pad(d.getMonth()+1/*sigh*/),'-',pad(d.getDate()), |
| 638 | + '@',pad(d.getHours()),':',pad(d.getMinutes()) |
| 639 | + ].join(''); |
| 640 | + }; |
| 641 | + } |
| 642 | + const index = stasher.getIndex(), ilist = []; |
| 643 | + Object.keys(index).forEach((winfo)=>{ |
| 644 | + ilist.push(index[winfo]); |
| 645 | + }); |
| 646 | + const self = this; |
| 647 | + D.clearElement(this.e.select); |
| 648 | + if(0===ilist.length){ |
| 649 | + D.addClass(this.e.btnClear, 'hidden'); |
| 650 | + D.option(D.disable(this.e.select),"No local edits"); |
| 651 | + return; |
| 652 | + } |
| 653 | + D.enable(this.e.select); |
| 654 | + if(false){ |
| 655 | + /* The problem with this Clear button is that it allows the user |
| 656 | + to nuke a non-empty newly-added page without the failsafe confirmation |
| 657 | + we have if they use P.e.btnReload. Not yet sure how best to resolve that, |
| 658 | + so we'll leave the button hidden for the time being. */ |
| 659 | + D.removeClass(this.e.btnClear, 'hidden'); |
| 660 | + } |
| 661 | + D.disable(D.option(this.e.select,0,"Select a local edit...")); |
| 662 | + const currentFinfo = theWinfo || P.winfo || {}; |
| 663 | + ilist.sort(f.compare).forEach(function(winfo,n){ |
| 664 | + const key = stasher.indexKey(winfo), |
| 665 | + rev = winfo.version || ''; |
| 666 | + const opt = D.option( |
| 667 | + self.e.select, n+1/*value is (almost) irrelevant*/, |
| 668 | + [winfo.name, |
| 669 | + rev ? ' ['+F.hashDigits(rev, 6)+']' : ' [new/local]', |
| 670 | + ' @ ', |
| 671 | + f.timestring(new Date(winfo.stashTime)) |
| 672 | + ].join('') |
| 673 | + ); |
| 674 | + opt._winfo = winfo; |
| 675 | + if(0===f.compare(currentFinfo, winfo)){ |
| 676 | + D.attr(opt, 'selected', true); |
| 677 | + } |
| 678 | + }); |
| 576 | 679 | } |
| 577 | | - }; |
| 680 | + }/*P.stashWidget*/; |
| 578 | 681 | |
| 579 | 682 | /** |
| 580 | 683 | Keep track of how many in-flight AJAX requests there are so we |
| 581 | 684 | can disable input elements while any are pending. For |
| 582 | 685 | simplicity's sake we simply disable ALL OF IT while any AJAX is |
| | @@ -644,17 +747,17 @@ |
| 644 | 747 | misc: E('#wikiedit-tab-misc') |
| 645 | 748 | //commit: E('#wikiedit-tab-commit') |
| 646 | 749 | } |
| 647 | 750 | }; |
| 648 | 751 | P.tabs = new fossil.TabManager(D.clearElement(P.e.tabContainer)); |
| 649 | | - P.tabs.e.container.insertBefore(P.e.editStatus, P.tabs.e.tabs); |
| 650 | 752 | P.tabs.e.container.insertBefore( |
| 651 | 753 | /* Move the status bar between the tab buttons and |
| 652 | 754 | tab panels. Seems to be the best fit in terms of |
| 653 | 755 | functionality and visibility. */ |
| 654 | 756 | E('#fossil-status-bar'), P.tabs.e.tabs |
| 655 | 757 | ); |
| 758 | + P.tabs.e.container.insertBefore(P.e.editStatus, P.tabs.e.tabs); |
| 656 | 759 | P.tabs.addEventListener( |
| 657 | 760 | /* Set up some before-switch-to tab event tasks... */ |
| 658 | 761 | 'before-switch-to', function(ev){ |
| 659 | 762 | const theTab = ev.detail, btnSlot = theTab.querySelector('.save-button-slot'); |
| 660 | 763 | if(btnSlot){ |
| | @@ -733,11 +836,11 @@ |
| 733 | 836 | delete P.winfo; |
| 734 | 837 | P.updatePageTitle(); |
| 735 | 838 | F.message("Discarded new page ["+w.name+"]."); |
| 736 | 839 | } |
| 737 | 840 | }, |
| 738 | | - ticks: 3 |
| 841 | + ticks: F.config.confirmerButtonTicks |
| 739 | 842 | }); |
| 740 | 843 | F.confirmer(P.e.btnSave, { |
| 741 | 844 | confirmText: "Really save changes?", |
| 742 | 845 | onconfirm: function(e){ |
| 743 | 846 | const w = P.winfo; |
| | @@ -745,11 +848,11 @@ |
| 745 | 848 | F.error("No page loaded."); |
| 746 | 849 | return; |
| 747 | 850 | } |
| 748 | 851 | P.save(); |
| 749 | 852 | }, |
| 750 | | - ticks: 3 |
| 853 | + ticks: F.config.confirmerButtonTicks |
| 751 | 854 | }); |
| 752 | 855 | |
| 753 | 856 | P.e.taEditor.addEventListener( |
| 754 | 857 | 'change', ()=>P.stashContentChange(), false |
| 755 | 858 | ); |
| | @@ -796,16 +899,27 @@ |
| 796 | 899 | (e)=>{ |
| 797 | 900 | D.clearElement(P.e.diffTarget, P.e.previewTarget); |
| 798 | 901 | // TODO: replace preview with new content |
| 799 | 902 | } |
| 800 | 903 | ); |
| 801 | | - WikiList.init( P.e.tabs.pageList.firstElementChild ); |
| 904 | + P.addEventListener('wiki-stash-updated',function(){ |
| 905 | + /* MUST come before WikiList.init() and P.stashWidget.init() so |
| 906 | + that interwoven event handlers get called in the right |
| 907 | + order. */ |
| 908 | + if(P.winfo && !P.winfo.version && !$stash.getWinfo(P.winfo)){ |
| 909 | + // New local page was removed. |
| 910 | + delete P.winfo; |
| 911 | + P.wikiContent(''); |
| 912 | + P.updatePageTitle(); |
| 913 | + } |
| 914 | + P.updateSaveButton(); |
| 915 | + }).updatePageTitle().updateSaveButton(); |
| 916 | + |
| 802 | 917 | P.addEventListener( |
| 803 | 918 | // Update various state on wiki page load |
| 804 | 919 | 'wiki-page-loaded', |
| 805 | 920 | function(ev){ |
| 806 | | - delete P.winfo; |
| 807 | 921 | const winfo = ev.detail; |
| 808 | 922 | P.winfo = winfo; |
| 809 | 923 | P.previewNeedsUpdate = true; |
| 810 | 924 | P.e.selectMimetype.value = winfo.mimetype; |
| 811 | 925 | P.tabs.switchToTab(P.e.tabs.content); |
| | @@ -816,12 +930,13 @@ |
| 816 | 930 | } |
| 817 | 931 | P.updatePageTitle(); |
| 818 | 932 | }, |
| 819 | 933 | false |
| 820 | 934 | ); |
| 821 | | - P.addEventListener('wiki-stash-updated', ()=>P.updateSaveButton()) |
| 822 | | - .updatePageTitle().updateSaveButton(); |
| 935 | + /* These init()s need to come after P's event handlers are registered */ |
| 936 | + WikiList.init( P.e.tabs.pageList.firstElementChild ); |
| 937 | + P.stashWidget.init(P.e.tabs.content.lastElementChild); |
| 823 | 938 | }/*F.onPageLoad()*/); |
| 824 | 939 | |
| 825 | 940 | /** |
| 826 | 941 | Returns true if fossil.page.winfo is set, indicating that a page |
| 827 | 942 | has been loaded, else it reports an error and returns false. |
| | @@ -845,17 +960,17 @@ |
| 845 | 960 | if(!wi){ |
| 846 | 961 | D.append(f.eName, '(no page loaded)'); |
| 847 | 962 | return; |
| 848 | 963 | } |
| 849 | 964 | var marker = getEditMarker(wi, false); |
| 850 | | - D.append(f.eName,marker,wi.name,); |
| 965 | + D.append(f.eName,marker,wi.name); |
| 851 | 966 | if(wi.version){ |
| 852 | 967 | D.append( |
| 853 | 968 | f.eLinks, |
| 854 | | - D.a(F.repoUrl('whistory',{name:wi.name}),'[history]'), |
| 855 | | - D.a(F.repoUrl('attachlist',{page:wi.name}),"[attachments]"), |
| 856 | | - D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "[attach]") |
| 969 | + D.a(F.repoUrl('whistory',{name:wi.name}),'history'), |
| 970 | + D.a(F.repoUrl('attachlist',{page:wi.name}),"attachments"), |
| 971 | + D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "attach") |
| 857 | 972 | ); |
| 858 | 973 | } |
| 859 | 974 | }; |
| 860 | 975 | |
| 861 | 976 | /** |
| | @@ -978,11 +1093,13 @@ |
| 978 | 1093 | }else if(1===arguments.length && 'string' !== typeof name){ |
| 979 | 1094 | /* Assume winfo-like object */ |
| 980 | 1095 | const arg = arguments[0]; |
| 981 | 1096 | name = arg.name; |
| 982 | 1097 | } |
| 983 | | - const onload = (r)=>this.dispatchEvent('wiki-page-loaded', r); |
| 1098 | + const onload = (r)=>{ |
| 1099 | + this.dispatchEvent('wiki-page-loaded', r); |
| 1100 | + }; |
| 984 | 1101 | const stashWinfo = this.getStashedWinfo({name: name}); |
| 985 | 1102 | if(stashWinfo){ // fake a response from the stash... |
| 986 | 1103 | F.message("Fetched from the local-edit storage:", stashWinfo.name); |
| 987 | 1104 | onload({ |
| 988 | 1105 | name: stashWinfo.name, |
| 989 | 1106 | |