Fossil SCM

Milestone: eliminated the remaining assign-to-DOMElement.innerHTML in the fossil.*.js APIs (ostensible security enhancement), thanks to the DOMParser interface. Fixed an obscure minor bug in /fileedit where a commit message which contained HTML tags could cause the page to misbehave if the 'response manifest' debugging option was turned on.

stephan 2020-09-12 12:21 trunk
Commit 79023c9273e2536d1aacae7734ea7996f6fe31462a32380b86f105087b636fcd
--- src/default.css
+++ src/default.css
@@ -1124,10 +1124,11 @@
11241124
.input-with-label > * {
11251125
vertical-align: middle;
11261126
}
11271127
.input-with-label > label {
11281128
display: inline; /* some skins set label display to block! */
1129
+ cursor: pointer;
11291130
}
11301131
.input-with-label > input {
11311132
margin: 0;
11321133
}
11331134
.input-with-label > button {
11341135
--- src/default.css
+++ src/default.css
@@ -1124,10 +1124,11 @@
1124 .input-with-label > * {
1125 vertical-align: middle;
1126 }
1127 .input-with-label > label {
1128 display: inline; /* some skins set label display to block! */
 
1129 }
1130 .input-with-label > input {
1131 margin: 0;
1132 }
1133 .input-with-label > button {
1134
--- src/default.css
+++ src/default.css
@@ -1124,10 +1124,11 @@
1124 .input-with-label > * {
1125 vertical-align: middle;
1126 }
1127 .input-with-label > label {
1128 display: inline; /* some skins set label display to block! */
1129 cursor: pointer;
1130 }
1131 .input-with-label > input {
1132 margin: 0;
1133 }
1134 .input-with-label > button {
1135
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -199,139 +199,19 @@
199199
return ('string'==typeof hash ? hash.substr(
200200
0, n
201201
) : hash);
202202
};
203203
204
- /**
205
- Sets up pseudo-automatic content preview handling between a
206
- source element (typically a TEXTAREA) and a target rendering
207
- element (typically a DIV). The selector argument must be one of:
208
-
209
- - A single DOM element
210
- - A collection of DOM elements with a forEach method.
211
- - A CSS selector
212
-
213
- Each element in the collection must have the following data
214
- attributes:
215
-
216
- - data-f-preview-from: is either a DOM element id, WITH a leading
217
- '#' prefix, or the name of a method (see below). If it's an ID,
218
- the DOM element must support .value to get the content.
219
-
220
- - data-f-preview-to: the DOM element id of the target "previewer"
221
- element, WITH a leading '#', or the name of a method (see below).
222
-
223
- - data-f-preview-via: the name of a method (see below).
224
-
225
- - OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
226
-
227
- Each element gets a click handler added to it which does the
228
- following:
229
-
230
- 1) Reads the content from its data-f-preview-from element or, if
231
- that property refers to a method, calls the method without
232
- arguments and uses its result as the content.
233
-
234
- 2) Passes the content to
235
- methodNamespace[f-data-post-via](content,callback). f-data-post-via
236
- is responsible for submitting the preview HTTP request, including
237
- any parameters the request might require. When the response
238
- arrives, it must pass the content of the response to its 2nd
239
- argument, an auto-generated callback installed by this mechanism
240
- which...
241
-
242
- 3) Assigns the response text to the data-f-preview-to element or
243
- passes it to the function methodNamespace[f-data-preview-to](content), as
244
- appropriate. If data-f-preview-to is a DOM element and
245
- data-f-preview-as-text is '0' (the default) then the content is
246
- assigned to the target element's innerHTML property, else it is
247
- assigned to the element's textContent property.
248
-
249
- The methodNamespace (2nd argument) defaults to fossil.page, and
250
- any method-name data properties, e.g. data-f-preview-via and
251
- potentially data-f-preview-from/to, must be a single method name,
252
- not a property-access-style string. e.g. "myPreview" is legal but
253
- "foo.myPreview" is not (unless, of course, the method is actually
254
- named "foo.myPreview" (which is legal but would be
255
- unconventional)).
256
-
257
- An example...
258
-
259
- First an input button:
260
-
261
- <button id='test-preview-connector'
262
- data-f-preview-from='#fileedit-content-editor' // elem ID or method name
263
- data-f-preview-via='myPreview' // method name
264
- data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
265
- >Preview update</button>
266
-
267
- And a sample data-f-preview-via method:
268
-
269
- fossil.page.myPreview = function(content,callback){
270
- const fd = new FormData();
271
- fd.append('foo', ...);
272
- fossil.fetch('preview_forumpost',{
273
- payload: fd,
274
- onload: callback,
275
- onerror: (e)=>{ // only if app-specific handling is needed
276
- fossil.fetch.onerror(e); // default impl
277
- ... any app-specific error reporting ...
278
- }
279
- });
280
- };
281
-
282
- Then connect the parts with:
283
-
284
- fossil.connectPagePreviewers('#test-preview-connector');
285
-
286
- Note that the data-f-preview-from, data-f-preview-via, and
287
- data-f-preview-to selector are not resolved until the button is
288
- actually clicked, so they need not exist in the DOM at the
289
- instant when the connection is set up, so long as they can be
290
- resolved when the preview-refreshing element is clicked.
291
- */
292
- F.connectPagePreviewers = function f(selector,methodNamespace){
293
- if('string'===typeof selector){
294
- selector = document.querySelectorAll(selector);
295
- }else if(!selector.forEach){
296
- selector = [selector];
297
- }
298
- if(!methodNamespace){
299
- methodNamespace = F.page;
300
- }
301
- selector.forEach(function(e){
302
- e.addEventListener(
303
- 'click', function(r){
304
- const eTo = '#'===e.dataset.fPreviewTo[0]
305
- ? document.querySelector(e.dataset.fPreviewTo)
306
- : methodNamespace[e.dataset.fPreviewTo],
307
- eFrom = '#'===e.dataset.fPreviewFrom[0]
308
- ? document.querySelector(e.dataset.fPreviewFrom)
309
- : methodNamespace[e.dataset.fPreviewFrom],
310
- asText = +(e.dataset.fPreviewAsText || 0);
311
- eTo.textContent = "Fetching preview...";
312
- methodNamespace[e.dataset.fPreviewVia](
313
- (eFrom instanceof Function ? eFrom() : eFrom.value),
314
- (r)=>{
315
- if(eTo instanceof Function) eTo(r||'');
316
- else eTo[asText ? 'textContent' : 'innerHTML'] = r||'';
317
- }
318
- );
319
- }, false
320
- );
321
- });
322
- return this;
323
- };
324
-
325204
/**
326205
Convenience wrapper which adds an onload event listener to the
327206
window object. Returns this.
328207
*/
329208
F.onPageLoad = function(callback){
330209
window.addEventListener('load', callback, false);
331210
return this;
332211
};
212
+
333213
/**
334214
Convenience wrapper which adds a DOMContentLoadedevent listener
335215
to the window object. Returns this.
336216
*/
337217
F.onDOMContentLoaded = function(callback){
338218
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -199,139 +199,19 @@
199 return ('string'==typeof hash ? hash.substr(
200 0, n
201 ) : hash);
202 };
203
204 /**
205 Sets up pseudo-automatic content preview handling between a
206 source element (typically a TEXTAREA) and a target rendering
207 element (typically a DIV). The selector argument must be one of:
208
209 - A single DOM element
210 - A collection of DOM elements with a forEach method.
211 - A CSS selector
212
213 Each element in the collection must have the following data
214 attributes:
215
216 - data-f-preview-from: is either a DOM element id, WITH a leading
217 '#' prefix, or the name of a method (see below). If it's an ID,
218 the DOM element must support .value to get the content.
219
220 - data-f-preview-to: the DOM element id of the target "previewer"
221 element, WITH a leading '#', or the name of a method (see below).
222
223 - data-f-preview-via: the name of a method (see below).
224
225 - OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
226
227 Each element gets a click handler added to it which does the
228 following:
229
230 1) Reads the content from its data-f-preview-from element or, if
231 that property refers to a method, calls the method without
232 arguments and uses its result as the content.
233
234 2) Passes the content to
235 methodNamespace[f-data-post-via](content,callback). f-data-post-via
236 is responsible for submitting the preview HTTP request, including
237 any parameters the request might require. When the response
238 arrives, it must pass the content of the response to its 2nd
239 argument, an auto-generated callback installed by this mechanism
240 which...
241
242 3) Assigns the response text to the data-f-preview-to element or
243 passes it to the function methodNamespace[f-data-preview-to](content), as
244 appropriate. If data-f-preview-to is a DOM element and
245 data-f-preview-as-text is '0' (the default) then the content is
246 assigned to the target element's innerHTML property, else it is
247 assigned to the element's textContent property.
248
249 The methodNamespace (2nd argument) defaults to fossil.page, and
250 any method-name data properties, e.g. data-f-preview-via and
251 potentially data-f-preview-from/to, must be a single method name,
252 not a property-access-style string. e.g. "myPreview" is legal but
253 "foo.myPreview" is not (unless, of course, the method is actually
254 named "foo.myPreview" (which is legal but would be
255 unconventional)).
256
257 An example...
258
259 First an input button:
260
261 <button id='test-preview-connector'
262 data-f-preview-from='#fileedit-content-editor' // elem ID or method name
263 data-f-preview-via='myPreview' // method name
264 data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
265 >Preview update</button>
266
267 And a sample data-f-preview-via method:
268
269 fossil.page.myPreview = function(content,callback){
270 const fd = new FormData();
271 fd.append('foo', ...);
272 fossil.fetch('preview_forumpost',{
273 payload: fd,
274 onload: callback,
275 onerror: (e)=>{ // only if app-specific handling is needed
276 fossil.fetch.onerror(e); // default impl
277 ... any app-specific error reporting ...
278 }
279 });
280 };
281
282 Then connect the parts with:
283
284 fossil.connectPagePreviewers('#test-preview-connector');
285
286 Note that the data-f-preview-from, data-f-preview-via, and
287 data-f-preview-to selector are not resolved until the button is
288 actually clicked, so they need not exist in the DOM at the
289 instant when the connection is set up, so long as they can be
290 resolved when the preview-refreshing element is clicked.
291 */
292 F.connectPagePreviewers = function f(selector,methodNamespace){
293 if('string'===typeof selector){
294 selector = document.querySelectorAll(selector);
295 }else if(!selector.forEach){
296 selector = [selector];
297 }
298 if(!methodNamespace){
299 methodNamespace = F.page;
300 }
301 selector.forEach(function(e){
302 e.addEventListener(
303 'click', function(r){
304 const eTo = '#'===e.dataset.fPreviewTo[0]
305 ? document.querySelector(e.dataset.fPreviewTo)
306 : methodNamespace[e.dataset.fPreviewTo],
307 eFrom = '#'===e.dataset.fPreviewFrom[0]
308 ? document.querySelector(e.dataset.fPreviewFrom)
309 : methodNamespace[e.dataset.fPreviewFrom],
310 asText = +(e.dataset.fPreviewAsText || 0);
311 eTo.textContent = "Fetching preview...";
312 methodNamespace[e.dataset.fPreviewVia](
313 (eFrom instanceof Function ? eFrom() : eFrom.value),
314 (r)=>{
315 if(eTo instanceof Function) eTo(r||'');
316 else eTo[asText ? 'textContent' : 'innerHTML'] = r||'';
317 }
318 );
319 }, false
320 );
321 });
322 return this;
323 };
324
325 /**
326 Convenience wrapper which adds an onload event listener to the
327 window object. Returns this.
328 */
329 F.onPageLoad = function(callback){
330 window.addEventListener('load', callback, false);
331 return this;
332 };
 
333 /**
334 Convenience wrapper which adds a DOMContentLoadedevent listener
335 to the window object. Returns this.
336 */
337 F.onDOMContentLoaded = function(callback){
338
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -199,139 +199,19 @@
199 return ('string'==typeof hash ? hash.substr(
200 0, n
201 ) : hash);
202 };
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204 /**
205 Convenience wrapper which adds an onload event listener to the
206 window object. Returns this.
207 */
208 F.onPageLoad = function(callback){
209 window.addEventListener('load', callback, false);
210 return this;
211 };
212
213 /**
214 Convenience wrapper which adds a DOMContentLoadedevent listener
215 to the window object. Returns this.
216 */
217 F.onDOMContentLoaded = function(callback){
218
--- src/fossil.confirmer.js
+++ src/fossil.confirmer.js
@@ -118,12 +118,13 @@
118118
"times out" if the element is clicked again. e.g. a button which says
119119
"Saving..." and cancels the op if it's clicked again, else it saves
120120
after X time/ticks.
121121
122122
- Internally we save/restore the initial text of non-INPUT elements
123
-using innerHTML. We should instead move their child nodes aside (into
124
-an internal out-of-DOM element) and restore them as needed.
123
+using a relatively expensive bit of DOMParser hoop-jumping. We
124
+"should" instead move their child nodes aside (into an internal
125
+out-of-DOM element) and restore them as needed.
125126
126127
Terse Change history:
127128
128129
- 20200811
129130
- Added pinSize option.
@@ -156,11 +157,18 @@
156157
this.timerID = undefined;
157158
this.state = this.states.initial;
158159
const isInput = f.isInput(target);
159160
const updateText = function(msg){
160161
if(isInput) target.value = msg;
161
- else target.innerHTML = msg;
162
+ else{
163
+ /* Jump through some hoops to avoid assigning to innerHTML... */
164
+ const newNode = new DOMParser().parseFromString(msg, 'text/html');
165
+ let childs = newNode.documentElement.querySelector('body');
166
+ childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
167
+ target.innerText = '';
168
+ childs.forEach((e)=>target.appendChild(e));
169
+ }
162170
}
163171
const formatCountdown = (txt, number) => txt + " ["+number+"]";
164172
if(opt.pinSize && opt.confirmText){
165173
/* Try to pin the element's width the the greater of its
166174
current width or its waiting-on-confirmation width
167175
--- src/fossil.confirmer.js
+++ src/fossil.confirmer.js
@@ -118,12 +118,13 @@
118 "times out" if the element is clicked again. e.g. a button which says
119 "Saving..." and cancels the op if it's clicked again, else it saves
120 after X time/ticks.
121
122 - Internally we save/restore the initial text of non-INPUT elements
123 using innerHTML. We should instead move their child nodes aside (into
124 an internal out-of-DOM element) and restore them as needed.
 
125
126 Terse Change history:
127
128 - 20200811
129 - Added pinSize option.
@@ -156,11 +157,18 @@
156 this.timerID = undefined;
157 this.state = this.states.initial;
158 const isInput = f.isInput(target);
159 const updateText = function(msg){
160 if(isInput) target.value = msg;
161 else target.innerHTML = msg;
 
 
 
 
 
 
 
162 }
163 const formatCountdown = (txt, number) => txt + " ["+number+"]";
164 if(opt.pinSize && opt.confirmText){
165 /* Try to pin the element's width the the greater of its
166 current width or its waiting-on-confirmation width
167
--- src/fossil.confirmer.js
+++ src/fossil.confirmer.js
@@ -118,12 +118,13 @@
118 "times out" if the element is clicked again. e.g. a button which says
119 "Saving..." and cancels the op if it's clicked again, else it saves
120 after X time/ticks.
121
122 - Internally we save/restore the initial text of non-INPUT elements
123 using a relatively expensive bit of DOMParser hoop-jumping. We
124 "should" instead move their child nodes aside (into an internal
125 out-of-DOM element) and restore them as needed.
126
127 Terse Change history:
128
129 - 20200811
130 - Added pinSize option.
@@ -156,11 +157,18 @@
157 this.timerID = undefined;
158 this.state = this.states.initial;
159 const isInput = f.isInput(target);
160 const updateText = function(msg){
161 if(isInput) target.value = msg;
162 else{
163 /* Jump through some hoops to avoid assigning to innerHTML... */
164 const newNode = new DOMParser().parseFromString(msg, 'text/html');
165 let childs = newNode.documentElement.querySelector('body');
166 childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
167 target.innerText = '';
168 childs.forEach((e)=>target.appendChild(e));
169 }
170 }
171 const formatCountdown = (txt, number) => txt + " ["+number+"]";
172 if(opt.pinSize && opt.confirmText){
173 /* Try to pin the element's width the the greater of its
174 current width or its waiting-on-confirmation width
175
+185 -9
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -420,27 +420,25 @@
420420
dom.hasClass = function(e,c){
421421
return (e && e.classList) ? e.classList.contains(c) : false;
422422
};
423423
424424
/**
425
- Each argument after the first may be a single DOM element
426
- or a container of them with a forEach() method. All such
427
- elements are appended, in the given order, to the dest
428
- element.
425
+ Each argument after the first may be a single DOM element or a
426
+ container of them with a forEach() method. All such elements are
427
+ appended, in the given order, to the dest element using
428
+ dom.append(dest,theElement). Thus the 2nd and susequent arguments
429
+ may be any type supported as the 2nd argument to that function.
429430
430431
Returns dest.
431432
*/
432433
dom.moveTo = function(dest,e){
433434
const n = arguments.length;
434435
var i = 1;
436
+ const self = this;
435437
for( ; i < n; ++i ){
436438
e = arguments[i];
437
- if(e.forEach){
438
- e.forEach((x)=>dest.appendChild(x));
439
- }else{
440
- dest.appendChild(e);
441
- }
439
+ this.append(dest, e);
442440
}
443441
return dest;
444442
};
445443
/**
446444
Each argument after the first may be a single DOM element
@@ -697,7 +695,185 @@
697695
}
698696
}
699697
return e;
700698
};
701699
700
+ /**
701
+ Parses a string as HTML.
702
+
703
+ Usages:
704
+
705
+ (htmlString)
706
+ (DOMElement target, htmlString)
707
+
708
+ The first form parses the string as HTML and returns an Array of
709
+ all elements parsed from it. If string is falsy then it returns
710
+ an empty array.
711
+
712
+ The second form parses the HTML string and appends all elements
713
+ to the given target element using dom.append(), then returns the
714
+ first argument.
715
+
716
+ Caveats:
717
+
718
+ - It expects a partial HTML document as input, not a full HTML
719
+ document with a HEAD and BODY tags. Because of how DOMParser
720
+ works, only children of the parsed document's (virtual) body are
721
+ acknowledged by this routine.
722
+ */
723
+ dom.parseHtml = function(){
724
+ let childs, string, tgt;
725
+ if(1===arguments.length){
726
+ string = arguments[0];
727
+ }else if(2==arguments.length){
728
+ tgt = arguments[0];
729
+ string = arguments[1];
730
+ }
731
+ if(string){
732
+ const newNode = new DOMParser().parseFromString(string, 'text/html');
733
+ childs = newNode.documentElement.querySelector('body');
734
+ childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
735
+ /* ^^^ we need a clone of the list because reparenting them
736
+ modifies a NodeList they're in. */
737
+ }else{
738
+ childs = [];
739
+ }
740
+ return tgt ? this.moveTo(tgt, childs) : childs;
741
+ };
742
+
743
+ /**
744
+ Sets up pseudo-automatic content preview handling between a
745
+ source element (typically a TEXTAREA) and a target rendering
746
+ element (typically a DIV). The selector argument must be one of:
747
+
748
+ - A single DOM element
749
+ - A collection of DOM elements with a forEach method.
750
+ - A CSS selector
751
+
752
+ Each element in the collection must have the following data
753
+ attributes:
754
+
755
+ - data-f-preview-from: is either a DOM element id, WITH a leading
756
+ '#' prefix, or the name of a method (see below). If it's an ID,
757
+ the DOM element must support .value to get the content.
758
+
759
+ - data-f-preview-to: the DOM element id of the target "previewer"
760
+ element, WITH a leading '#', or the name of a method (see below).
761
+
762
+ - data-f-preview-via: the name of a method (see below).
763
+
764
+ - OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
765
+
766
+ Each element gets a click handler added to it which does the
767
+ following:
768
+
769
+ 1) Reads the content from its data-f-preview-from element or, if
770
+ that property refers to a method, calls the method without
771
+ arguments and uses its result as the content.
772
+
773
+ 2) Passes the content to
774
+ methodNamespace[f-data-post-via](content,callback). f-data-post-via
775
+ is responsible for submitting the preview HTTP request, including
776
+ any parameters the request might require. When the response
777
+ arrives, it must pass the content of the response to its 2nd
778
+ argument, an auto-generated callback installed by this mechanism
779
+ which...
780
+
781
+ 3) Assigns the response text to the data-f-preview-to element or
782
+ passes it to the function
783
+ methodNamespace[f-data-preview-to](content), as appropriate. If
784
+ data-f-preview-to is a DOM element and data-f-preview-as-text is
785
+ '0' (the default) then the target elements contents are replaced
786
+ with the given content as HTML, else the content is assigned to
787
+ the target's textContent property. (Note that this routine uses
788
+ DOMParser, rather than assignment to innerHTML, to apply
789
+ HTML-format content.)
790
+
791
+ The methodNamespace (2nd argument) defaults to fossil.page, and
792
+ any method-name data properties, e.g. data-f-preview-via and
793
+ potentially data-f-preview-from/to, must be a single method name,
794
+ not a property-access-style string. e.g. "myPreview" is legal but
795
+ "foo.myPreview" is not (unless, of course, the method is actually
796
+ named "foo.myPreview" (which is legal but would be
797
+ unconventional)).
798
+
799
+ An example...
800
+
801
+ First an input button:
802
+
803
+ <button id='test-preview-connector'
804
+ data-f-preview-from='#fileedit-content-editor' // elem ID or method name
805
+ data-f-preview-via='myPreview' // method name
806
+ data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
807
+ >Preview update</button>
808
+
809
+ And a sample data-f-preview-via method:
810
+
811
+ fossil.page.myPreview = function(content,callback){
812
+ const fd = new FormData();
813
+ fd.append('foo', ...);
814
+ fossil.fetch('preview_forumpost',{
815
+ payload: fd,
816
+ onload: callback,
817
+ onerror: (e)=>{ // only if app-specific handling is needed
818
+ fossil.fetch.onerror(e); // default impl
819
+ ... any app-specific error reporting ...
820
+ }
821
+ });
822
+ };
823
+
824
+ Then connect the parts with:
825
+
826
+ fossil.connectPagePreviewers('#test-preview-connector');
827
+
828
+ Note that the data-f-preview-from, data-f-preview-via, and
829
+ data-f-preview-to selector are not resolved until the button is
830
+ actually clicked, so they need not exist in the DOM at the
831
+ instant when the connection is set up, so long as they can be
832
+ resolved when the preview-refreshing element is clicked.
833
+
834
+ Maintenance reminder: this method is not strictly part of
835
+ fossil.dom, but is in its file because it needs access to
836
+ dom.parseHtml() to avoid an innerHTML assignment and all code
837
+ which uses this routine also needs fossil.dom.
838
+ */
839
+ F.connectPagePreviewers = function f(selector,methodNamespace){
840
+ if('string'===typeof selector){
841
+ selector = document.querySelectorAll(selector);
842
+ }else if(!selector.forEach){
843
+ selector = [selector];
844
+ }
845
+ if(!methodNamespace){
846
+ methodNamespace = F.page;
847
+ }
848
+ selector.forEach(function(e){
849
+ e.addEventListener(
850
+ 'click', function(r){
851
+ const eTo = '#'===e.dataset.fPreviewTo[0]
852
+ ? document.querySelector(e.dataset.fPreviewTo)
853
+ : methodNamespace[e.dataset.fPreviewTo],
854
+ eFrom = '#'===e.dataset.fPreviewFrom[0]
855
+ ? document.querySelector(e.dataset.fPreviewFrom)
856
+ : methodNamespace[e.dataset.fPreviewFrom],
857
+ asText = +(e.dataset.fPreviewAsText || 0);
858
+ eTo.textContent = "Fetching preview...";
859
+ methodNamespace[e.dataset.fPreviewVia](
860
+ (eFrom instanceof Function ? eFrom() : eFrom.value),
861
+ function(r){
862
+ if(eTo instanceof Function) eTo(r||'');
863
+ else if(!r){
864
+ dom.clearElement(eTo);
865
+ }else if(asText){
866
+ eTo.textContent = r;
867
+ }else{
868
+ dom.parseHtml(dom.clearElement(eTo), r);
869
+ }
870
+ }
871
+ );
872
+ }, false
873
+ );
874
+ });
875
+ return this;
876
+ }/*F.connectPagePreviewers()*/;
877
+
702878
return F.dom = dom;
703879
})(window.fossil);
704880
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -420,27 +420,25 @@
420 dom.hasClass = function(e,c){
421 return (e && e.classList) ? e.classList.contains(c) : false;
422 };
423
424 /**
425 Each argument after the first may be a single DOM element
426 or a container of them with a forEach() method. All such
427 elements are appended, in the given order, to the dest
428 element.
 
429
430 Returns dest.
431 */
432 dom.moveTo = function(dest,e){
433 const n = arguments.length;
434 var i = 1;
 
435 for( ; i < n; ++i ){
436 e = arguments[i];
437 if(e.forEach){
438 e.forEach((x)=>dest.appendChild(x));
439 }else{
440 dest.appendChild(e);
441 }
442 }
443 return dest;
444 };
445 /**
446 Each argument after the first may be a single DOM element
@@ -697,7 +695,185 @@
697 }
698 }
699 return e;
700 };
701
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702 return F.dom = dom;
703 })(window.fossil);
704
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -420,27 +420,25 @@
420 dom.hasClass = function(e,c){
421 return (e && e.classList) ? e.classList.contains(c) : false;
422 };
423
424 /**
425 Each argument after the first may be a single DOM element or a
426 container of them with a forEach() method. All such elements are
427 appended, in the given order, to the dest element using
428 dom.append(dest,theElement). Thus the 2nd and susequent arguments
429 may be any type supported as the 2nd argument to that function.
430
431 Returns dest.
432 */
433 dom.moveTo = function(dest,e){
434 const n = arguments.length;
435 var i = 1;
436 const self = this;
437 for( ; i < n; ++i ){
438 e = arguments[i];
439 this.append(dest, e);
 
 
 
 
440 }
441 return dest;
442 };
443 /**
444 Each argument after the first may be a single DOM element
@@ -697,7 +695,185 @@
695 }
696 }
697 return e;
698 };
699
700 /**
701 Parses a string as HTML.
702
703 Usages:
704
705 (htmlString)
706 (DOMElement target, htmlString)
707
708 The first form parses the string as HTML and returns an Array of
709 all elements parsed from it. If string is falsy then it returns
710 an empty array.
711
712 The second form parses the HTML string and appends all elements
713 to the given target element using dom.append(), then returns the
714 first argument.
715
716 Caveats:
717
718 - It expects a partial HTML document as input, not a full HTML
719 document with a HEAD and BODY tags. Because of how DOMParser
720 works, only children of the parsed document's (virtual) body are
721 acknowledged by this routine.
722 */
723 dom.parseHtml = function(){
724 let childs, string, tgt;
725 if(1===arguments.length){
726 string = arguments[0];
727 }else if(2==arguments.length){
728 tgt = arguments[0];
729 string = arguments[1];
730 }
731 if(string){
732 const newNode = new DOMParser().parseFromString(string, 'text/html');
733 childs = newNode.documentElement.querySelector('body');
734 childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
735 /* ^^^ we need a clone of the list because reparenting them
736 modifies a NodeList they're in. */
737 }else{
738 childs = [];
739 }
740 return tgt ? this.moveTo(tgt, childs) : childs;
741 };
742
743 /**
744 Sets up pseudo-automatic content preview handling between a
745 source element (typically a TEXTAREA) and a target rendering
746 element (typically a DIV). The selector argument must be one of:
747
748 - A single DOM element
749 - A collection of DOM elements with a forEach method.
750 - A CSS selector
751
752 Each element in the collection must have the following data
753 attributes:
754
755 - data-f-preview-from: is either a DOM element id, WITH a leading
756 '#' prefix, or the name of a method (see below). If it's an ID,
757 the DOM element must support .value to get the content.
758
759 - data-f-preview-to: the DOM element id of the target "previewer"
760 element, WITH a leading '#', or the name of a method (see below).
761
762 - data-f-preview-via: the name of a method (see below).
763
764 - OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
765
766 Each element gets a click handler added to it which does the
767 following:
768
769 1) Reads the content from its data-f-preview-from element or, if
770 that property refers to a method, calls the method without
771 arguments and uses its result as the content.
772
773 2) Passes the content to
774 methodNamespace[f-data-post-via](content,callback). f-data-post-via
775 is responsible for submitting the preview HTTP request, including
776 any parameters the request might require. When the response
777 arrives, it must pass the content of the response to its 2nd
778 argument, an auto-generated callback installed by this mechanism
779 which...
780
781 3) Assigns the response text to the data-f-preview-to element or
782 passes it to the function
783 methodNamespace[f-data-preview-to](content), as appropriate. If
784 data-f-preview-to is a DOM element and data-f-preview-as-text is
785 '0' (the default) then the target elements contents are replaced
786 with the given content as HTML, else the content is assigned to
787 the target's textContent property. (Note that this routine uses
788 DOMParser, rather than assignment to innerHTML, to apply
789 HTML-format content.)
790
791 The methodNamespace (2nd argument) defaults to fossil.page, and
792 any method-name data properties, e.g. data-f-preview-via and
793 potentially data-f-preview-from/to, must be a single method name,
794 not a property-access-style string. e.g. "myPreview" is legal but
795 "foo.myPreview" is not (unless, of course, the method is actually
796 named "foo.myPreview" (which is legal but would be
797 unconventional)).
798
799 An example...
800
801 First an input button:
802
803 <button id='test-preview-connector'
804 data-f-preview-from='#fileedit-content-editor' // elem ID or method name
805 data-f-preview-via='myPreview' // method name
806 data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
807 >Preview update</button>
808
809 And a sample data-f-preview-via method:
810
811 fossil.page.myPreview = function(content,callback){
812 const fd = new FormData();
813 fd.append('foo', ...);
814 fossil.fetch('preview_forumpost',{
815 payload: fd,
816 onload: callback,
817 onerror: (e)=>{ // only if app-specific handling is needed
818 fossil.fetch.onerror(e); // default impl
819 ... any app-specific error reporting ...
820 }
821 });
822 };
823
824 Then connect the parts with:
825
826 fossil.connectPagePreviewers('#test-preview-connector');
827
828 Note that the data-f-preview-from, data-f-preview-via, and
829 data-f-preview-to selector are not resolved until the button is
830 actually clicked, so they need not exist in the DOM at the
831 instant when the connection is set up, so long as they can be
832 resolved when the preview-refreshing element is clicked.
833
834 Maintenance reminder: this method is not strictly part of
835 fossil.dom, but is in its file because it needs access to
836 dom.parseHtml() to avoid an innerHTML assignment and all code
837 which uses this routine also needs fossil.dom.
838 */
839 F.connectPagePreviewers = function f(selector,methodNamespace){
840 if('string'===typeof selector){
841 selector = document.querySelectorAll(selector);
842 }else if(!selector.forEach){
843 selector = [selector];
844 }
845 if(!methodNamespace){
846 methodNamespace = F.page;
847 }
848 selector.forEach(function(e){
849 e.addEventListener(
850 'click', function(r){
851 const eTo = '#'===e.dataset.fPreviewTo[0]
852 ? document.querySelector(e.dataset.fPreviewTo)
853 : methodNamespace[e.dataset.fPreviewTo],
854 eFrom = '#'===e.dataset.fPreviewFrom[0]
855 ? document.querySelector(e.dataset.fPreviewFrom)
856 : methodNamespace[e.dataset.fPreviewFrom],
857 asText = +(e.dataset.fPreviewAsText || 0);
858 eTo.textContent = "Fetching preview...";
859 methodNamespace[e.dataset.fPreviewVia](
860 (eFrom instanceof Function ? eFrom() : eFrom.value),
861 function(r){
862 if(eTo instanceof Function) eTo(r||'');
863 else if(!r){
864 dom.clearElement(eTo);
865 }else if(asText){
866 eTo.textContent = r;
867 }else{
868 dom.parseHtml(dom.clearElement(eTo), r);
869 }
870 }
871 );
872 }, false
873 );
874 });
875 return this;
876 }/*F.connectPagePreviewers()*/;
877
878 return F.dom = dom;
879 })(window.fossil);
880
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -1116,11 +1116,11 @@
11161116
if(!affirmHasFile()) return this;
11171117
const target = this.e.previewTarget,
11181118
self = this;
11191119
const updateView = function(c){
11201120
D.clearElement(target);
1121
- if('string'===typeof c) target.innerHTML = c;
1121
+ if('string'===typeof c) D.parseHtml(target,c);
11221122
if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
11231123
};
11241124
return this._postPreview(this.fileContent(), updateView);
11251125
};
11261126
@@ -1204,16 +1204,16 @@
12041204
F.message(
12051205
"Fetching diff..."
12061206
).fetch('fileedit/diff',{
12071207
payload: fd,
12081208
onload: function(c){
1209
- target.innerHTML = [
1209
+ D.parseHtml(D.clearElement(target),[
12101210
"<div>Diff <code>[",
12111211
self.finfo.checkin,
12121212
"]</code> &rarr; Local Edits</div>",
12131213
c||'No changes.'
1214
- ].join('');
1214
+ ].join(''));
12151215
if(sbs) P.tweakSbsDiffs2();
12161216
F.message('Updated diff.');
12171217
self.tabs.switchToTab(self.e.tabs.diff);
12181218
}
12191219
});
@@ -1236,18 +1236,21 @@
12361236
filename = this.finfo.filename;
12371237
if(!f.onload){
12381238
f.onload = function(c){
12391239
const oldFinfo = JSON.parse(JSON.stringify(self.finfo))
12401240
if(c.manifest){
1241
- target.innerHTML = [
1241
+ D.parseHtml(D.clearElement(target), [
12421242
"<h3>Manifest",
12431243
(c.dryRun?" (dry run)":""),
12441244
": ", F.hashDigits(c.checkin),"</h3>",
1245
- "<code class='fileedit-manifest'>",
1246
- c.manifest,
1245
+ "<pre><code class='fileedit-manifest'>",
1246
+ c.manifest.replace(/</g,'&lt;'),
1247
+ /* ^^^ replace() necessary or this breaks if the manifest
1248
+ comment contains an unclosed HTML tags,
1249
+ e.g. <script> */
12471250
"</code></pre>"
1248
- ].join('');
1251
+ ].join(''));
12491252
delete c.manifest/*so we don't stash this with finfo*/;
12501253
}
12511254
const msg = [
12521255
'Committed',
12531256
c.dryRun ? '(dry run)' : '',
12541257
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -1116,11 +1116,11 @@
1116 if(!affirmHasFile()) return this;
1117 const target = this.e.previewTarget,
1118 self = this;
1119 const updateView = function(c){
1120 D.clearElement(target);
1121 if('string'===typeof c) target.innerHTML = c;
1122 if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
1123 };
1124 return this._postPreview(this.fileContent(), updateView);
1125 };
1126
@@ -1204,16 +1204,16 @@
1204 F.message(
1205 "Fetching diff..."
1206 ).fetch('fileedit/diff',{
1207 payload: fd,
1208 onload: function(c){
1209 target.innerHTML = [
1210 "<div>Diff <code>[",
1211 self.finfo.checkin,
1212 "]</code> &rarr; Local Edits</div>",
1213 c||'No changes.'
1214 ].join('');
1215 if(sbs) P.tweakSbsDiffs2();
1216 F.message('Updated diff.');
1217 self.tabs.switchToTab(self.e.tabs.diff);
1218 }
1219 });
@@ -1236,18 +1236,21 @@
1236 filename = this.finfo.filename;
1237 if(!f.onload){
1238 f.onload = function(c){
1239 const oldFinfo = JSON.parse(JSON.stringify(self.finfo))
1240 if(c.manifest){
1241 target.innerHTML = [
1242 "<h3>Manifest",
1243 (c.dryRun?" (dry run)":""),
1244 ": ", F.hashDigits(c.checkin),"</h3>",
1245 "<code class='fileedit-manifest'>",
1246 c.manifest,
 
 
 
1247 "</code></pre>"
1248 ].join('');
1249 delete c.manifest/*so we don't stash this with finfo*/;
1250 }
1251 const msg = [
1252 'Committed',
1253 c.dryRun ? '(dry run)' : '',
1254
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -1116,11 +1116,11 @@
1116 if(!affirmHasFile()) return this;
1117 const target = this.e.previewTarget,
1118 self = this;
1119 const updateView = function(c){
1120 D.clearElement(target);
1121 if('string'===typeof c) D.parseHtml(target,c);
1122 if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
1123 };
1124 return this._postPreview(this.fileContent(), updateView);
1125 };
1126
@@ -1204,16 +1204,16 @@
1204 F.message(
1205 "Fetching diff..."
1206 ).fetch('fileedit/diff',{
1207 payload: fd,
1208 onload: function(c){
1209 D.parseHtml(D.clearElement(target),[
1210 "<div>Diff <code>[",
1211 self.finfo.checkin,
1212 "]</code> &rarr; Local Edits</div>",
1213 c||'No changes.'
1214 ].join(''));
1215 if(sbs) P.tweakSbsDiffs2();
1216 F.message('Updated diff.');
1217 self.tabs.switchToTab(self.e.tabs.diff);
1218 }
1219 });
@@ -1236,18 +1236,21 @@
1236 filename = this.finfo.filename;
1237 if(!f.onload){
1238 f.onload = function(c){
1239 const oldFinfo = JSON.parse(JSON.stringify(self.finfo))
1240 if(c.manifest){
1241 D.parseHtml(D.clearElement(target), [
1242 "<h3>Manifest",
1243 (c.dryRun?" (dry run)":""),
1244 ": ", F.hashDigits(c.checkin),"</h3>",
1245 "<pre><code class='fileedit-manifest'>",
1246 c.manifest.replace(/</g,'&lt;'),
1247 /* ^^^ replace() necessary or this breaks if the manifest
1248 comment contains an unclosed HTML tags,
1249 e.g. <script> */
1250 "</code></pre>"
1251 ].join(''));
1252 delete c.manifest/*so we don't stash this with finfo*/;
1253 }
1254 const msg = [
1255 'Committed',
1256 c.dryRun ? '(dry run)' : '',
1257
--- src/fossil.page.pikchrshow.js
+++ src/fossil.page.pikchrshow.js
@@ -256,25 +256,14 @@
256256
f.getMarkupAlignmentClass = function(){
257257
if(P.e.markupAlignCenter.checked) return ' center';
258258
else if(P.e.markupAlignIndent.checked) return ' indent';
259259
return '';
260260
};
261
- /* Parses P.response.raw as HTML without using innerHTML. */
262
- f.parseResponse = function(tgt){
263
- let childs;
264
- if(P.response.raw){
265
- const newNode = new DOMParser().parseFromString(P.response.raw, 'text/html');
266
- childs = newNode.documentElement.querySelectorAll('body > *');
267
- }else{
268
- childs = [];
269
- }
270
- D.append(D.clearElement(tgt), childs);
271
- };
272261
}
273262
const preTgt = this.e.previewTarget;
274263
if(this.response.isError){
275
- f.parseResponse(preTgt);
264
+ D.append(D.clearElement(preTgt), D.parseHtml(P.response.raw));
276265
D.addClass(preTgt, 'error');
277266
this.e.previewModeLabel.innerText = "Error";
278267
return;
279268
}
280269
D.removeClass(preTgt, 'error');
@@ -284,11 +273,11 @@
284273
let label;
285274
switch(this.previewMode){
286275
case 0:
287276
label = "SVG";
288277
f.showMarkupAlignment(false);
289
- f.parseResponse(preTgt);
278
+ D.append(D.clearElement(preTgt), D.parseHtml(P.response.raw));
290279
this.e.taPreviewText.value =
291280
this.response.raw.replace(f.rxNonce, '')/*for copy button*/;
292281
break;
293282
case 1:
294283
label = "Markdown";
295284
--- src/fossil.page.pikchrshow.js
+++ src/fossil.page.pikchrshow.js
@@ -256,25 +256,14 @@
256 f.getMarkupAlignmentClass = function(){
257 if(P.e.markupAlignCenter.checked) return ' center';
258 else if(P.e.markupAlignIndent.checked) return ' indent';
259 return '';
260 };
261 /* Parses P.response.raw as HTML without using innerHTML. */
262 f.parseResponse = function(tgt){
263 let childs;
264 if(P.response.raw){
265 const newNode = new DOMParser().parseFromString(P.response.raw, 'text/html');
266 childs = newNode.documentElement.querySelectorAll('body > *');
267 }else{
268 childs = [];
269 }
270 D.append(D.clearElement(tgt), childs);
271 };
272 }
273 const preTgt = this.e.previewTarget;
274 if(this.response.isError){
275 f.parseResponse(preTgt);
276 D.addClass(preTgt, 'error');
277 this.e.previewModeLabel.innerText = "Error";
278 return;
279 }
280 D.removeClass(preTgt, 'error');
@@ -284,11 +273,11 @@
284 let label;
285 switch(this.previewMode){
286 case 0:
287 label = "SVG";
288 f.showMarkupAlignment(false);
289 f.parseResponse(preTgt);
290 this.e.taPreviewText.value =
291 this.response.raw.replace(f.rxNonce, '')/*for copy button*/;
292 break;
293 case 1:
294 label = "Markdown";
295
--- src/fossil.page.pikchrshow.js
+++ src/fossil.page.pikchrshow.js
@@ -256,25 +256,14 @@
256 f.getMarkupAlignmentClass = function(){
257 if(P.e.markupAlignCenter.checked) return ' center';
258 else if(P.e.markupAlignIndent.checked) return ' indent';
259 return '';
260 };
 
 
 
 
 
 
 
 
 
 
 
261 }
262 const preTgt = this.e.previewTarget;
263 if(this.response.isError){
264 D.append(D.clearElement(preTgt), D.parseHtml(P.response.raw));
265 D.addClass(preTgt, 'error');
266 this.e.previewModeLabel.innerText = "Error";
267 return;
268 }
269 D.removeClass(preTgt, 'error');
@@ -284,11 +273,11 @@
273 let label;
274 switch(this.previewMode){
275 case 0:
276 label = "SVG";
277 f.showMarkupAlignment(false);
278 D.append(D.clearElement(preTgt), D.parseHtml(P.response.raw));
279 this.e.taPreviewText.value =
280 this.response.raw.replace(f.rxNonce, '')/*for copy button*/;
281 break;
282 case 1:
283 label = "Markdown";
284
--- src/fossil.page.wikiedit-wysiwyg-legacy.js
+++ src/fossil.page.wikiedit-wysiwyg-legacy.js
@@ -444,11 +444,11 @@
444444
D.append(D.clearElement(oDoc), content)
445445
oDoc.style.whiteSpace = "pre-wrap";
446446
D.addClass(setDocMode.toHide, 'hidden');
447447
} else {
448448
/* Markup -> WYSIWYG */
449
- oDoc.innerHTML = content;
449
+ D.parseHtml(D.clearElement(oDoc), content);
450450
oDoc.style.whiteSpace = "normal";
451451
D.removeClass(setDocMode.toHide, 'hidden');
452452
}
453453
oDoc.focus();
454454
}
455455
--- src/fossil.page.wikiedit-wysiwyg-legacy.js
+++ src/fossil.page.wikiedit-wysiwyg-legacy.js
@@ -444,11 +444,11 @@
444 D.append(D.clearElement(oDoc), content)
445 oDoc.style.whiteSpace = "pre-wrap";
446 D.addClass(setDocMode.toHide, 'hidden');
447 } else {
448 /* Markup -> WYSIWYG */
449 oDoc.innerHTML = content;
450 oDoc.style.whiteSpace = "normal";
451 D.removeClass(setDocMode.toHide, 'hidden');
452 }
453 oDoc.focus();
454 }
455
--- src/fossil.page.wikiedit-wysiwyg-legacy.js
+++ src/fossil.page.wikiedit-wysiwyg-legacy.js
@@ -444,11 +444,11 @@
444 D.append(D.clearElement(oDoc), content)
445 oDoc.style.whiteSpace = "pre-wrap";
446 D.addClass(setDocMode.toHide, 'hidden');
447 } else {
448 /* Markup -> WYSIWYG */
449 D.parseHtml(D.clearElement(oDoc), content);
450 oDoc.style.whiteSpace = "normal";
451 D.removeClass(setDocMode.toHide, 'hidden');
452 }
453 oDoc.focus();
454 }
455
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1315,13 +1315,13 @@
13151315
*/
13161316
P.preview = function f(switchToTab){
13171317
if(!affirmPageLoaded()) return this;
13181318
const target = this.e.previewTarget,
13191319
self = this;
1320
- const updateView = function(c){
1320
+ const updateView = function(c,mimetype){
13211321
D.clearElement(target);
1322
- if('string'===typeof c) target.innerHTML = c;
1322
+ if('string'===typeof c) D.parseHtml(target,c);
13231323
if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
13241324
};
13251325
return this._postPreview(this.wikiContent(), updateView);
13261326
};
13271327
@@ -1398,16 +1398,16 @@
13981398
F.message(
13991399
"Fetching diff..."
14001400
).fetch('wikiajax/diff',{
14011401
payload: fd,
14021402
onload: function(c){
1403
- target.innerHTML = [
1403
+ D.parseHtml(D.clearElement(target), [
14041404
"<div>Diff <code>[",
14051405
self.winfo.name,
14061406
"]</code> &rarr; Local Edits</div>",
14071407
c||'No changes.'
1408
- ].join('');
1408
+ ].join(''));
14091409
if(sbs) P.tweakSbsDiffs2();
14101410
F.message('Updated diff.');
14111411
self.tabs.switchToTab(self.e.tabs.diff);
14121412
}
14131413
});
14141414
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1315,13 +1315,13 @@
1315 */
1316 P.preview = function f(switchToTab){
1317 if(!affirmPageLoaded()) return this;
1318 const target = this.e.previewTarget,
1319 self = this;
1320 const updateView = function(c){
1321 D.clearElement(target);
1322 if('string'===typeof c) target.innerHTML = c;
1323 if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
1324 };
1325 return this._postPreview(this.wikiContent(), updateView);
1326 };
1327
@@ -1398,16 +1398,16 @@
1398 F.message(
1399 "Fetching diff..."
1400 ).fetch('wikiajax/diff',{
1401 payload: fd,
1402 onload: function(c){
1403 target.innerHTML = [
1404 "<div>Diff <code>[",
1405 self.winfo.name,
1406 "]</code> &rarr; Local Edits</div>",
1407 c||'No changes.'
1408 ].join('');
1409 if(sbs) P.tweakSbsDiffs2();
1410 F.message('Updated diff.');
1411 self.tabs.switchToTab(self.e.tabs.diff);
1412 }
1413 });
1414
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1315,13 +1315,13 @@
1315 */
1316 P.preview = function f(switchToTab){
1317 if(!affirmPageLoaded()) return this;
1318 const target = this.e.previewTarget,
1319 self = this;
1320 const updateView = function(c,mimetype){
1321 D.clearElement(target);
1322 if('string'===typeof c) D.parseHtml(target,c);
1323 if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
1324 };
1325 return this._postPreview(this.wikiContent(), updateView);
1326 };
1327
@@ -1398,16 +1398,16 @@
1398 F.message(
1399 "Fetching diff..."
1400 ).fetch('wikiajax/diff',{
1401 payload: fd,
1402 onload: function(c){
1403 D.parseHtml(D.clearElement(target), [
1404 "<div>Diff <code>[",
1405 self.winfo.name,
1406 "]</code> &rarr; Local Edits</div>",
1407 c||'No changes.'
1408 ].join(''));
1409 if(sbs) P.tweakSbsDiffs2();
1410 F.message('Updated diff.');
1411 self.tabs.switchToTab(self.e.tabs.diff);
1412 }
1413 });
1414

Keyboard Shortcuts

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