Fossil SCM

/wikiedit now marks "deleted" (empty) pages and offers a filter to show/hide them.

stephan 2020-08-08 11:29 trunk
Commit 424baf1e10b530c145c33ba06fef226529379977102d3ce1a4444d44822ab364
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -17,11 +17,12 @@
1717
name: string,
1818
mimetype: mimetype string,
1919
type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
2020
version: UUID string or null for a sandbox page or new page,
2121
parent: parent UUID string or null if no parent,
22
- content: string
22
+ isEmpty: true if page has no content (is "deleted").
23
+ content: string, optional in most contexts
2324
}
2425
2526
The internal docs and code frequently use the term "winfo", and such
2627
references refer to an object with that form.
2728
@@ -172,12 +173,14 @@
172173
record.mimetype = winfo.mimetype;
173174
record.type = winfo.type;
174175
record.parent = winfo.parent;
175176
record.version = winfo.version;
176177
record.stashTime = new Date().getTime();
178
+ record.isEmpty = !!winfo.isEmpty;
177179
this.storeIndex();
178180
if(arguments.length>1){
181
+ if(content) delete record.isEmpty;
179182
F.storage.set(this.contentKey(key), content);
180183
}
181184
this._fireStashEvent();
182185
return this;
183186
},
@@ -264,24 +267,31 @@
264267
// Force UI update
265268
s.dispatchEvent(new Event('change',{target:s}));
266269
}
267270
};
268271
269
- /** Internal helper to get an edit status indicator for the given winfo object. */
272
+ /**
273
+ Internal helper to get an edit status indicator for the given
274
+ winfo object.
275
+ */
270276
const getEditMarker = function f(winfo, textOnly){
271277
const esm = F.config.editStateMarkers;
272
- if(1===winfo){ /* force is-new */
278
+ if(f.NEW===winfo){ /* force is-new */
273279
return textOnly ? esm.isNew :
274280
D.addClass(D.append(D.span(),esm.isNew), 'is-new');
275
- }else if(2===winfo){ /* force is-modified */
281
+ }else if(f.MODIFIED===winfo){ /* force is-modified */
276282
return textOnly ? esm.isModified :
277283
D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
284
+ }else if(f.DELETED===winfo){/* force is-deleted */
285
+ return textOnly ? esm.isDeleted :
286
+ D.addClass(D.append(D.span(),esm.isDeleted), 'is-deleted');
278287
}else if(winfo && winfo.version){ /* is existing page modified? */
279288
if($stash.getWinfo(winfo)){
280289
return textOnly ? esm.isModified :
281290
D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
282291
}
292
+ /*fall through*/
283293
}
284294
else if(winfo){ /* is new non-sandbox or is modified sandbox? */
285295
if('sandbox'!==winfo.type){
286296
return textOnly ? esm.isNew :
287297
D.addClass(D.append(D.span(),esm.isNew), 'is-new');
@@ -290,10 +300,21 @@
290300
D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
291301
}
292302
}
293303
return textOnly ? '' : D.span();
294304
};
305
+ getEditMarker.NEW = 1;
306
+ getEditMarker.MODIFIED = 2;
307
+ getEditMarker.DELETED = 3;
308
+
309
+ /**
310
+ Returns true if the given winfo object appears to be "new", else
311
+ returns false.
312
+ */
313
+ const winfoIsNew = function(winfo){
314
+ return 'sandbox'===winfo.type ? false : !winfo.version;
315
+ };
295316
296317
/**
297318
Sets up and maintains the widgets for the list of wiki pages.
298319
*/
299320
const WikiList = {
@@ -303,10 +324,12 @@
303324
except for "sandbox" type, which is assumed to be covered by
304325
the "normal" type filter. */},
305326
},
306327
cache: {
307328
pageList: [],
329
+ optByName:{/*map of page names to OPTION object, to speed up
330
+ certain operations.*/},
308331
names: {
309332
/* Map of page names to "something." We don't map to their
310333
winfo bits because those regularly get swapped out via
311334
de/serialization. We need this map to support the add-new-page
312335
feature, to give us a way to check for dupes without asking
@@ -316,38 +339,49 @@
316339
/**
317340
Updates OPTION elements to reflect whether the page has local
318341
changes or is new/unsaved. This implementation is horribly
319342
inefficient, in that we have to walk and validate the whole
320343
list for each stash-level change.
344
+
345
+ If passed an argument, it is assumed to be an OPTION element
346
+ and only that element is updated, else all OPTION elements
347
+ in this.e.select are updated.
321348
322349
Reminder to self: in order to mark is-edited/is-new state we
323350
have to update the OPTION element's inner text to reflect the
324351
is-modified/is-new flags, rather than use CSS classes to tag
325352
them, because mobile Chrome can neither restyle OPTION elements
326353
no render ::before content on them. We *also* use CSS tags, but
327354
they aren't sufficient for the mobile browsers.
328355
*/
329
- _refreshStashMarks: function callee(){
356
+ _refreshStashMarks: function callee(option){
330357
if(!callee.eachOpt){
331358
const self = this;
332
- callee.eachOpt = function(key){
333
- const opt = self.e.select.options[key];
359
+ callee.eachOpt = function(keyOrOpt){
360
+ const opt = 'string'===typeof keyOrOpt ? self.e.select.options[keyOrOpt] : keyOrOpt;
334361
const stashed = $stash.getWinfo({name:opt.value});
335362
var prefix = '';
363
+ D.removeClass(opt, 'stashed', 'stashed-new', 'deleted');
336364
if(stashed){
337
- const isNew = 'sandbox'===stashed.type ? false : !stashed.version;
338
- prefix = getEditMarker(isNew ? 1 : 2, true);
365
+ const isNew = winfoIsNew(stashed);
366
+ prefix = getEditMarker(isNew ? getEditMarker.NEW : getEditMarker.MODIFIED, true);
339367
D.addClass(opt, isNew ? 'stashed-new' : 'stashed');
340
- }else{
341
- D.removeClass(opt, 'stashed', 'stashed-new');
368
+ D.removeClass(opt, 'deleted');
369
+ }else if(opt.dataset.isDeleted){
370
+ prefix = getEditMarker(getEditMarker.DELETED,true);
371
+ D.addClass(opt, 'deleted');
342372
}
343373
opt.innerText = prefix + opt.value;
344374
self.cache.names[opt.value] = true;
345375
};
346376
}
347
- this.cache.names = {/*must reset it to acount for local page removals*/};
348
- Object.keys(this.e.select.options).forEach(callee.eachOpt);
377
+ if(arguments.length){
378
+ callee.eachOpt(option);
379
+ }else{
380
+ this.cache.names = {/*must reset it to acount for local page removals*/};
381
+ Object.keys(this.e.select.options).forEach(callee.eachOpt);
382
+ }
349383
},
350384
/** Removes the given wiki page entry from the page selection
351385
list, if it's in the list. */
352386
removeEntry: function(name){
353387
const sel = this.e.select;
@@ -357,16 +391,18 @@
357391
if(ndx === sel.selectedIndex) ndx = -1;
358392
sel.options.remove(sel.selectedIndex);
359393
}
360394
sel.selectedIndex = ndx;
361395
delete this.cache.names[name];
396
+ delete this.cache.optByName[name];
362397
this.cache.pageList = this.cache.pageList.filter((wi)=>name !== wi.name);
363398
},
364399
365400
/**
366401
Rebuilds the selection list. Necessary when it's loaded from
367
- the server or we locally create a new page.
402
+ the server, we locally create a new page, or we remove a
403
+ locally-created new page.
368404
*/
369405
_rebuildList: function callee(){
370406
/* Jump through some hoops to integrate new/unsaved
371407
pages into the list of existing pages... We use a map
372408
as an intermediary in order to filter out any local-stash
@@ -395,20 +431,23 @@
395431
const winfo = map[name];
396432
const opt = D.option(sel, winfo.name);
397433
const wtype = opt.dataset.wtype =
398434
winfo.type==='sandbox' ? 'normal' : (winfo.type||'normal');
399435
const cb = self.e.filterCheckboxes[wtype];
436
+ self.cache.optByName[winfo.name] = opt;
400437
if(cb && !cb.checked) D.addClass(opt, 'hidden');
438
+ if(winfo.isEmpty){
439
+ opt.dataset.isDeleted = true;
440
+ }
441
+ self._refreshStashMarks(opt);
401442
});
402443
D.enable(sel);
403444
if(P.winfo) sel.value = P.winfo.name;
404
- this._refreshStashMarks();
405445
},
406446
407447
/** Loads the page list and populates the selection list. */
408448
loadList: function callee(){
409
- delete this.pageMap;
410449
if(!callee.onload){
411450
const self = this;
412451
callee.onload = function(list){
413452
self.cache.pageList = list;
414453
self._rebuildList();
@@ -508,48 +547,69 @@
508547
509548
/** Set up filter checkboxes for the various types
510549
of wiki pages... */
511550
const fsFilter = D.fieldset("Page types"),
512551
fsFilterBody = D.div(),
513
- filters = ['normal', 'branch', 'checkin', 'tag']
552
+ filters = ['normal', 'branch/...', 'tag/...', 'checkin/...']
514553
;
515554
D.append(fsFilter, fsFilterBody);
516555
D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
517
- const filterSelection = function(wtype, show){
556
+
557
+ // Add filters by page type...
558
+ const self = this;
559
+ const filterByType = function(wtype, show){
518560
sel.querySelectorAll('option[data-wtype='+wtype+']').forEach(function(opt){
519561
if(show) opt.classList.remove('hidden');
520562
else opt.classList.add('hidden');
521563
});
522564
};
523
- const self = this;
524
- filters.forEach(function(wtype){
565
+ filters.forEach(function(label){
566
+ const wtype = label.split('/')[0];
525567
const cbId = 'wtype-filter-'+wtype,
526
- lbl = D.attr(D.append(D.label(),wtype),
568
+ lbl = D.attr(D.append(D.label(),label),
527569
'for', cbId),
528
- cb = D.attr(D.input('checkbox'), 'id', cbId),
529
- span = D.append(D.span(), cb, lbl);
570
+ cb = D.attr(D.input('checkbox'), 'id', cbId);
571
+ D.append(fsFilterBody, D.append(D.span(), cb, lbl));
530572
self.e.filterCheckboxes[wtype] = cb;
531573
cb.checked = true;
532
- filterSelection(wtype, cb.checked);
574
+ filterByType(wtype, cb.checked);
533575
cb.addEventListener(
534576
'change',
535
- function(ev){filterSelection(wtype, ev.target.checked)},
577
+ function(ev){filterByType(wtype, ev.target.checked)},
536578
false
537579
);
538
- D.append(fsFilterBody, span);
539580
});
540
-
581
+ { /* add filter for "deleted" pages */
582
+ const cbId = 'wtype-filter-deleted',
583
+ lbl = D.attr(D.append(D.label(),
584
+ getEditMarker(getEditMarker.DELETED,false),
585
+ 'deleted'),
586
+ 'for', cbId),
587
+ cb = D.attr(D.input('checkbox'), 'id', cbId);
588
+ cb.checked = true;
589
+ D.attr(lbl, 'title',
590
+ 'Fossil considers empty pages to be "deleted" in some contexts.');
591
+ D.append(fsFilterBody, D.append(D.span(), cb, lbl));
592
+ cb.addEventListener(
593
+ 'change',
594
+ function(ev){
595
+ if(ev.target.checked) D.removeClass(parentElem,'hide-deleted');
596
+ else D.addClass(parentElem,'hide-deleted');
597
+ },
598
+ false);
599
+ }
541600
/* A legend of the meanings of the symbols we use in
542601
the OPTION elements to denote certain state. */
543602
const fsLegend = D.fieldset("Edit status"),
544603
fsLegendBody = D.div();
545604
D.append(fsLegend, fsLegendBody);
546605
D.addClass(fsLegendBody, 'flex-container', 'flex-column', 'stretch');
547606
D.append(
548607
fsLegendBody,
549
- D.append(D.span(), getEditMarker(1,false)," = page is new/unsaved"),
550
- D.append(D.span(), getEditMarker(2,false)," = page has local edits")
608
+ D.append(D.span(), getEditMarker(getEditMarker.NEW,false)," = page is new/unsaved"),
609
+ D.append(D.span(), getEditMarker(getEditMarker.MODIFIED,false)," = page has local edits"),
610
+ D.append(D.span(), getEditMarker(getEditMarker.DELETED,false)," = page is empty (deleted)")
551611
);
552612
553613
const fsNewPage = D.fieldset("Create new page"),
554614
fsNewPageBody = D.div(),
555615
newPageName = D.input('text'),
@@ -582,10 +642,22 @@
582642
sel.addEventListener('change', onSelect, false);
583643
sel.addEventListener('dblclick', onSelect, false);
584644
F.page.addEventListener('wiki-stash-updated', ()=>{
585645
if(P.winfo) this._refreshStashMarks();
586646
else this._rebuildList();
647
+ });
648
+ F.page.addEventListener('wiki-page-loaded', function(ev){
649
+ /* Needed to handle the saved-an-empty-page case. */
650
+ const page = ev.detail,
651
+ opt = self.cache.optByName[page.name];
652
+ if(opt){
653
+ if(page.isEmpty) opt.dataset.isDeleted = true;
654
+ else delete opt.dataset.isDeleted;
655
+ self._refreshStashMarks(opt);
656
+ }else{
657
+ F.error("BUG: internal mis-handling of page object: missing OPTION for page "+page.name);
658
+ }
587659
});
588660
delete this.init;
589661
}
590662
};
591663
@@ -947,10 +1019,11 @@
9471019
false
9481020
);
9491021
/* These init()s need to come after P's event handlers are registered */
9501022
WikiList.init( P.e.tabs.pageList.firstElementChild );
9511023
P.stashWidget.init(P.e.tabs.content.lastElementChild);
1024
+ //P.$wikiList = WikiList/*only for testing/debugging*/;
9521025
}/*F.onPageLoad()*/);
9531026
9541027
/**
9551028
Returns true if fossil.page.winfo is set, indicating that a page
9561029
has been loaded, else it reports an error and returns false.
@@ -1119,10 +1192,11 @@
11191192
name: stashWinfo.name,
11201193
mimetype: stashWinfo.mimetype,
11211194
type: stashWinfo.type,
11221195
version: stashWinfo.version,
11231196
parent: stashWinfo.parent,
1197
+ isEmpty: !!stashWinfo.isEmpty,
11241198
content: $stash.stashedContent(stashWinfo)
11251199
});
11261200
return this;
11271201
}
11281202
F.message(
11291203
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -17,11 +17,12 @@
17 name: string,
18 mimetype: mimetype string,
19 type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20 version: UUID string or null for a sandbox page or new page,
21 parent: parent UUID string or null if no parent,
22 content: string
 
23 }
24
25 The internal docs and code frequently use the term "winfo", and such
26 references refer to an object with that form.
27
@@ -172,12 +173,14 @@
172 record.mimetype = winfo.mimetype;
173 record.type = winfo.type;
174 record.parent = winfo.parent;
175 record.version = winfo.version;
176 record.stashTime = new Date().getTime();
 
177 this.storeIndex();
178 if(arguments.length>1){
 
179 F.storage.set(this.contentKey(key), content);
180 }
181 this._fireStashEvent();
182 return this;
183 },
@@ -264,24 +267,31 @@
264 // Force UI update
265 s.dispatchEvent(new Event('change',{target:s}));
266 }
267 };
268
269 /** Internal helper to get an edit status indicator for the given winfo object. */
 
 
 
270 const getEditMarker = function f(winfo, textOnly){
271 const esm = F.config.editStateMarkers;
272 if(1===winfo){ /* force is-new */
273 return textOnly ? esm.isNew :
274 D.addClass(D.append(D.span(),esm.isNew), 'is-new');
275 }else if(2===winfo){ /* force is-modified */
276 return textOnly ? esm.isModified :
277 D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
 
 
 
278 }else if(winfo && winfo.version){ /* is existing page modified? */
279 if($stash.getWinfo(winfo)){
280 return textOnly ? esm.isModified :
281 D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
282 }
 
283 }
284 else if(winfo){ /* is new non-sandbox or is modified sandbox? */
285 if('sandbox'!==winfo.type){
286 return textOnly ? esm.isNew :
287 D.addClass(D.append(D.span(),esm.isNew), 'is-new');
@@ -290,10 +300,21 @@
290 D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
291 }
292 }
293 return textOnly ? '' : D.span();
294 };
 
 
 
 
 
 
 
 
 
 
 
295
296 /**
297 Sets up and maintains the widgets for the list of wiki pages.
298 */
299 const WikiList = {
@@ -303,10 +324,12 @@
303 except for "sandbox" type, which is assumed to be covered by
304 the "normal" type filter. */},
305 },
306 cache: {
307 pageList: [],
 
 
308 names: {
309 /* Map of page names to "something." We don't map to their
310 winfo bits because those regularly get swapped out via
311 de/serialization. We need this map to support the add-new-page
312 feature, to give us a way to check for dupes without asking
@@ -316,38 +339,49 @@
316 /**
317 Updates OPTION elements to reflect whether the page has local
318 changes or is new/unsaved. This implementation is horribly
319 inefficient, in that we have to walk and validate the whole
320 list for each stash-level change.
 
 
 
 
321
322 Reminder to self: in order to mark is-edited/is-new state we
323 have to update the OPTION element's inner text to reflect the
324 is-modified/is-new flags, rather than use CSS classes to tag
325 them, because mobile Chrome can neither restyle OPTION elements
326 no render ::before content on them. We *also* use CSS tags, but
327 they aren't sufficient for the mobile browsers.
328 */
329 _refreshStashMarks: function callee(){
330 if(!callee.eachOpt){
331 const self = this;
332 callee.eachOpt = function(key){
333 const opt = self.e.select.options[key];
334 const stashed = $stash.getWinfo({name:opt.value});
335 var prefix = '';
 
336 if(stashed){
337 const isNew = 'sandbox'===stashed.type ? false : !stashed.version;
338 prefix = getEditMarker(isNew ? 1 : 2, true);
339 D.addClass(opt, isNew ? 'stashed-new' : 'stashed');
340 }else{
341 D.removeClass(opt, 'stashed', 'stashed-new');
 
 
342 }
343 opt.innerText = prefix + opt.value;
344 self.cache.names[opt.value] = true;
345 };
346 }
347 this.cache.names = {/*must reset it to acount for local page removals*/};
348 Object.keys(this.e.select.options).forEach(callee.eachOpt);
 
 
 
 
349 },
350 /** Removes the given wiki page entry from the page selection
351 list, if it's in the list. */
352 removeEntry: function(name){
353 const sel = this.e.select;
@@ -357,16 +391,18 @@
357 if(ndx === sel.selectedIndex) ndx = -1;
358 sel.options.remove(sel.selectedIndex);
359 }
360 sel.selectedIndex = ndx;
361 delete this.cache.names[name];
 
362 this.cache.pageList = this.cache.pageList.filter((wi)=>name !== wi.name);
363 },
364
365 /**
366 Rebuilds the selection list. Necessary when it's loaded from
367 the server or we locally create a new page.
 
368 */
369 _rebuildList: function callee(){
370 /* Jump through some hoops to integrate new/unsaved
371 pages into the list of existing pages... We use a map
372 as an intermediary in order to filter out any local-stash
@@ -395,20 +431,23 @@
395 const winfo = map[name];
396 const opt = D.option(sel, winfo.name);
397 const wtype = opt.dataset.wtype =
398 winfo.type==='sandbox' ? 'normal' : (winfo.type||'normal');
399 const cb = self.e.filterCheckboxes[wtype];
 
400 if(cb && !cb.checked) D.addClass(opt, 'hidden');
 
 
 
 
401 });
402 D.enable(sel);
403 if(P.winfo) sel.value = P.winfo.name;
404 this._refreshStashMarks();
405 },
406
407 /** Loads the page list and populates the selection list. */
408 loadList: function callee(){
409 delete this.pageMap;
410 if(!callee.onload){
411 const self = this;
412 callee.onload = function(list){
413 self.cache.pageList = list;
414 self._rebuildList();
@@ -508,48 +547,69 @@
508
509 /** Set up filter checkboxes for the various types
510 of wiki pages... */
511 const fsFilter = D.fieldset("Page types"),
512 fsFilterBody = D.div(),
513 filters = ['normal', 'branch', 'checkin', 'tag']
514 ;
515 D.append(fsFilter, fsFilterBody);
516 D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
517 const filterSelection = function(wtype, show){
 
 
 
518 sel.querySelectorAll('option[data-wtype='+wtype+']').forEach(function(opt){
519 if(show) opt.classList.remove('hidden');
520 else opt.classList.add('hidden');
521 });
522 };
523 const self = this;
524 filters.forEach(function(wtype){
525 const cbId = 'wtype-filter-'+wtype,
526 lbl = D.attr(D.append(D.label(),wtype),
527 'for', cbId),
528 cb = D.attr(D.input('checkbox'), 'id', cbId),
529 span = D.append(D.span(), cb, lbl);
530 self.e.filterCheckboxes[wtype] = cb;
531 cb.checked = true;
532 filterSelection(wtype, cb.checked);
533 cb.addEventListener(
534 'change',
535 function(ev){filterSelection(wtype, ev.target.checked)},
536 false
537 );
538 D.append(fsFilterBody, span);
539 });
540
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541 /* A legend of the meanings of the symbols we use in
542 the OPTION elements to denote certain state. */
543 const fsLegend = D.fieldset("Edit status"),
544 fsLegendBody = D.div();
545 D.append(fsLegend, fsLegendBody);
546 D.addClass(fsLegendBody, 'flex-container', 'flex-column', 'stretch');
547 D.append(
548 fsLegendBody,
549 D.append(D.span(), getEditMarker(1,false)," = page is new/unsaved"),
550 D.append(D.span(), getEditMarker(2,false)," = page has local edits")
 
551 );
552
553 const fsNewPage = D.fieldset("Create new page"),
554 fsNewPageBody = D.div(),
555 newPageName = D.input('text'),
@@ -582,10 +642,22 @@
582 sel.addEventListener('change', onSelect, false);
583 sel.addEventListener('dblclick', onSelect, false);
584 F.page.addEventListener('wiki-stash-updated', ()=>{
585 if(P.winfo) this._refreshStashMarks();
586 else this._rebuildList();
 
 
 
 
 
 
 
 
 
 
 
 
587 });
588 delete this.init;
589 }
590 };
591
@@ -947,10 +1019,11 @@
947 false
948 );
949 /* These init()s need to come after P's event handlers are registered */
950 WikiList.init( P.e.tabs.pageList.firstElementChild );
951 P.stashWidget.init(P.e.tabs.content.lastElementChild);
 
952 }/*F.onPageLoad()*/);
953
954 /**
955 Returns true if fossil.page.winfo is set, indicating that a page
956 has been loaded, else it reports an error and returns false.
@@ -1119,10 +1192,11 @@
1119 name: stashWinfo.name,
1120 mimetype: stashWinfo.mimetype,
1121 type: stashWinfo.type,
1122 version: stashWinfo.version,
1123 parent: stashWinfo.parent,
 
1124 content: $stash.stashedContent(stashWinfo)
1125 });
1126 return this;
1127 }
1128 F.message(
1129
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -17,11 +17,12 @@
17 name: string,
18 mimetype: mimetype string,
19 type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20 version: UUID string or null for a sandbox page or new page,
21 parent: parent UUID string or null if no parent,
22 isEmpty: true if page has no content (is "deleted").
23 content: string, optional in most contexts
24 }
25
26 The internal docs and code frequently use the term "winfo", and such
27 references refer to an object with that form.
28
@@ -172,12 +173,14 @@
173 record.mimetype = winfo.mimetype;
174 record.type = winfo.type;
175 record.parent = winfo.parent;
176 record.version = winfo.version;
177 record.stashTime = new Date().getTime();
178 record.isEmpty = !!winfo.isEmpty;
179 this.storeIndex();
180 if(arguments.length>1){
181 if(content) delete record.isEmpty;
182 F.storage.set(this.contentKey(key), content);
183 }
184 this._fireStashEvent();
185 return this;
186 },
@@ -264,24 +267,31 @@
267 // Force UI update
268 s.dispatchEvent(new Event('change',{target:s}));
269 }
270 };
271
272 /**
273 Internal helper to get an edit status indicator for the given
274 winfo object.
275 */
276 const getEditMarker = function f(winfo, textOnly){
277 const esm = F.config.editStateMarkers;
278 if(f.NEW===winfo){ /* force is-new */
279 return textOnly ? esm.isNew :
280 D.addClass(D.append(D.span(),esm.isNew), 'is-new');
281 }else if(f.MODIFIED===winfo){ /* force is-modified */
282 return textOnly ? esm.isModified :
283 D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
284 }else if(f.DELETED===winfo){/* force is-deleted */
285 return textOnly ? esm.isDeleted :
286 D.addClass(D.append(D.span(),esm.isDeleted), 'is-deleted');
287 }else if(winfo && winfo.version){ /* is existing page modified? */
288 if($stash.getWinfo(winfo)){
289 return textOnly ? esm.isModified :
290 D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
291 }
292 /*fall through*/
293 }
294 else if(winfo){ /* is new non-sandbox or is modified sandbox? */
295 if('sandbox'!==winfo.type){
296 return textOnly ? esm.isNew :
297 D.addClass(D.append(D.span(),esm.isNew), 'is-new');
@@ -290,10 +300,21 @@
300 D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
301 }
302 }
303 return textOnly ? '' : D.span();
304 };
305 getEditMarker.NEW = 1;
306 getEditMarker.MODIFIED = 2;
307 getEditMarker.DELETED = 3;
308
309 /**
310 Returns true if the given winfo object appears to be "new", else
311 returns false.
312 */
313 const winfoIsNew = function(winfo){
314 return 'sandbox'===winfo.type ? false : !winfo.version;
315 };
316
317 /**
318 Sets up and maintains the widgets for the list of wiki pages.
319 */
320 const WikiList = {
@@ -303,10 +324,12 @@
324 except for "sandbox" type, which is assumed to be covered by
325 the "normal" type filter. */},
326 },
327 cache: {
328 pageList: [],
329 optByName:{/*map of page names to OPTION object, to speed up
330 certain operations.*/},
331 names: {
332 /* Map of page names to "something." We don't map to their
333 winfo bits because those regularly get swapped out via
334 de/serialization. We need this map to support the add-new-page
335 feature, to give us a way to check for dupes without asking
@@ -316,38 +339,49 @@
339 /**
340 Updates OPTION elements to reflect whether the page has local
341 changes or is new/unsaved. This implementation is horribly
342 inefficient, in that we have to walk and validate the whole
343 list for each stash-level change.
344
345 If passed an argument, it is assumed to be an OPTION element
346 and only that element is updated, else all OPTION elements
347 in this.e.select are updated.
348
349 Reminder to self: in order to mark is-edited/is-new state we
350 have to update the OPTION element's inner text to reflect the
351 is-modified/is-new flags, rather than use CSS classes to tag
352 them, because mobile Chrome can neither restyle OPTION elements
353 no render ::before content on them. We *also* use CSS tags, but
354 they aren't sufficient for the mobile browsers.
355 */
356 _refreshStashMarks: function callee(option){
357 if(!callee.eachOpt){
358 const self = this;
359 callee.eachOpt = function(keyOrOpt){
360 const opt = 'string'===typeof keyOrOpt ? self.e.select.options[keyOrOpt] : keyOrOpt;
361 const stashed = $stash.getWinfo({name:opt.value});
362 var prefix = '';
363 D.removeClass(opt, 'stashed', 'stashed-new', 'deleted');
364 if(stashed){
365 const isNew = winfoIsNew(stashed);
366 prefix = getEditMarker(isNew ? getEditMarker.NEW : getEditMarker.MODIFIED, true);
367 D.addClass(opt, isNew ? 'stashed-new' : 'stashed');
368 D.removeClass(opt, 'deleted');
369 }else if(opt.dataset.isDeleted){
370 prefix = getEditMarker(getEditMarker.DELETED,true);
371 D.addClass(opt, 'deleted');
372 }
373 opt.innerText = prefix + opt.value;
374 self.cache.names[opt.value] = true;
375 };
376 }
377 if(arguments.length){
378 callee.eachOpt(option);
379 }else{
380 this.cache.names = {/*must reset it to acount for local page removals*/};
381 Object.keys(this.e.select.options).forEach(callee.eachOpt);
382 }
383 },
384 /** Removes the given wiki page entry from the page selection
385 list, if it's in the list. */
386 removeEntry: function(name){
387 const sel = this.e.select;
@@ -357,16 +391,18 @@
391 if(ndx === sel.selectedIndex) ndx = -1;
392 sel.options.remove(sel.selectedIndex);
393 }
394 sel.selectedIndex = ndx;
395 delete this.cache.names[name];
396 delete this.cache.optByName[name];
397 this.cache.pageList = this.cache.pageList.filter((wi)=>name !== wi.name);
398 },
399
400 /**
401 Rebuilds the selection list. Necessary when it's loaded from
402 the server, we locally create a new page, or we remove a
403 locally-created new page.
404 */
405 _rebuildList: function callee(){
406 /* Jump through some hoops to integrate new/unsaved
407 pages into the list of existing pages... We use a map
408 as an intermediary in order to filter out any local-stash
@@ -395,20 +431,23 @@
431 const winfo = map[name];
432 const opt = D.option(sel, winfo.name);
433 const wtype = opt.dataset.wtype =
434 winfo.type==='sandbox' ? 'normal' : (winfo.type||'normal');
435 const cb = self.e.filterCheckboxes[wtype];
436 self.cache.optByName[winfo.name] = opt;
437 if(cb && !cb.checked) D.addClass(opt, 'hidden');
438 if(winfo.isEmpty){
439 opt.dataset.isDeleted = true;
440 }
441 self._refreshStashMarks(opt);
442 });
443 D.enable(sel);
444 if(P.winfo) sel.value = P.winfo.name;
 
445 },
446
447 /** Loads the page list and populates the selection list. */
448 loadList: function callee(){
 
449 if(!callee.onload){
450 const self = this;
451 callee.onload = function(list){
452 self.cache.pageList = list;
453 self._rebuildList();
@@ -508,48 +547,69 @@
547
548 /** Set up filter checkboxes for the various types
549 of wiki pages... */
550 const fsFilter = D.fieldset("Page types"),
551 fsFilterBody = D.div(),
552 filters = ['normal', 'branch/...', 'tag/...', 'checkin/...']
553 ;
554 D.append(fsFilter, fsFilterBody);
555 D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
556
557 // Add filters by page type...
558 const self = this;
559 const filterByType = function(wtype, show){
560 sel.querySelectorAll('option[data-wtype='+wtype+']').forEach(function(opt){
561 if(show) opt.classList.remove('hidden');
562 else opt.classList.add('hidden');
563 });
564 };
565 filters.forEach(function(label){
566 const wtype = label.split('/')[0];
567 const cbId = 'wtype-filter-'+wtype,
568 lbl = D.attr(D.append(D.label(),label),
569 'for', cbId),
570 cb = D.attr(D.input('checkbox'), 'id', cbId);
571 D.append(fsFilterBody, D.append(D.span(), cb, lbl));
572 self.e.filterCheckboxes[wtype] = cb;
573 cb.checked = true;
574 filterByType(wtype, cb.checked);
575 cb.addEventListener(
576 'change',
577 function(ev){filterByType(wtype, ev.target.checked)},
578 false
579 );
 
580 });
581 { /* add filter for "deleted" pages */
582 const cbId = 'wtype-filter-deleted',
583 lbl = D.attr(D.append(D.label(),
584 getEditMarker(getEditMarker.DELETED,false),
585 'deleted'),
586 'for', cbId),
587 cb = D.attr(D.input('checkbox'), 'id', cbId);
588 cb.checked = true;
589 D.attr(lbl, 'title',
590 'Fossil considers empty pages to be "deleted" in some contexts.');
591 D.append(fsFilterBody, D.append(D.span(), cb, lbl));
592 cb.addEventListener(
593 'change',
594 function(ev){
595 if(ev.target.checked) D.removeClass(parentElem,'hide-deleted');
596 else D.addClass(parentElem,'hide-deleted');
597 },
598 false);
599 }
600 /* A legend of the meanings of the symbols we use in
601 the OPTION elements to denote certain state. */
602 const fsLegend = D.fieldset("Edit status"),
603 fsLegendBody = D.div();
604 D.append(fsLegend, fsLegendBody);
605 D.addClass(fsLegendBody, 'flex-container', 'flex-column', 'stretch');
606 D.append(
607 fsLegendBody,
608 D.append(D.span(), getEditMarker(getEditMarker.NEW,false)," = page is new/unsaved"),
609 D.append(D.span(), getEditMarker(getEditMarker.MODIFIED,false)," = page has local edits"),
610 D.append(D.span(), getEditMarker(getEditMarker.DELETED,false)," = page is empty (deleted)")
611 );
612
613 const fsNewPage = D.fieldset("Create new page"),
614 fsNewPageBody = D.div(),
615 newPageName = D.input('text'),
@@ -582,10 +642,22 @@
642 sel.addEventListener('change', onSelect, false);
643 sel.addEventListener('dblclick', onSelect, false);
644 F.page.addEventListener('wiki-stash-updated', ()=>{
645 if(P.winfo) this._refreshStashMarks();
646 else this._rebuildList();
647 });
648 F.page.addEventListener('wiki-page-loaded', function(ev){
649 /* Needed to handle the saved-an-empty-page case. */
650 const page = ev.detail,
651 opt = self.cache.optByName[page.name];
652 if(opt){
653 if(page.isEmpty) opt.dataset.isDeleted = true;
654 else delete opt.dataset.isDeleted;
655 self._refreshStashMarks(opt);
656 }else{
657 F.error("BUG: internal mis-handling of page object: missing OPTION for page "+page.name);
658 }
659 });
660 delete this.init;
661 }
662 };
663
@@ -947,10 +1019,11 @@
1019 false
1020 );
1021 /* These init()s need to come after P's event handlers are registered */
1022 WikiList.init( P.e.tabs.pageList.firstElementChild );
1023 P.stashWidget.init(P.e.tabs.content.lastElementChild);
1024 //P.$wikiList = WikiList/*only for testing/debugging*/;
1025 }/*F.onPageLoad()*/);
1026
1027 /**
1028 Returns true if fossil.page.winfo is set, indicating that a page
1029 has been loaded, else it reports an error and returns false.
@@ -1119,10 +1192,11 @@
1192 name: stashWinfo.name,
1193 mimetype: stashWinfo.mimetype,
1194 type: stashWinfo.type,
1195 version: stashWinfo.version,
1196 parent: stashWinfo.parent,
1197 isEmpty: !!stashWinfo.isEmpty,
1198 content: $stash.stashedContent(stashWinfo)
1199 });
1200 return this;
1201 }
1202 F.message(
1203
+1 -1
--- src/style.c
+++ src/style.c
@@ -1455,11 +1455,11 @@
14551455
CX("/* Length of UUID hashes for display purposes. */");
14561456
CX("hashDigits: %d, hashDigitsUrl: %d,\n",
14571457
hash_digits(0), hash_digits(1));
14581458
CX("editStateMarkers: {"
14591459
"/*Symbolic markers to denote certain edit states.*/"
1460
- "isNew:'[+]', isModified:'[*]'},\n");
1460
+ "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n");
14611461
CX("confirmerButtonTicks: 3 "
14621462
"/*default fossil.confirmer tick count.*/\n");
14631463
CX("};\n"/* fossil.config */);
14641464
#if 0
14651465
/* Is it safe to emit the CSRF token here? Some pages add it
14661466
--- src/style.c
+++ src/style.c
@@ -1455,11 +1455,11 @@
1455 CX("/* Length of UUID hashes for display purposes. */");
1456 CX("hashDigits: %d, hashDigitsUrl: %d,\n",
1457 hash_digits(0), hash_digits(1));
1458 CX("editStateMarkers: {"
1459 "/*Symbolic markers to denote certain edit states.*/"
1460 "isNew:'[+]', isModified:'[*]'},\n");
1461 CX("confirmerButtonTicks: 3 "
1462 "/*default fossil.confirmer tick count.*/\n");
1463 CX("};\n"/* fossil.config */);
1464 #if 0
1465 /* Is it safe to emit the CSRF token here? Some pages add it
1466
--- src/style.c
+++ src/style.c
@@ -1455,11 +1455,11 @@
1455 CX("/* Length of UUID hashes for display purposes. */");
1456 CX("hashDigits: %d, hashDigitsUrl: %d,\n",
1457 hash_digits(0), hash_digits(1));
1458 CX("editStateMarkers: {"
1459 "/*Symbolic markers to denote certain edit states.*/"
1460 "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n");
1461 CX("confirmerButtonTicks: 3 "
1462 "/*default fossil.confirmer tick count.*/\n");
1463 CX("};\n"/* fossil.config */);
1464 #if 0
1465 /* Is it safe to emit the CSRF token here? Some pages add it
1466
--- src/style.wikiedit.css
+++ src/style.wikiedit.css
@@ -64,12 +64,16 @@
6464
}
6565
body.wikiedit .WikiList select option {
6666
margin: 0 0 0.5em 0.55em;
6767
}
6868
body.wikiedit .WikiList select option.stashed,
69
-body.wikiedit .WikiList select option.stashed-new {
69
+body.wikiedit .WikiList select option.stashed-new,
70
+body.wikiedit .WikiList select option.deleted {
7071
margin-left: -1em;
72
+}
73
+body.wikiedit .WikiList.hide-deleted select option.deleted {
74
+ display: none;
7175
}
7276
body.wikiedit textarea {
7377
max-width: initial;
7478
}
7579
body.wikiedit .tabs .tab-panel {
@@ -142,11 +146,12 @@
142146
143147
body.wikiedit #wikiedit-edit-status > span {
144148
display: block;
145149
}
146150
body.wikiedit .WikiList span.is-new,
147
-body.wikiedit .WikiList span.is-modified {
151
+body.wikiedit .WikiList span.is-modified,
152
+body.wikiedit .WikiList span.is-deleted {
148153
font-family: monospace;
149154
}
150155
body.wikiedit #wikiedit-edit-status span.links > a {
151156
margin: 0 0.25em;
152157
white-space: nowrap;
153158
--- src/style.wikiedit.css
+++ src/style.wikiedit.css
@@ -64,12 +64,16 @@
64 }
65 body.wikiedit .WikiList select option {
66 margin: 0 0 0.5em 0.55em;
67 }
68 body.wikiedit .WikiList select option.stashed,
69 body.wikiedit .WikiList select option.stashed-new {
 
70 margin-left: -1em;
 
 
 
71 }
72 body.wikiedit textarea {
73 max-width: initial;
74 }
75 body.wikiedit .tabs .tab-panel {
@@ -142,11 +146,12 @@
142
143 body.wikiedit #wikiedit-edit-status > span {
144 display: block;
145 }
146 body.wikiedit .WikiList span.is-new,
147 body.wikiedit .WikiList span.is-modified {
 
148 font-family: monospace;
149 }
150 body.wikiedit #wikiedit-edit-status span.links > a {
151 margin: 0 0.25em;
152 white-space: nowrap;
153
--- src/style.wikiedit.css
+++ src/style.wikiedit.css
@@ -64,12 +64,16 @@
64 }
65 body.wikiedit .WikiList select option {
66 margin: 0 0 0.5em 0.55em;
67 }
68 body.wikiedit .WikiList select option.stashed,
69 body.wikiedit .WikiList select option.stashed-new,
70 body.wikiedit .WikiList select option.deleted {
71 margin-left: -1em;
72 }
73 body.wikiedit .WikiList.hide-deleted select option.deleted {
74 display: none;
75 }
76 body.wikiedit textarea {
77 max-width: initial;
78 }
79 body.wikiedit .tabs .tab-panel {
@@ -142,11 +146,12 @@
146
147 body.wikiedit #wikiedit-edit-status > span {
148 display: block;
149 }
150 body.wikiedit .WikiList span.is-new,
151 body.wikiedit .WikiList span.is-modified,
152 body.wikiedit .WikiList span.is-deleted {
153 font-family: monospace;
154 }
155 body.wikiedit #wikiedit-edit-status span.links > a {
156 margin: 0 0.25em;
157 white-space: nowrap;
158
+6 -1
--- src/wiki.c
+++ src/wiki.c
@@ -735,11 +735,13 @@
735735
** { name: "page name",
736736
** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
737737
** mimetype: "mime type",
738738
** version: UUID string or null for a sandbox page,
739739
** parent: "parent uuid" or null if no parent,
740
-** content: "page content"
740
+** isDeleted: true if the page has no content (is "deleted")
741
+** else not set (making it "falsy" in JS),
742
+** content: "page content" (only if includeContent is true)
741743
** }
742744
**
743745
** If includeContent is false then the content member is elided.
744746
*/
745747
static int wiki_ajax_emit_page_object(const char *zPageName,
@@ -778,10 +780,13 @@
778780
CX("\"parent\": ");
779781
if(pWiki->nParent){
780782
CX("%!j", pWiki->azParent[0]);
781783
}else{
782784
CX("null");
785
+ }
786
+ if(!pWiki->zWiki || !pWiki->zWiki[0]){
787
+ CX(", \"isEmpty\": true");
783788
}
784789
if(includeContent){
785790
CX(", \"content\": %!j", pWiki->zWiki);
786791
}
787792
CX("}");
788793
--- src/wiki.c
+++ src/wiki.c
@@ -735,11 +735,13 @@
735 ** { name: "page name",
736 ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
737 ** mimetype: "mime type",
738 ** version: UUID string or null for a sandbox page,
739 ** parent: "parent uuid" or null if no parent,
740 ** content: "page content"
 
 
741 ** }
742 **
743 ** If includeContent is false then the content member is elided.
744 */
745 static int wiki_ajax_emit_page_object(const char *zPageName,
@@ -778,10 +780,13 @@
778 CX("\"parent\": ");
779 if(pWiki->nParent){
780 CX("%!j", pWiki->azParent[0]);
781 }else{
782 CX("null");
 
 
 
783 }
784 if(includeContent){
785 CX(", \"content\": %!j", pWiki->zWiki);
786 }
787 CX("}");
788
--- src/wiki.c
+++ src/wiki.c
@@ -735,11 +735,13 @@
735 ** { name: "page name",
736 ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
737 ** mimetype: "mime type",
738 ** version: UUID string or null for a sandbox page,
739 ** parent: "parent uuid" or null if no parent,
740 ** isDeleted: true if the page has no content (is "deleted")
741 ** else not set (making it "falsy" in JS),
742 ** content: "page content" (only if includeContent is true)
743 ** }
744 **
745 ** If includeContent is false then the content member is elided.
746 */
747 static int wiki_ajax_emit_page_object(const char *zPageName,
@@ -778,10 +780,13 @@
780 CX("\"parent\": ");
781 if(pWiki->nParent){
782 CX("%!j", pWiki->azParent[0]);
783 }else{
784 CX("null");
785 }
786 if(!pWiki->zWiki || !pWiki->zWiki[0]){
787 CX(", \"isEmpty\": true");
788 }
789 if(includeContent){
790 CX(", \"content\": %!j", pWiki->zWiki);
791 }
792 CX("}");
793

Keyboard Shortcuts

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