Fossil SCM

Plugged a leaky abstraction in wikiedit and fileedit which applied when connecting a 3rd-party editor widget and added an example of how to plug in TinyMCE to the fileedit docs.

stephan 2020-08-29 14:07 trunk
Commit 88703f001d7b0cb3b47351e98f36636689e24d36e50f11c53748ed63551b8852
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -753,13 +753,11 @@
753753
});
754754
E('#comment-toggle').addEventListener(
755755
"click",(e)=>P.toggleCommentMode(), false
756756
);
757757
758
- P.e.taEditor.addEventListener(
759
- 'change', ()=>P.stashContentChange(), false
760
- );
758
+ P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
761759
P.e.cbIsExe.addEventListener(
762760
'change', ()=>P.stashContentChange(true), false
763761
);
764762
765763
/**
@@ -856,10 +854,27 @@
856854
P.setContentMethods = function(getter, setter){
857855
this.fileContent.get = getter;
858856
this.fileContent.set = setter;
859857
return this;
860858
};
859
+
860
+ /**
861
+ Alerts the editor app that a "change" has happened in the editor.
862
+ When connecting 3rd-party editor widgets to this app, it is (or
863
+ may be) necessary to call this for any "change" events the widget
864
+ emits. Whether or not "change" means that there were "really"
865
+ edits is irrelevant.
866
+
867
+ This function may perform an arbitrary amount of work, so it
868
+ should not be called for every keypress within the editor
869
+ widget. Calling it for "blur" events is generally sufficient, and
870
+ calling it for each Enter keypress is generally reasonable but
871
+ also computationally costly.
872
+ */
873
+ P.notifyOfChange = function(){
874
+ P.stashContentChange();
875
+ };
861876
862877
/**
863878
Removes the default editor widget (and any dependent elements)
864879
from the DOM, adds the given element in its place, removes this
865880
method from this object, and returns this object.
866881
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -753,13 +753,11 @@
753 });
754 E('#comment-toggle').addEventListener(
755 "click",(e)=>P.toggleCommentMode(), false
756 );
757
758 P.e.taEditor.addEventListener(
759 'change', ()=>P.stashContentChange(), false
760 );
761 P.e.cbIsExe.addEventListener(
762 'change', ()=>P.stashContentChange(true), false
763 );
764
765 /**
@@ -856,10 +854,27 @@
856 P.setContentMethods = function(getter, setter){
857 this.fileContent.get = getter;
858 this.fileContent.set = setter;
859 return this;
860 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
862 /**
863 Removes the default editor widget (and any dependent elements)
864 from the DOM, adds the given element in its place, removes this
865 method from this object, and returns this object.
866
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -753,13 +753,11 @@
753 });
754 E('#comment-toggle').addEventListener(
755 "click",(e)=>P.toggleCommentMode(), false
756 );
757
758 P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
 
 
759 P.e.cbIsExe.addEventListener(
760 'change', ()=>P.stashContentChange(true), false
761 );
762
763 /**
@@ -856,10 +854,27 @@
854 P.setContentMethods = function(getter, setter){
855 this.fileContent.get = getter;
856 this.fileContent.set = setter;
857 return this;
858 };
859
860 /**
861 Alerts the editor app that a "change" has happened in the editor.
862 When connecting 3rd-party editor widgets to this app, it is (or
863 may be) necessary to call this for any "change" events the widget
864 emits. Whether or not "change" means that there were "really"
865 edits is irrelevant.
866
867 This function may perform an arbitrary amount of work, so it
868 should not be called for every keypress within the editor
869 widget. Calling it for "blur" events is generally sufficient, and
870 calling it for each Enter keypress is generally reasonable but
871 also computationally costly.
872 */
873 P.notifyOfChange = function(){
874 P.stashContentChange();
875 };
876
877 /**
878 Removes the default editor widget (and any dependent elements)
879 from the DOM, adds the given element in its place, removes this
880 method from this object, and returns this object.
881
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -994,16 +994,11 @@
994994
}else{
995995
P.e.btnSave.addEventListener('click', ()=>doSave(), false);
996996
P.e.btnSaveClose.addEventListener('click', ()=>doSave(true), false);
997997
}
998998
999
- P.e.taEditor.addEventListener(
1000
- 'change', function(){
1001
- P._isDirty = true;
1002
- P.stashContentChange();
1003
- }, false
1004
- );
999
+ P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
10051000
10061001
P.selectMimetype(false, true);
10071002
P.e.selectMimetype.addEventListener(
10081003
'change',
10091004
function(e){
@@ -1195,15 +1190,37 @@
11951190
P.setContentMethods = function(getter, setter){
11961191
this.wikiContent.get = getter;
11971192
this.wikiContent.set = setter;
11981193
return this;
11991194
};
1195
+
1196
+ /**
1197
+ Alerts the editor app that a "change" has happened in the editor.
1198
+ When connecting 3rd-party editor widgets to this app, it is
1199
+ necessary to call this for any "change" events the widget emits.
1200
+ Whether or not "change" means that there were "really" edits is
1201
+ irrelevant, but this app will not allow saving unless it believes
1202
+ at least one "change" has been made (by being signaled through
1203
+ this method).
1204
+
1205
+ This function may perform an arbitrary amount of work, so it
1206
+ should not be called for every keypress within the editor
1207
+ widget. Calling it for "blur" events is generally sufficient, and
1208
+ calling it for each Enter keypress is generally reasonable but
1209
+ also computationally costly.
1210
+ */
1211
+ P.notifyOfChange = function(){
1212
+ P._isDirty = true;
1213
+ P.stashContentChange();
1214
+ };
12001215
12011216
/**
12021217
Removes the default editor widget (and any dependent elements)
12031218
from the DOM, adds the given element in its place, removes this
1204
- method from this object, and returns this object.
1219
+ method from this object, and returns this object. This is not
1220
+ needed if the 3rd-party widget replaces or hides this app's
1221
+ editor widget (e.g. TinyMCE).
12051222
*/
12061223
P.replaceEditorElement = function(newEditor){
12071224
P.e.taEditor.parentNode.insertBefore(newEditor, P.e.taEditor);
12081225
P.e.taEditor.remove();
12091226
P.e.selectFontSizeWrap.remove();
@@ -1453,11 +1470,11 @@
14531470
if(onlyWinfo && $stash.hasStashedContent(wi)){
14541471
$stash.updateWinfo(wi);
14551472
}else{
14561473
$stash.updateWinfo(wi, P.wikiContent());
14571474
}
1458
- F.message("Stashed change(s) to page ["+wi.name+"].");
1475
+ F.message("Stashed changes to page ["+wi.name+"].");
14591476
P.updatePageTitle();
14601477
$stash.prune();
14611478
this.previewNeedsUpdate = true;
14621479
}
14631480
return this;
14641481
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -994,16 +994,11 @@
994 }else{
995 P.e.btnSave.addEventListener('click', ()=>doSave(), false);
996 P.e.btnSaveClose.addEventListener('click', ()=>doSave(true), false);
997 }
998
999 P.e.taEditor.addEventListener(
1000 'change', function(){
1001 P._isDirty = true;
1002 P.stashContentChange();
1003 }, false
1004 );
1005
1006 P.selectMimetype(false, true);
1007 P.e.selectMimetype.addEventListener(
1008 'change',
1009 function(e){
@@ -1195,15 +1190,37 @@
1195 P.setContentMethods = function(getter, setter){
1196 this.wikiContent.get = getter;
1197 this.wikiContent.set = setter;
1198 return this;
1199 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
1201 /**
1202 Removes the default editor widget (and any dependent elements)
1203 from the DOM, adds the given element in its place, removes this
1204 method from this object, and returns this object.
 
 
1205 */
1206 P.replaceEditorElement = function(newEditor){
1207 P.e.taEditor.parentNode.insertBefore(newEditor, P.e.taEditor);
1208 P.e.taEditor.remove();
1209 P.e.selectFontSizeWrap.remove();
@@ -1453,11 +1470,11 @@
1453 if(onlyWinfo && $stash.hasStashedContent(wi)){
1454 $stash.updateWinfo(wi);
1455 }else{
1456 $stash.updateWinfo(wi, P.wikiContent());
1457 }
1458 F.message("Stashed change(s) to page ["+wi.name+"].");
1459 P.updatePageTitle();
1460 $stash.prune();
1461 this.previewNeedsUpdate = true;
1462 }
1463 return this;
1464
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -994,16 +994,11 @@
994 }else{
995 P.e.btnSave.addEventListener('click', ()=>doSave(), false);
996 P.e.btnSaveClose.addEventListener('click', ()=>doSave(true), false);
997 }
998
999 P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
 
 
 
 
 
1000
1001 P.selectMimetype(false, true);
1002 P.e.selectMimetype.addEventListener(
1003 'change',
1004 function(e){
@@ -1195,15 +1190,37 @@
1190 P.setContentMethods = function(getter, setter){
1191 this.wikiContent.get = getter;
1192 this.wikiContent.set = setter;
1193 return this;
1194 };
1195
1196 /**
1197 Alerts the editor app that a "change" has happened in the editor.
1198 When connecting 3rd-party editor widgets to this app, it is
1199 necessary to call this for any "change" events the widget emits.
1200 Whether or not "change" means that there were "really" edits is
1201 irrelevant, but this app will not allow saving unless it believes
1202 at least one "change" has been made (by being signaled through
1203 this method).
1204
1205 This function may perform an arbitrary amount of work, so it
1206 should not be called for every keypress within the editor
1207 widget. Calling it for "blur" events is generally sufficient, and
1208 calling it for each Enter keypress is generally reasonable but
1209 also computationally costly.
1210 */
1211 P.notifyOfChange = function(){
1212 P._isDirty = true;
1213 P.stashContentChange();
1214 };
1215
1216 /**
1217 Removes the default editor widget (and any dependent elements)
1218 from the DOM, adds the given element in its place, removes this
1219 method from this object, and returns this object. This is not
1220 needed if the 3rd-party widget replaces or hides this app's
1221 editor widget (e.g. TinyMCE).
1222 */
1223 P.replaceEditorElement = function(newEditor){
1224 P.e.taEditor.parentNode.insertBefore(newEditor, P.e.taEditor);
1225 P.e.taEditor.remove();
1226 P.e.selectFontSizeWrap.remove();
@@ -1453,11 +1470,11 @@
1470 if(onlyWinfo && $stash.hasStashedContent(wi)){
1471 $stash.updateWinfo(wi);
1472 }else{
1473 $stash.updateWinfo(wi, P.wikiContent());
1474 }
1475 F.message("Stashed changes to page ["+wi.name+"].");
1476 P.updatePageTitle();
1477 $stash.prune();
1478 this.previewNeedsUpdate = true;
1479 }
1480 return this;
1481
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -225,12 +225,14 @@
225225
the preview which explicitly have a CSS class named
226226
`language-`something, and then asks highlightjs to highlight them.
227227
228228
## Integrating a Custom Editor Widget
229229
230
-*Hypothetically*, though this is currently unproven "in the wild," it
231
-is possible to replace `/filepage`'s basic text-editing widget (a
230
+(These instructions also work for the `/wikiedit` page by eplacing
231
+"fileedit" with "wikiedit" in any strings or symbol names!)
232
+
233
+It is possible to replace `/filepage`'s basic text-editing widget (a
232234
`textarea` element) with a fancy 3rd-party editor widget by following
233235
these instructions...
234236
235237
All JavaScript code which follows is assumed to be in a script tag
236238
similar to the one shown in the previous section:
@@ -251,21 +253,68 @@
251253
function(){ return text-form content of your widget },
252254
function(content){ set text-form content of your widget }
253255
};
254256
```
255257
256
-Secondly, inject the custom editor widget into the UI, replacing
257
-the default editor widget:
258
+Secondly, we need to alert the editor app when there are changes so
259
+that it can do things like store edits locally so that they are not
260
+lost on a page reload. How that is done is completely dependent on the
261
+3rd-party editor widget, but it generically looks something like:
262
+
263
+```
264
+myCustomWidget.on('eventName', ()=>fossil.page.notifyOfChange());
265
+```
266
+
267
+(This feature requires fossil version 2.13 or later. In 2.12 it is
268
+possible to do this but requires making use of a "leaky abstraction".)
269
+
270
+Lastly, if the 3rd-party editor does *not* hide or remove the native
271
+editor widget, and does not inject itself into the DOM on the caller's
272
+behalf, we can replace the native widget with the 3rd-party one with:
258273
259274
```javascript
260275
fossil.page.replaceEditorWidget(yourNewWidgetElement);
261276
```
262277
263278
That method must be passed a DOM element and may only be called once:
264279
it *removes itself* the first time it is called.
265280
266
-That "should" be all there is to it. When `fossil.page` needs to get
267
-the being-edited content, it will call `fossil.page.fileContent()`
268
-with no arguments, and when it sets the content (immediately after
269
-(re)loading a file), it will pass that content to
270
-`fossil.page.fileContent()`. Those, in turn will trigger the installed
271
-proxies and fire any relevant events.
281
+That should be all there is to it. When `fossil.page` needs to get the
282
+being-edited content, it will call the installed content-getter
283
+function with no arguments, and when it sets the content (immediately
284
+after (re)loading a file or grabbing local edits), it will pass that
285
+content to the installed content-setter method. Those, in turn will
286
+trigger the installed proxies and fire any relevant events.
287
+
288
+Below is an example of Fossil skin footer content which plugs in the
289
+TinyMCE HTML editor into the `/wikiedit` page, but the process is
290
+identical for `/fileedit` (noting that `/fileedit` may need to be able
291
+to edit multiple types of files for which a special-purpose editor
292
+like TinyMCE may not be suitable). Note that any paths to CSS and JS
293
+resources of course need to be modified to suit one's own
294
+installation.
295
+
296
+```
297
+<!-- TinyMCE CSS and JS: -->
298
+<link href="$<home>/doc/ckout/skin.min.css" rel="stylesheet" type="text/css">
299
+<link href="$<home>/doc/ckout/content.min.css" rel="stylesheet" type="text/css">
300
+<script src='$<home>/doc/ckout/tinymce.min.js'></script>
301
+<script src='$<home>/doc/ckout/theme.min.js'></script>
302
+<script src='$<home>/doc/ckout/icons.min.js'></script>
303
+<!-- Integrate TinyMCE into /wikiedit: -->
304
+<script nonce="$<nonce>">
305
+if(window.fossil && window.fossil.page.name==='wikiedit'){
306
+ window.fossil.onPageLoad( function(){
307
+ const elemId = 'wikiedit-content-editor';
308
+ tinymce.init({selector: 'textarea#'+elemId});
309
+ const widget = tinymce.get(elemId);
310
+ fossil.page.setContentMethods(
311
+ function(){return widget.getContent()},
312
+ function(content){widget.setContent(content)}
313
+ );
314
+ widget.on('change', function(){
315
+ if(widget.isDirty()) fossil.page.notifyOfChange();
316
+ });
317
+ });
318
+}
319
+</script>
320
+```
272321
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -225,12 +225,14 @@
225 the preview which explicitly have a CSS class named
226 `language-`something, and then asks highlightjs to highlight them.
227
228 ## Integrating a Custom Editor Widget
229
230 *Hypothetically*, though this is currently unproven "in the wild," it
231 is possible to replace `/filepage`'s basic text-editing widget (a
 
 
232 `textarea` element) with a fancy 3rd-party editor widget by following
233 these instructions...
234
235 All JavaScript code which follows is assumed to be in a script tag
236 similar to the one shown in the previous section:
@@ -251,21 +253,68 @@
251 function(){ return text-form content of your widget },
252 function(content){ set text-form content of your widget }
253 };
254 ```
255
256 Secondly, inject the custom editor widget into the UI, replacing
257 the default editor widget:
 
 
 
 
 
 
 
 
 
 
 
 
 
258
259 ```javascript
260 fossil.page.replaceEditorWidget(yourNewWidgetElement);
261 ```
262
263 That method must be passed a DOM element and may only be called once:
264 it *removes itself* the first time it is called.
265
266 That "should" be all there is to it. When `fossil.page` needs to get
267 the being-edited content, it will call `fossil.page.fileContent()`
268 with no arguments, and when it sets the content (immediately after
269 (re)loading a file), it will pass that content to
270 `fossil.page.fileContent()`. Those, in turn will trigger the installed
271 proxies and fire any relevant events.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -225,12 +225,14 @@
225 the preview which explicitly have a CSS class named
226 `language-`something, and then asks highlightjs to highlight them.
227
228 ## Integrating a Custom Editor Widget
229
230 (These instructions also work for the `/wikiedit` page by eplacing
231 "fileedit" with "wikiedit" in any strings or symbol names!)
232
233 It is possible to replace `/filepage`'s basic text-editing widget (a
234 `textarea` element) with a fancy 3rd-party editor widget by following
235 these instructions...
236
237 All JavaScript code which follows is assumed to be in a script tag
238 similar to the one shown in the previous section:
@@ -251,21 +253,68 @@
253 function(){ return text-form content of your widget },
254 function(content){ set text-form content of your widget }
255 };
256 ```
257
258 Secondly, we need to alert the editor app when there are changes so
259 that it can do things like store edits locally so that they are not
260 lost on a page reload. How that is done is completely dependent on the
261 3rd-party editor widget, but it generically looks something like:
262
263 ```
264 myCustomWidget.on('eventName', ()=>fossil.page.notifyOfChange());
265 ```
266
267 (This feature requires fossil version 2.13 or later. In 2.12 it is
268 possible to do this but requires making use of a "leaky abstraction".)
269
270 Lastly, if the 3rd-party editor does *not* hide or remove the native
271 editor widget, and does not inject itself into the DOM on the caller's
272 behalf, we can replace the native widget with the 3rd-party one with:
273
274 ```javascript
275 fossil.page.replaceEditorWidget(yourNewWidgetElement);
276 ```
277
278 That method must be passed a DOM element and may only be called once:
279 it *removes itself* the first time it is called.
280
281 That should be all there is to it. When `fossil.page` needs to get the
282 being-edited content, it will call the installed content-getter
283 function with no arguments, and when it sets the content (immediately
284 after (re)loading a file or grabbing local edits), it will pass that
285 content to the installed content-setter method. Those, in turn will
286 trigger the installed proxies and fire any relevant events.
287
288 Below is an example of Fossil skin footer content which plugs in the
289 TinyMCE HTML editor into the `/wikiedit` page, but the process is
290 identical for `/fileedit` (noting that `/fileedit` may need to be able
291 to edit multiple types of files for which a special-purpose editor
292 like TinyMCE may not be suitable). Note that any paths to CSS and JS
293 resources of course need to be modified to suit one's own
294 installation.
295
296 ```
297 <!-- TinyMCE CSS and JS: -->
298 <link href="$<home>/doc/ckout/skin.min.css" rel="stylesheet" type="text/css">
299 <link href="$<home>/doc/ckout/content.min.css" rel="stylesheet" type="text/css">
300 <script src='$<home>/doc/ckout/tinymce.min.js'></script>
301 <script src='$<home>/doc/ckout/theme.min.js'></script>
302 <script src='$<home>/doc/ckout/icons.min.js'></script>
303 <!-- Integrate TinyMCE into /wikiedit: -->
304 <script nonce="$<nonce>">
305 if(window.fossil && window.fossil.page.name==='wikiedit'){
306 window.fossil.onPageLoad( function(){
307 const elemId = 'wikiedit-content-editor';
308 tinymce.init({selector: 'textarea#'+elemId});
309 const widget = tinymce.get(elemId);
310 fossil.page.setContentMethods(
311 function(){return widget.getContent()},
312 function(content){widget.setContent(content)}
313 );
314 widget.on('change', function(){
315 if(widget.isDirty()) fossil.page.notifyOfChange();
316 });
317 });
318 }
319 </script>
320 ```
321

Keyboard Shortcuts

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