Fossil SCM

Teach the Edit and Reply buttons in the forum to clearly show when a corresponding local draft edit is available.

stephan 2026-06-09 22:39 UTC forum-editor-2026
Commit 04e8708769b4a748e5be1f27cddcdc4a7cc741b1bda2516e33de97c087b56f58
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -734,11 +734,11 @@
734734
if( o && (!days || (o.mtime+age < now)) ){
735735
F.storage.remove(k);
736736
}
737737
};
738738
if( key instanceof RegExp ){
739
- for(const k of F.storage.shortKeys().filter(v=>key.test(v))){
739
+ for(const k of F.storage.keys(false).filter(v=>key.test(v))){
740740
check(k);
741741
}
742742
}else{
743743
check(key);
744744
}
@@ -1019,11 +1019,10 @@
10191019
//console.debug("ondiscard/onsubmit", fpe, artifact);
10201020
if( response/*onsubmit()*/ ){
10211021
window.location = F.repoUrl('forumpost/'+response.uuid);
10221022
fpe.close();
10231023
}else{/*ondiscard()*/
1024
- eBtnReply.classList.remove('draft');
10251024
}
10261025
};
10271026
const fpe = new F.ForumPostEditor({
10281027
hiddenFields: form.querySelectorAll(
10291028
'input[type=hidden][name=csrf]'
@@ -1064,11 +1063,10 @@
10641063
restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
10651064
window.location = F.repoUrl('forumpost/'+response.uuid);
10661065
}
10671066
}else{
10681067
/*ondiscard()*/
1069
- eBtnEdit.classList.remove('draft');
10701068
restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
10711069
}
10721070
};
10731071
const eStatusSelect = ePost.querySelector(
10741072
':scope > fieldset.forum-status-selection select[name=status]'
@@ -1101,27 +1099,29 @@
11011099
console.warn("Unexpected missing fpid", eThePost);
11021100
return;
11031101
}
11041102
11051103
const checkButtonForDraft = (draftKeyPrefix, eBtn)=>{
1106
- if( 1 ){
1107
- /* 2026-06-09: this is currently disabled because it's
1108
- much of the solution but not all of it. Still to solve
1109
- is how to tag/untag these elements as local drafts are
1110
- added/removed during this page's lifetime.
1111
- ForumPostEditor does not have access to these buttons
1112
- so can't flag them. We may need to add events to
1113
- F.storage and monitor those.
1114
- */
1115
- return;
1116
- }
1104
+ /* If a draft is found associated with eThePost, mark eBtn
1105
+ as a draft and set up storage event listeners to update
1106
+ the button as new drafts come and go. */
11171107
const fpid = eThePost.dataset.fpid;
11181108
const fEditHead = eThePost.dataset.fedithead;
11191109
const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid);
11201110
if( F.storage.contains(draftKey) ){
11211111
eBtn.classList.add('draft');
11221112
}
1113
+ F.storage.addEventListener('set', ({detail})=>{
1114
+ if( draftKey === detail.key ){
1115
+ eBtn.classList.add('draft');
1116
+ }
1117
+ });
1118
+ F.storage.addEventListener('remove', ({detail})=>{
1119
+ if( draftKey === detail.key ){
1120
+ eBtn.classList.remove('draft');
1121
+ }
1122
+ });
11231123
};
11241124
/* Replace the Reply and Edit buttons with ones which will activate
11251125
a ForumPostEditor. */
11261126
const btnReply = form.querySelector('input[type=submit][name=reply]');
11271127
if( btnReply ){
11281128
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -734,11 +734,11 @@
734 if( o && (!days || (o.mtime+age < now)) ){
735 F.storage.remove(k);
736 }
737 };
738 if( key instanceof RegExp ){
739 for(const k of F.storage.shortKeys().filter(v=>key.test(v))){
740 check(k);
741 }
742 }else{
743 check(key);
744 }
@@ -1019,11 +1019,10 @@
1019 //console.debug("ondiscard/onsubmit", fpe, artifact);
1020 if( response/*onsubmit()*/ ){
1021 window.location = F.repoUrl('forumpost/'+response.uuid);
1022 fpe.close();
1023 }else{/*ondiscard()*/
1024 eBtnReply.classList.remove('draft');
1025 }
1026 };
1027 const fpe = new F.ForumPostEditor({
1028 hiddenFields: form.querySelectorAll(
1029 'input[type=hidden][name=csrf]'
@@ -1064,11 +1063,10 @@
1064 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1065 window.location = F.repoUrl('forumpost/'+response.uuid);
1066 }
1067 }else{
1068 /*ondiscard()*/
1069 eBtnEdit.classList.remove('draft');
1070 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1071 }
1072 };
1073 const eStatusSelect = ePost.querySelector(
1074 ':scope > fieldset.forum-status-selection select[name=status]'
@@ -1101,27 +1099,29 @@
1101 console.warn("Unexpected missing fpid", eThePost);
1102 return;
1103 }
1104
1105 const checkButtonForDraft = (draftKeyPrefix, eBtn)=>{
1106 if( 1 ){
1107 /* 2026-06-09: this is currently disabled because it's
1108 much of the solution but not all of it. Still to solve
1109 is how to tag/untag these elements as local drafts are
1110 added/removed during this page's lifetime.
1111 ForumPostEditor does not have access to these buttons
1112 so can't flag them. We may need to add events to
1113 F.storage and monitor those.
1114 */
1115 return;
1116 }
1117 const fpid = eThePost.dataset.fpid;
1118 const fEditHead = eThePost.dataset.fedithead;
1119 const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid);
1120 if( F.storage.contains(draftKey) ){
1121 eBtn.classList.add('draft');
1122 }
 
 
 
 
 
 
 
 
 
 
1123 };
1124 /* Replace the Reply and Edit buttons with ones which will activate
1125 a ForumPostEditor. */
1126 const btnReply = form.querySelector('input[type=submit][name=reply]');
1127 if( btnReply ){
1128
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -734,11 +734,11 @@
734 if( o && (!days || (o.mtime+age < now)) ){
735 F.storage.remove(k);
736 }
737 };
738 if( key instanceof RegExp ){
739 for(const k of F.storage.keys(false).filter(v=>key.test(v))){
740 check(k);
741 }
742 }else{
743 check(key);
744 }
@@ -1019,11 +1019,10 @@
1019 //console.debug("ondiscard/onsubmit", fpe, artifact);
1020 if( response/*onsubmit()*/ ){
1021 window.location = F.repoUrl('forumpost/'+response.uuid);
1022 fpe.close();
1023 }else{/*ondiscard()*/
 
1024 }
1025 };
1026 const fpe = new F.ForumPostEditor({
1027 hiddenFields: form.querySelectorAll(
1028 'input[type=hidden][name=csrf]'
@@ -1064,11 +1063,10 @@
1063 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1064 window.location = F.repoUrl('forumpost/'+response.uuid);
1065 }
1066 }else{
1067 /*ondiscard()*/
 
1068 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1069 }
1070 };
1071 const eStatusSelect = ePost.querySelector(
1072 ':scope > fieldset.forum-status-selection select[name=status]'
@@ -1101,27 +1099,29 @@
1099 console.warn("Unexpected missing fpid", eThePost);
1100 return;
1101 }
1102
1103 const checkButtonForDraft = (draftKeyPrefix, eBtn)=>{
1104 /* If a draft is found associated with eThePost, mark eBtn
1105 as a draft and set up storage event listeners to update
1106 the button as new drafts come and go. */
 
 
 
 
 
 
 
 
1107 const fpid = eThePost.dataset.fpid;
1108 const fEditHead = eThePost.dataset.fedithead;
1109 const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid);
1110 if( F.storage.contains(draftKey) ){
1111 eBtn.classList.add('draft');
1112 }
1113 F.storage.addEventListener('set', ({detail})=>{
1114 if( draftKey === detail.key ){
1115 eBtn.classList.add('draft');
1116 }
1117 });
1118 F.storage.addEventListener('remove', ({detail})=>{
1119 if( draftKey === detail.key ){
1120 eBtn.classList.remove('draft');
1121 }
1122 });
1123 };
1124 /* Replace the Reply and Edit buttons with ones which will activate
1125 a ForumPostEditor. */
1126 const btnReply = form.querySelector('input[type=submit][name=reply]');
1127 if( btnReply ){
1128
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -82,24 +82,53 @@
8282
)+'::' : (
8383
'' /* transient storage */
8484
)
8585
);
8686
87
+ /**
88
+ Proxy for custom events. Created on demand.
89
+ */
90
+ let events;
8791
/**
8892
A proxy for localStorage or sessionStorage or a
8993
page-instance-local proxy, if neither one is availble.
9094
9195
Which exact storage implementation is uses is unspecified, and
9296
apps must not rely on it.
9397
*/
9498
F.storage = {
9599
storageKeyPrefix: storageKeyPrefix,
100
+ addEventListener(...args){
101
+ events ??= new EventTarget()
102
+ return events.addEventListener(...args);
103
+ },
104
+ removeEventListener(...args){
105
+ events ??= new EventTarget()
106
+ return events.removeEventListener(...args);
107
+ },
96108
/** Sets the storage key k to value v, implicitly converting
97
- it to a string. */
98
- set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
109
+ it to a string.
110
+
111
+ Fires a 'set' CustomEvent with a detail value in the form
112
+ {key, value} with the new value.
113
+ */
114
+ set: (k,v)=>{
115
+ $storage.setItem(storageKeyPrefix+k,v);
116
+ if( events ){
117
+ events.dispatchEvent(
118
+ new CustomEvent('set',{
119
+ detail: F.nu({
120
+ key: k, value: v
121
+ })
122
+ })
123
+ );
124
+ }
125
+ },
99126
/** Sets storage key k to JSON.stringify(v). */
100
- setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
127
+ setJSON: function(k,v){
128
+ return this.set(k,JSON.stringify(v));
129
+ },
101130
/** Returns the value for the given storage key, or
102131
dflt if the key is not found in the storage. */
103132
get: (k,dflt)=>$storageHolder.hasOwnProperty(
104133
storageKeyPrefix+k
105134
) ? $storage.getItem(storageKeyPrefix+k) : dflt,
@@ -121,29 +150,47 @@
121150
catch(e){return dflt}
122151
},
123152
/** Returns true if the storage contains the given key,
124153
else false. */
125154
contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
126
- /** Removes the given key from the storage. Returns this. */
155
+ /**
156
+ Removes the given key from the storage. Returns this.
157
+
158
+ Fires a 'remove' CustomEvent with a detail value in the form
159
+ {key}.
160
+ */
127161
remove: function(k){
128
- $storage.removeItem(storageKeyPrefix+k);
162
+ const kk = storageKeyPrefix+k;
163
+ if( events ){
164
+ const had = $storageHolder.hasOwnProperty(kk)
165
+ $storage.removeItem(kk);
166
+ if( had ){
167
+ events.dispatchEvent(
168
+ new CustomEvent('remove',{
169
+ detail: F.nu({key: k})
170
+ })
171
+ );
172
+ }
173
+ }else{
174
+ $storage.removeItem(kk);
175
+ }
129176
return this;
130177
},
131178
/** Clears ALL keys from the storage. Returns this. */
132179
clear: function(){
133180
this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
134181
return this;
135182
},
136
- /** Returns an array of all keys currently in the storage. These
137
- include the storage key prefix. */
138
- keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
139
- /**
140
- Like this.keys() but returns the keys shorn of the key prefix.
141
- */
142
- shortKeys: function(){
183
+ /** Returns an array of all keys currently in the storage. If full
184
+ is true then the keys include the storage key prefix, else
185
+ they don't. It should default to false but does not for
186
+ historical compatibility. */
187
+ keys: (full=true)=>{
188
+ const li = Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix));
189
+ if( full ) return li;
143190
const n = this.storageKeyPrefix.length;
144
- return this.keys().map(v=>v.substring(n));
191
+ return li.map(v=>v.substring(n));
145192
},
146193
/** Returns true if this storage is transient (only available
147194
until the page is reloaded), indicating that fileStorage
148195
and sessionStorage are unavailable. */
149196
isTransient: ()=>$storageHolder!==$storage,
150197
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -82,24 +82,53 @@
82 )+'::' : (
83 '' /* transient storage */
84 )
85 );
86
 
 
 
 
87 /**
88 A proxy for localStorage or sessionStorage or a
89 page-instance-local proxy, if neither one is availble.
90
91 Which exact storage implementation is uses is unspecified, and
92 apps must not rely on it.
93 */
94 F.storage = {
95 storageKeyPrefix: storageKeyPrefix,
 
 
 
 
 
 
 
 
96 /** Sets the storage key k to value v, implicitly converting
97 it to a string. */
98 set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99 /** Sets storage key k to JSON.stringify(v). */
100 setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
 
 
101 /** Returns the value for the given storage key, or
102 dflt if the key is not found in the storage. */
103 get: (k,dflt)=>$storageHolder.hasOwnProperty(
104 storageKeyPrefix+k
105 ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
@@ -121,29 +150,47 @@
121 catch(e){return dflt}
122 },
123 /** Returns true if the storage contains the given key,
124 else false. */
125 contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
126 /** Removes the given key from the storage. Returns this. */
 
 
 
 
 
127 remove: function(k){
128 $storage.removeItem(storageKeyPrefix+k);
 
 
 
 
 
 
 
 
 
 
 
 
 
129 return this;
130 },
131 /** Clears ALL keys from the storage. Returns this. */
132 clear: function(){
133 this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
134 return this;
135 },
136 /** Returns an array of all keys currently in the storage. These
137 include the storage key prefix. */
138 keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
139 /**
140 Like this.keys() but returns the keys shorn of the key prefix.
141 */
142 shortKeys: function(){
143 const n = this.storageKeyPrefix.length;
144 return this.keys().map(v=>v.substring(n));
145 },
146 /** Returns true if this storage is transient (only available
147 until the page is reloaded), indicating that fileStorage
148 and sessionStorage are unavailable. */
149 isTransient: ()=>$storageHolder!==$storage,
150
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -82,24 +82,53 @@
82 )+'::' : (
83 '' /* transient storage */
84 )
85 );
86
87 /**
88 Proxy for custom events. Created on demand.
89 */
90 let events;
91 /**
92 A proxy for localStorage or sessionStorage or a
93 page-instance-local proxy, if neither one is availble.
94
95 Which exact storage implementation is uses is unspecified, and
96 apps must not rely on it.
97 */
98 F.storage = {
99 storageKeyPrefix: storageKeyPrefix,
100 addEventListener(...args){
101 events ??= new EventTarget()
102 return events.addEventListener(...args);
103 },
104 removeEventListener(...args){
105 events ??= new EventTarget()
106 return events.removeEventListener(...args);
107 },
108 /** Sets the storage key k to value v, implicitly converting
109 it to a string.
110
111 Fires a 'set' CustomEvent with a detail value in the form
112 {key, value} with the new value.
113 */
114 set: (k,v)=>{
115 $storage.setItem(storageKeyPrefix+k,v);
116 if( events ){
117 events.dispatchEvent(
118 new CustomEvent('set',{
119 detail: F.nu({
120 key: k, value: v
121 })
122 })
123 );
124 }
125 },
126 /** Sets storage key k to JSON.stringify(v). */
127 setJSON: function(k,v){
128 return this.set(k,JSON.stringify(v));
129 },
130 /** Returns the value for the given storage key, or
131 dflt if the key is not found in the storage. */
132 get: (k,dflt)=>$storageHolder.hasOwnProperty(
133 storageKeyPrefix+k
134 ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
@@ -121,29 +150,47 @@
150 catch(e){return dflt}
151 },
152 /** Returns true if the storage contains the given key,
153 else false. */
154 contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
155 /**
156 Removes the given key from the storage. Returns this.
157
158 Fires a 'remove' CustomEvent with a detail value in the form
159 {key}.
160 */
161 remove: function(k){
162 const kk = storageKeyPrefix+k;
163 if( events ){
164 const had = $storageHolder.hasOwnProperty(kk)
165 $storage.removeItem(kk);
166 if( had ){
167 events.dispatchEvent(
168 new CustomEvent('remove',{
169 detail: F.nu({key: k})
170 })
171 );
172 }
173 }else{
174 $storage.removeItem(kk);
175 }
176 return this;
177 },
178 /** Clears ALL keys from the storage. Returns this. */
179 clear: function(){
180 this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
181 return this;
182 },
183 /** Returns an array of all keys currently in the storage. If full
184 is true then the keys include the storage key prefix, else
185 they don't. It should default to false but does not for
186 historical compatibility. */
187 keys: (full=true)=>{
188 const li = Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix));
189 if( full ) return li;
190 const n = this.storageKeyPrefix.length;
191 return li.map(v=>v.substring(n));
192 },
193 /** Returns true if this storage is transient (only available
194 until the page is reloaded), indicating that fileStorage
195 and sessionStorage are unavailable. */
196 isTransient: ()=>$storageHolder!==$storage,
197

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button