Fossil SCM
Teach the Edit and Reply buttons in the forum to clearly show when a corresponding local draft edit is available.
Commit
04e8708769b4a748e5be1f27cddcdc4a7cc741b1bda2516e33de97c087b56f58
Parent
bee75fb8b22ff89…
2 files changed
+14
-14
+60
-13
+14
-14
| --- src/fossil.page.forumpost.js | ||
| +++ src/fossil.page.forumpost.js | ||
| @@ -734,11 +734,11 @@ | ||
| 734 | 734 | if( o && (!days || (o.mtime+age < now)) ){ |
| 735 | 735 | F.storage.remove(k); |
| 736 | 736 | } |
| 737 | 737 | }; |
| 738 | 738 | 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))){ | |
| 740 | 740 | check(k); |
| 741 | 741 | } |
| 742 | 742 | }else{ |
| 743 | 743 | check(key); |
| 744 | 744 | } |
| @@ -1019,11 +1019,10 @@ | ||
| 1019 | 1019 | //console.debug("ondiscard/onsubmit", fpe, artifact); |
| 1020 | 1020 | if( response/*onsubmit()*/ ){ |
| 1021 | 1021 | window.location = F.repoUrl('forumpost/'+response.uuid); |
| 1022 | 1022 | fpe.close(); |
| 1023 | 1023 | }else{/*ondiscard()*/ |
| 1024 | - eBtnReply.classList.remove('draft'); | |
| 1025 | 1024 | } |
| 1026 | 1025 | }; |
| 1027 | 1026 | const fpe = new F.ForumPostEditor({ |
| 1028 | 1027 | hiddenFields: form.querySelectorAll( |
| 1029 | 1028 | 'input[type=hidden][name=csrf]' |
| @@ -1064,11 +1063,10 @@ | ||
| 1064 | 1063 | restoreEditReplyElement(ePost, eBtnEdit, eToDisable); |
| 1065 | 1064 | window.location = F.repoUrl('forumpost/'+response.uuid); |
| 1066 | 1065 | } |
| 1067 | 1066 | }else{ |
| 1068 | 1067 | /*ondiscard()*/ |
| 1069 | - eBtnEdit.classList.remove('draft'); | |
| 1070 | 1068 | restoreEditReplyElement(ePost, eBtnEdit, eToDisable); |
| 1071 | 1069 | } |
| 1072 | 1070 | }; |
| 1073 | 1071 | const eStatusSelect = ePost.querySelector( |
| 1074 | 1072 | ':scope > fieldset.forum-status-selection select[name=status]' |
| @@ -1101,27 +1099,29 @@ | ||
| 1101 | 1099 | console.warn("Unexpected missing fpid", eThePost); |
| 1102 | 1100 | return; |
| 1103 | 1101 | } |
| 1104 | 1102 | |
| 1105 | 1103 | 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. */ | |
| 1117 | 1107 | const fpid = eThePost.dataset.fpid; |
| 1118 | 1108 | const fEditHead = eThePost.dataset.fedithead; |
| 1119 | 1109 | const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid); |
| 1120 | 1110 | if( F.storage.contains(draftKey) ){ |
| 1121 | 1111 | eBtn.classList.add('draft'); |
| 1122 | 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 | 1123 | }; |
| 1124 | 1124 | /* Replace the Reply and Edit buttons with ones which will activate |
| 1125 | 1125 | a ForumPostEditor. */ |
| 1126 | 1126 | const btnReply = form.querySelector('input[type=submit][name=reply]'); |
| 1127 | 1127 | if( btnReply ){ |
| 1128 | 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.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 |
+60
-13
| --- src/fossil.storage.js | ||
| +++ src/fossil.storage.js | ||
| @@ -82,24 +82,53 @@ | ||
| 82 | 82 | )+'::' : ( |
| 83 | 83 | '' /* transient storage */ |
| 84 | 84 | ) |
| 85 | 85 | ); |
| 86 | 86 | |
| 87 | + /** | |
| 88 | + Proxy for custom events. Created on demand. | |
| 89 | + */ | |
| 90 | + let events; | |
| 87 | 91 | /** |
| 88 | 92 | A proxy for localStorage or sessionStorage or a |
| 89 | 93 | page-instance-local proxy, if neither one is availble. |
| 90 | 94 | |
| 91 | 95 | Which exact storage implementation is uses is unspecified, and |
| 92 | 96 | apps must not rely on it. |
| 93 | 97 | */ |
| 94 | 98 | F.storage = { |
| 95 | 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 | + }, | |
| 96 | 108 | /** 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 | + }, | |
| 99 | 126 | /** 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 | + }, | |
| 101 | 130 | /** Returns the value for the given storage key, or |
| 102 | 131 | dflt if the key is not found in the storage. */ |
| 103 | 132 | get: (k,dflt)=>$storageHolder.hasOwnProperty( |
| 104 | 133 | storageKeyPrefix+k |
| 105 | 134 | ) ? $storage.getItem(storageKeyPrefix+k) : dflt, |
| @@ -121,29 +150,47 @@ | ||
| 121 | 150 | catch(e){return dflt} |
| 122 | 151 | }, |
| 123 | 152 | /** Returns true if the storage contains the given key, |
| 124 | 153 | else false. */ |
| 125 | 154 | 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 | + */ | |
| 127 | 161 | 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 | + } | |
| 129 | 176 | return this; |
| 130 | 177 | }, |
| 131 | 178 | /** Clears ALL keys from the storage. Returns this. */ |
| 132 | 179 | clear: function(){ |
| 133 | 180 | this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k)); |
| 134 | 181 | return this; |
| 135 | 182 | }, |
| 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; | |
| 143 | 190 | const n = this.storageKeyPrefix.length; |
| 144 | - return this.keys().map(v=>v.substring(n)); | |
| 191 | + return li.map(v=>v.substring(n)); | |
| 145 | 192 | }, |
| 146 | 193 | /** Returns true if this storage is transient (only available |
| 147 | 194 | until the page is reloaded), indicating that fileStorage |
| 148 | 195 | and sessionStorage are unavailable. */ |
| 149 | 196 | isTransient: ()=>$storageHolder!==$storage, |
| 150 | 197 |
| --- 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 |