Fossil SCM
In browsers which support Web Locks, do not allow the Edit or Reply buttons to function if an Edit resp. Reply is open for that same post in another tab. Instead, display an error to the user describing the problem. The Edit/Reply buttons will start working once the locking tab is closed. This lock is per draft, not per tab, so it is legal to edit different posts in different tabs or multiple posts in the same tab. In browsers without Web Locks support, most-recent-edit-wins applies.
Commit
e48fe702cac8249393ea96cbf9ba6809870286b73cd307e39fba18970de2f6aa
Parent
32539a2c0b42c20…
2 files changed
+121
-17
+12
-5
+121
-17
| --- src/fossil.page.forumpost.js | ||
| +++ src/fossil.page.forumpost.js | ||
| @@ -71,10 +71,15 @@ | ||
| 71 | 71 | opt.onsubmit[=function]: if set, this function is called |
| 72 | 72 | immediately after the post has been successfully saved, and |
| 73 | 73 | passed this object and a JSON-format response object from the |
| 74 | 74 | save request. It is generally then up to the caller to close() |
| 75 | 75 | this object and/or redirect to /forumpost/${arguments[1].uuid}. |
| 76 | + | |
| 77 | + opt.onclose[=function]: like opt.onsubmit, this function is | |
| 78 | + called when this.close() is called, and passed no arguments. | |
| 79 | + onclose() is called before the widget is removed from the dom | |
| 80 | + and _does not_ fire if it is not in the DOM. | |
| 76 | 81 | |
| 77 | 82 | opt.hiddenFields: an optional list of input elements to |
| 78 | 83 | incorporate into the form for requests which request the |
| 79 | 84 | preview or save the post. |
| 80 | 85 | |
| @@ -516,18 +521,19 @@ | ||
| 516 | 521 | D.append( |
| 517 | 522 | D.li(list), |
| 518 | 523 | "WARNING: draft edits are keyed on the ID of the message they ", |
| 519 | 524 | "are editing or responding to. Attempting to edit or reply to ", |
| 520 | 525 | "the same post from multiple tabs will cause the most-recently-edited ", |
| 521 | - "one to overwrite the draft slot for that post." | |
| 526 | + "one to overwrite the draft slot for that post. In browsers which support ", | |
| 527 | + "Web Locks, a second attempt to edit or reply to a post will be blocked ", | |
| 528 | + "and an error will be shown explaning the problme." | |
| 522 | 529 | ); |
| 523 | 530 | if( this.#e.status ){ |
| 524 | 531 | D.append( |
| 525 | 532 | D.li(list), |
| 526 | - "Tip: to change just the status, use the widget which appears in ", | |
| 527 | - "the post, not the editor. That will save only a single tag instead of ", | |
| 528 | - "a new edit of the post." | |
| 533 | + "Tip: changing just the status in the editor will change only that, ", | |
| 534 | + "not a whole new (but unedited) copy of the post." | |
| 529 | 535 | ); |
| 530 | 536 | } |
| 531 | 537 | eh.append(list); |
| 532 | 538 | } |
| 533 | 539 | |
| @@ -1064,13 +1070,68 @@ | ||
| 1064 | 1070 | delete eButton.dataset.originalLabel; |
| 1065 | 1071 | } |
| 1066 | 1072 | D.enable(eToDisable); |
| 1067 | 1073 | }; |
| 1068 | 1074 | |
| 1069 | - const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{ | |
| 1070 | - const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable); | |
| 1075 | + /** | |
| 1076 | + Reports an error regarding the forum post element | |
| 1077 | + ePost, appending each entry in msg to a wrapper | |
| 1078 | + element with the class | |
| 1079 | + */ | |
| 1080 | + const reportFPEError = (ePost,...msg)=>{ | |
| 1081 | + const e = D.addClass(D.p(), 'error'); | |
| 1082 | + e.append( | |
| 1083 | + ...msg, | |
| 1084 | + D.br(), | |
| 1085 | + D.button("Clear error", ()=>e.remove()) | |
| 1086 | + ); | |
| 1087 | + ePost.append(e); | |
| 1088 | + }; | |
| 1089 | + | |
| 1090 | + const replyClicked = async (form, ePost, eBtnReply, eToDisable)=>{ | |
| 1091 | + const fpid = ePost.dataset.fpid; | |
| 1071 | 1092 | const fEditHead = ePost.dataset.fedithead; |
| 1093 | + const draftKey = makeDraftKey( | |
| 1094 | + 'draft-reply', fEditHead | |
| 1095 | + /* The problem with firt as a key is that firt is not | |
| 1096 | + necessarily the root edit of that post, which is | |
| 1097 | + what we really want as a draft key so that the | |
| 1098 | + draft does not disapper if firt is later edited | |
| 1099 | + (giving us a new firt value here). */ | |
| 1100 | + || fpid | |
| 1101 | + ); | |
| 1102 | + const lockName = 'fossil-'+draftKey; | |
| 1103 | + let releaseLock; | |
| 1104 | + | |
| 1105 | + if( window.navigator.locks ){ | |
| 1106 | + releaseLock = await new Promise((resolve)=>{ | |
| 1107 | + window.navigator.locks.request( | |
| 1108 | + lockName, { ifAvailable: true }, async (lock) => { | |
| 1109 | + if( !lock ){ | |
| 1110 | + /*lock contention*/ | |
| 1111 | + resolve(null); | |
| 1112 | + return; | |
| 1113 | + } | |
| 1114 | + let release; | |
| 1115 | + const lockReleased = new Promise(res=>release=res); | |
| 1116 | + resolve(release); | |
| 1117 | + await lockReleased/*hold the lock open*/; | |
| 1118 | + }); | |
| 1119 | + }); | |
| 1120 | + if( !releaseLock ){ | |
| 1121 | + reportFPEError( | |
| 1122 | + ePost, | |
| 1123 | + "This post is actively being replied to ", | |
| 1124 | + "in another tab. To avoid losing edits, ", | |
| 1125 | + "it cannot be opened here until the locking ", | |
| 1126 | + "tab is closed." | |
| 1127 | + ); | |
| 1128 | + return; | |
| 1129 | + } | |
| 1130 | + } | |
| 1131 | + | |
| 1132 | + setupEditReplyElement(ePost, eBtnReply, eToDisable); | |
| 1072 | 1133 | eBtnReply.innerText = "Replying..."; |
| 1073 | 1134 | const ondone = (fpe, response)=>{ |
| 1074 | 1135 | /* onsubmit() and ondiscard() callback */ |
| 1075 | 1136 | restoreEditReplyElement(ePost, eBtnReply, eToDisable); |
| 1076 | 1137 | //console.debug("ondiscard/onsubmit", fpe, artifact); |
| @@ -1086,27 +1147,55 @@ | ||
| 1086 | 1147 | /* Do not inherit the fpid field, else this will become |
| 1087 | 1148 | an edit to that post rather than a response. */ |
| 1088 | 1149 | ), |
| 1089 | 1150 | ondiscard: ondone, |
| 1090 | 1151 | onsubmit: ondone, |
| 1152 | + onclose: ()=>{ | |
| 1153 | + if( releaseLock ){ | |
| 1154 | + releaseLock(); | |
| 1155 | + releaseLock = null; | |
| 1156 | + } | |
| 1157 | + }, | |
| 1091 | 1158 | inReplyTo: fpid, |
| 1092 | - draftKey: makeDraftKey('draft-reply', fEditHead | |
| 1093 | - /* The problem with firt as a key is that firt is not | |
| 1094 | - necessarily the root edit of that post, which is | |
| 1095 | - what we really want as a draft key so that the | |
| 1096 | - draft does not disapper if firt is later edited | |
| 1097 | - (giving us a new firt value here). */ | |
| 1098 | - || fpid | |
| 1099 | - ) | |
| 1159 | + draftKey | |
| 1100 | 1160 | }); |
| 1101 | 1161 | initFPEWidget(ePost, fpe); |
| 1102 | 1162 | }/*replyClicked()*/; |
| 1103 | 1163 | |
| 1104 | - const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{ | |
| 1105 | - const fpid = setupEditReplyElement(ePost, eBtnEdit, eToDisable); | |
| 1164 | + const editClicked = async (form, ePost, eBtnEdit, eToDisable)=>{ | |
| 1165 | + const fpid = ePost.dataset.fpid; | |
| 1106 | 1166 | const firt = ePost.dataset.firt; |
| 1107 | 1167 | const fEditHead = ePost.dataset.fedithead; |
| 1168 | + const draftKey = makeDraftKey('draft-forumedit', fEditHead || fpid); | |
| 1169 | + const lockName = 'fossil-'+draftKey; | |
| 1170 | + let releaseLock; | |
| 1171 | + if( navigator.locks ){ | |
| 1172 | + releaseLock = await new Promise((resolve) => { | |
| 1173 | + navigator.locks.request(lockName, {ifAvailable: true}, async (lock)=>{ | |
| 1174 | + if( !lock ){ | |
| 1175 | + resolve(null); | |
| 1176 | + return; | |
| 1177 | + } | |
| 1178 | + let release; | |
| 1179 | + const lockReleased = new Promise(res=>release=res); | |
| 1180 | + resolve(release); | |
| 1181 | + await lockReleased; | |
| 1182 | + }); | |
| 1183 | + }); | |
| 1184 | + | |
| 1185 | + if( !releaseLock ){ | |
| 1186 | + reportFPEError( | |
| 1187 | + ePost, | |
| 1188 | + "This post is actively being edited ", | |
| 1189 | + "in another tab. To avoid losing edits, ", | |
| 1190 | + "it cannot be opened here until the locking ", | |
| 1191 | + "tab is closed." | |
| 1192 | + ); | |
| 1193 | + return; | |
| 1194 | + } | |
| 1195 | + } | |
| 1196 | + setupEditReplyElement(ePost, eBtnEdit, eToDisable); | |
| 1108 | 1197 | eBtnEdit.innerText = "Editing..."; |
| 1109 | 1198 | fetchPost(fpid) |
| 1110 | 1199 | .then(artifact=>{ |
| 1111 | 1200 | const ondone = (fpe, response)=>{ |
| 1112 | 1201 | /* onsubmit() and ondiscard() callback */ |
| @@ -1131,16 +1220,31 @@ | ||
| 1131 | 1220 | |
| 1132 | 1221 | const fpe = new F.ForumPostEditor({ |
| 1133 | 1222 | hiddenFields: form.querySelectorAll('input[type=hidden]'), |
| 1134 | 1223 | ondiscard: ondone, |
| 1135 | 1224 | onsubmit: ondone, |
| 1136 | - draftKey: makeDraftKey('draft-forumedit', fEditHead || fpid), | |
| 1225 | + onclose: ()=>{ | |
| 1226 | + if(releaseLock){ | |
| 1227 | + releaseLock(); | |
| 1228 | + releaseLock = null; | |
| 1229 | + } | |
| 1230 | + }, | |
| 1231 | + draftKey, | |
| 1137 | 1232 | edit: artifact, |
| 1138 | 1233 | status: eStatusSelect?.value, |
| 1139 | 1234 | inReplyTo: firt |
| 1140 | 1235 | }); |
| 1141 | 1236 | initFPEWidget(ePost, fpe); |
| 1237 | + }) | |
| 1238 | + .catch(err=>{ | |
| 1239 | + if( releaseLock ){ | |
| 1240 | + releaseLock(); | |
| 1241 | + releaseLock = null; | |
| 1242 | + } | |
| 1243 | + restoreEditReplyElement(ePost, eBtnEdit, eToDisable); | |
| 1244 | + console.error("Error fetching post:", err); | |
| 1245 | + reportFPEError(ePost, "Error fetching post: ", err.message); | |
| 1142 | 1246 | }); |
| 1143 | 1247 | }/*editClicked()*/; |
| 1144 | 1248 | |
| 1145 | 1249 | document.body.querySelectorAll( |
| 1146 | 1250 | '.forumpost-single-controls > form' |
| 1147 | 1251 |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -71,10 +71,15 @@ | |
| 71 | opt.onsubmit[=function]: if set, this function is called |
| 72 | immediately after the post has been successfully saved, and |
| 73 | passed this object and a JSON-format response object from the |
| 74 | save request. It is generally then up to the caller to close() |
| 75 | this object and/or redirect to /forumpost/${arguments[1].uuid}. |
| 76 | |
| 77 | opt.hiddenFields: an optional list of input elements to |
| 78 | incorporate into the form for requests which request the |
| 79 | preview or save the post. |
| 80 | |
| @@ -516,18 +521,19 @@ | |
| 516 | D.append( |
| 517 | D.li(list), |
| 518 | "WARNING: draft edits are keyed on the ID of the message they ", |
| 519 | "are editing or responding to. Attempting to edit or reply to ", |
| 520 | "the same post from multiple tabs will cause the most-recently-edited ", |
| 521 | "one to overwrite the draft slot for that post." |
| 522 | ); |
| 523 | if( this.#e.status ){ |
| 524 | D.append( |
| 525 | D.li(list), |
| 526 | "Tip: to change just the status, use the widget which appears in ", |
| 527 | "the post, not the editor. That will save only a single tag instead of ", |
| 528 | "a new edit of the post." |
| 529 | ); |
| 530 | } |
| 531 | eh.append(list); |
| 532 | } |
| 533 | |
| @@ -1064,13 +1070,68 @@ | |
| 1064 | delete eButton.dataset.originalLabel; |
| 1065 | } |
| 1066 | D.enable(eToDisable); |
| 1067 | }; |
| 1068 | |
| 1069 | const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{ |
| 1070 | const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable); |
| 1071 | const fEditHead = ePost.dataset.fedithead; |
| 1072 | eBtnReply.innerText = "Replying..."; |
| 1073 | const ondone = (fpe, response)=>{ |
| 1074 | /* onsubmit() and ondiscard() callback */ |
| 1075 | restoreEditReplyElement(ePost, eBtnReply, eToDisable); |
| 1076 | //console.debug("ondiscard/onsubmit", fpe, artifact); |
| @@ -1086,27 +1147,55 @@ | |
| 1086 | /* Do not inherit the fpid field, else this will become |
| 1087 | an edit to that post rather than a response. */ |
| 1088 | ), |
| 1089 | ondiscard: ondone, |
| 1090 | onsubmit: ondone, |
| 1091 | inReplyTo: fpid, |
| 1092 | draftKey: makeDraftKey('draft-reply', fEditHead |
| 1093 | /* The problem with firt as a key is that firt is not |
| 1094 | necessarily the root edit of that post, which is |
| 1095 | what we really want as a draft key so that the |
| 1096 | draft does not disapper if firt is later edited |
| 1097 | (giving us a new firt value here). */ |
| 1098 | || fpid |
| 1099 | ) |
| 1100 | }); |
| 1101 | initFPEWidget(ePost, fpe); |
| 1102 | }/*replyClicked()*/; |
| 1103 | |
| 1104 | const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{ |
| 1105 | const fpid = setupEditReplyElement(ePost, eBtnEdit, eToDisable); |
| 1106 | const firt = ePost.dataset.firt; |
| 1107 | const fEditHead = ePost.dataset.fedithead; |
| 1108 | eBtnEdit.innerText = "Editing..."; |
| 1109 | fetchPost(fpid) |
| 1110 | .then(artifact=>{ |
| 1111 | const ondone = (fpe, response)=>{ |
| 1112 | /* onsubmit() and ondiscard() callback */ |
| @@ -1131,16 +1220,31 @@ | |
| 1131 | |
| 1132 | const fpe = new F.ForumPostEditor({ |
| 1133 | hiddenFields: form.querySelectorAll('input[type=hidden]'), |
| 1134 | ondiscard: ondone, |
| 1135 | onsubmit: ondone, |
| 1136 | draftKey: makeDraftKey('draft-forumedit', fEditHead || fpid), |
| 1137 | edit: artifact, |
| 1138 | status: eStatusSelect?.value, |
| 1139 | inReplyTo: firt |
| 1140 | }); |
| 1141 | initFPEWidget(ePost, fpe); |
| 1142 | }); |
| 1143 | }/*editClicked()*/; |
| 1144 | |
| 1145 | document.body.querySelectorAll( |
| 1146 | '.forumpost-single-controls > form' |
| 1147 |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -71,10 +71,15 @@ | |
| 71 | opt.onsubmit[=function]: if set, this function is called |
| 72 | immediately after the post has been successfully saved, and |
| 73 | passed this object and a JSON-format response object from the |
| 74 | save request. It is generally then up to the caller to close() |
| 75 | this object and/or redirect to /forumpost/${arguments[1].uuid}. |
| 76 | |
| 77 | opt.onclose[=function]: like opt.onsubmit, this function is |
| 78 | called when this.close() is called, and passed no arguments. |
| 79 | onclose() is called before the widget is removed from the dom |
| 80 | and _does not_ fire if it is not in the DOM. |
| 81 | |
| 82 | opt.hiddenFields: an optional list of input elements to |
| 83 | incorporate into the form for requests which request the |
| 84 | preview or save the post. |
| 85 | |
| @@ -516,18 +521,19 @@ | |
| 521 | D.append( |
| 522 | D.li(list), |
| 523 | "WARNING: draft edits are keyed on the ID of the message they ", |
| 524 | "are editing or responding to. Attempting to edit or reply to ", |
| 525 | "the same post from multiple tabs will cause the most-recently-edited ", |
| 526 | "one to overwrite the draft slot for that post. In browsers which support ", |
| 527 | "Web Locks, a second attempt to edit or reply to a post will be blocked ", |
| 528 | "and an error will be shown explaning the problme." |
| 529 | ); |
| 530 | if( this.#e.status ){ |
| 531 | D.append( |
| 532 | D.li(list), |
| 533 | "Tip: changing just the status in the editor will change only that, ", |
| 534 | "not a whole new (but unedited) copy of the post." |
| 535 | ); |
| 536 | } |
| 537 | eh.append(list); |
| 538 | } |
| 539 | |
| @@ -1064,13 +1070,68 @@ | |
| 1070 | delete eButton.dataset.originalLabel; |
| 1071 | } |
| 1072 | D.enable(eToDisable); |
| 1073 | }; |
| 1074 | |
| 1075 | /** |
| 1076 | Reports an error regarding the forum post element |
| 1077 | ePost, appending each entry in msg to a wrapper |
| 1078 | element with the class |
| 1079 | */ |
| 1080 | const reportFPEError = (ePost,...msg)=>{ |
| 1081 | const e = D.addClass(D.p(), 'error'); |
| 1082 | e.append( |
| 1083 | ...msg, |
| 1084 | D.br(), |
| 1085 | D.button("Clear error", ()=>e.remove()) |
| 1086 | ); |
| 1087 | ePost.append(e); |
| 1088 | }; |
| 1089 | |
| 1090 | const replyClicked = async (form, ePost, eBtnReply, eToDisable)=>{ |
| 1091 | const fpid = ePost.dataset.fpid; |
| 1092 | const fEditHead = ePost.dataset.fedithead; |
| 1093 | const draftKey = makeDraftKey( |
| 1094 | 'draft-reply', fEditHead |
| 1095 | /* The problem with firt as a key is that firt is not |
| 1096 | necessarily the root edit of that post, which is |
| 1097 | what we really want as a draft key so that the |
| 1098 | draft does not disapper if firt is later edited |
| 1099 | (giving us a new firt value here). */ |
| 1100 | || fpid |
| 1101 | ); |
| 1102 | const lockName = 'fossil-'+draftKey; |
| 1103 | let releaseLock; |
| 1104 | |
| 1105 | if( window.navigator.locks ){ |
| 1106 | releaseLock = await new Promise((resolve)=>{ |
| 1107 | window.navigator.locks.request( |
| 1108 | lockName, { ifAvailable: true }, async (lock) => { |
| 1109 | if( !lock ){ |
| 1110 | /*lock contention*/ |
| 1111 | resolve(null); |
| 1112 | return; |
| 1113 | } |
| 1114 | let release; |
| 1115 | const lockReleased = new Promise(res=>release=res); |
| 1116 | resolve(release); |
| 1117 | await lockReleased/*hold the lock open*/; |
| 1118 | }); |
| 1119 | }); |
| 1120 | if( !releaseLock ){ |
| 1121 | reportFPEError( |
| 1122 | ePost, |
| 1123 | "This post is actively being replied to ", |
| 1124 | "in another tab. To avoid losing edits, ", |
| 1125 | "it cannot be opened here until the locking ", |
| 1126 | "tab is closed." |
| 1127 | ); |
| 1128 | return; |
| 1129 | } |
| 1130 | } |
| 1131 | |
| 1132 | setupEditReplyElement(ePost, eBtnReply, eToDisable); |
| 1133 | eBtnReply.innerText = "Replying..."; |
| 1134 | const ondone = (fpe, response)=>{ |
| 1135 | /* onsubmit() and ondiscard() callback */ |
| 1136 | restoreEditReplyElement(ePost, eBtnReply, eToDisable); |
| 1137 | //console.debug("ondiscard/onsubmit", fpe, artifact); |
| @@ -1086,27 +1147,55 @@ | |
| 1147 | /* Do not inherit the fpid field, else this will become |
| 1148 | an edit to that post rather than a response. */ |
| 1149 | ), |
| 1150 | ondiscard: ondone, |
| 1151 | onsubmit: ondone, |
| 1152 | onclose: ()=>{ |
| 1153 | if( releaseLock ){ |
| 1154 | releaseLock(); |
| 1155 | releaseLock = null; |
| 1156 | } |
| 1157 | }, |
| 1158 | inReplyTo: fpid, |
| 1159 | draftKey |
| 1160 | }); |
| 1161 | initFPEWidget(ePost, fpe); |
| 1162 | }/*replyClicked()*/; |
| 1163 | |
| 1164 | const editClicked = async (form, ePost, eBtnEdit, eToDisable)=>{ |
| 1165 | const fpid = ePost.dataset.fpid; |
| 1166 | const firt = ePost.dataset.firt; |
| 1167 | const fEditHead = ePost.dataset.fedithead; |
| 1168 | const draftKey = makeDraftKey('draft-forumedit', fEditHead || fpid); |
| 1169 | const lockName = 'fossil-'+draftKey; |
| 1170 | let releaseLock; |
| 1171 | if( navigator.locks ){ |
| 1172 | releaseLock = await new Promise((resolve) => { |
| 1173 | navigator.locks.request(lockName, {ifAvailable: true}, async (lock)=>{ |
| 1174 | if( !lock ){ |
| 1175 | resolve(null); |
| 1176 | return; |
| 1177 | } |
| 1178 | let release; |
| 1179 | const lockReleased = new Promise(res=>release=res); |
| 1180 | resolve(release); |
| 1181 | await lockReleased; |
| 1182 | }); |
| 1183 | }); |
| 1184 | |
| 1185 | if( !releaseLock ){ |
| 1186 | reportFPEError( |
| 1187 | ePost, |
| 1188 | "This post is actively being edited ", |
| 1189 | "in another tab. To avoid losing edits, ", |
| 1190 | "it cannot be opened here until the locking ", |
| 1191 | "tab is closed." |
| 1192 | ); |
| 1193 | return; |
| 1194 | } |
| 1195 | } |
| 1196 | setupEditReplyElement(ePost, eBtnEdit, eToDisable); |
| 1197 | eBtnEdit.innerText = "Editing..."; |
| 1198 | fetchPost(fpid) |
| 1199 | .then(artifact=>{ |
| 1200 | const ondone = (fpe, response)=>{ |
| 1201 | /* onsubmit() and ondiscard() callback */ |
| @@ -1131,16 +1220,31 @@ | |
| 1220 | |
| 1221 | const fpe = new F.ForumPostEditor({ |
| 1222 | hiddenFields: form.querySelectorAll('input[type=hidden]'), |
| 1223 | ondiscard: ondone, |
| 1224 | onsubmit: ondone, |
| 1225 | onclose: ()=>{ |
| 1226 | if(releaseLock){ |
| 1227 | releaseLock(); |
| 1228 | releaseLock = null; |
| 1229 | } |
| 1230 | }, |
| 1231 | draftKey, |
| 1232 | edit: artifact, |
| 1233 | status: eStatusSelect?.value, |
| 1234 | inReplyTo: firt |
| 1235 | }); |
| 1236 | initFPEWidget(ePost, fpe); |
| 1237 | }) |
| 1238 | .catch(err=>{ |
| 1239 | if( releaseLock ){ |
| 1240 | releaseLock(); |
| 1241 | releaseLock = null; |
| 1242 | } |
| 1243 | restoreEditReplyElement(ePost, eBtnEdit, eToDisable); |
| 1244 | console.error("Error fetching post:", err); |
| 1245 | reportFPEError(ePost, "Error fetching post: ", err.message); |
| 1246 | }); |
| 1247 | }/*editClicked()*/; |
| 1248 | |
| 1249 | document.body.querySelectorAll( |
| 1250 | '.forumpost-single-controls > form' |
| 1251 |
+12
-5
| --- www/forum.wiki | ||
| +++ www/forum.wiki | ||
| @@ -534,10 +534,22 @@ | ||
| 534 | 534 | If neither <code>localStorage</code> nor <code>sessionStorage</code> |
| 535 | 535 | are available, it uses a transient storage object which is recreated |
| 536 | 536 | on each page-load, so it cannot store drafts outside of a single |
| 537 | 537 | thread at a time. |
| 538 | 538 | |
| 539 | +Beware: Each draft is keyed to the post it is editing or replied to, | |
| 540 | +which means that editing or replying to the same post from different | |
| 541 | +tabs will cause collisions. The editor to most recently lose the focus | |
| 542 | +will save the draft, overwriting the other. | |
| 543 | + | |
| 544 | +In browsers which support Web Locks, attempts to edit resp. reply to a | |
| 545 | +post which is already being edited resp. replied to in another tab | |
| 546 | +will show an error for the second and subsequent tabs. The first tab | |
| 547 | +to use the edit/reply buttons will hold a separate Web Lock for each | |
| 548 | +and forbid edit/reply access to the post in other tabs until the | |
| 549 | +locking tab is closed. | |
| 550 | + | |
| 539 | 551 | |
| 540 | 552 | <h2 name="padding">Curious Extra Page Padding</h2> |
| 541 | 553 | |
| 542 | 554 | Users may notice that the forum pages get a bunch of padding added to |
| 543 | 555 | the bottom of the page when an editor widget is opened. This is not a |
| @@ -545,10 +557,5 @@ | ||
| 545 | 557 | rendering a preview wildly resizes the widget. The effect is |
| 546 | 558 | especially helpful when working on posts while the bottom footer of |
| 547 | 559 | the page is in the viewport, as the footer is a hard boundary against |
| 548 | 560 | scrolling. With the extra padding, the editor widget's top edge shifts |
| 549 | 561 | around less when the preview is shown and hidden. |
| 550 | - | |
| 551 | -Beware: Each draft is keyed to the post it is editing or replied to, | |
| 552 | -which means that editing or replying to the same post from different | |
| 553 | -tabs will cause collisions. The editor to most recently lose the focus | |
| 554 | -will save the draft, overwriting the other. | |
| 555 | 562 |
| --- www/forum.wiki | |
| +++ www/forum.wiki | |
| @@ -534,10 +534,22 @@ | |
| 534 | If neither <code>localStorage</code> nor <code>sessionStorage</code> |
| 535 | are available, it uses a transient storage object which is recreated |
| 536 | on each page-load, so it cannot store drafts outside of a single |
| 537 | thread at a time. |
| 538 | |
| 539 | |
| 540 | <h2 name="padding">Curious Extra Page Padding</h2> |
| 541 | |
| 542 | Users may notice that the forum pages get a bunch of padding added to |
| 543 | the bottom of the page when an editor widget is opened. This is not a |
| @@ -545,10 +557,5 @@ | |
| 545 | rendering a preview wildly resizes the widget. The effect is |
| 546 | especially helpful when working on posts while the bottom footer of |
| 547 | the page is in the viewport, as the footer is a hard boundary against |
| 548 | scrolling. With the extra padding, the editor widget's top edge shifts |
| 549 | around less when the preview is shown and hidden. |
| 550 | |
| 551 | Beware: Each draft is keyed to the post it is editing or replied to, |
| 552 | which means that editing or replying to the same post from different |
| 553 | tabs will cause collisions. The editor to most recently lose the focus |
| 554 | will save the draft, overwriting the other. |
| 555 |
| --- www/forum.wiki | |
| +++ www/forum.wiki | |
| @@ -534,10 +534,22 @@ | |
| 534 | If neither <code>localStorage</code> nor <code>sessionStorage</code> |
| 535 | are available, it uses a transient storage object which is recreated |
| 536 | on each page-load, so it cannot store drafts outside of a single |
| 537 | thread at a time. |
| 538 | |
| 539 | Beware: Each draft is keyed to the post it is editing or replied to, |
| 540 | which means that editing or replying to the same post from different |
| 541 | tabs will cause collisions. The editor to most recently lose the focus |
| 542 | will save the draft, overwriting the other. |
| 543 | |
| 544 | In browsers which support Web Locks, attempts to edit resp. reply to a |
| 545 | post which is already being edited resp. replied to in another tab |
| 546 | will show an error for the second and subsequent tabs. The first tab |
| 547 | to use the edit/reply buttons will hold a separate Web Lock for each |
| 548 | and forbid edit/reply access to the post in other tabs until the |
| 549 | locking tab is closed. |
| 550 | |
| 551 | |
| 552 | <h2 name="padding">Curious Extra Page Padding</h2> |
| 553 | |
| 554 | Users may notice that the forum pages get a bunch of padding added to |
| 555 | the bottom of the page when an editor widget is opened. This is not a |
| @@ -545,10 +557,5 @@ | |
| 557 | rendering a preview wildly resizes the widget. The effect is |
| 558 | especially helpful when working on posts while the bottom footer of |
| 559 | the page is in the viewport, as the footer is a hard boundary against |
| 560 | scrolling. With the extra padding, the editor widget's top edge shifts |
| 561 | around less when the preview is shown and hidden. |
| 562 |