Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | /wikiedit now marks "deleted" (empty) pages and offers a filter to show/hide them. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
424baf1e10b530c145c33ba06fef2265 |
| User & Date: | stephan 2020-08-08 11:29:56.948 |
Context
|
2020-08-08
| ||
| 12:11 | Reverted a recent modernization in sbsdiff.js because the MSIE<=11 family of browsers do not support NodeList.forEach. ... (check-in: 7f416ef175 user: stephan tags: trunk) | |
| 11:29 | /wikiedit now marks "deleted" (empty) pages and offers a filter to show/hide them. ... (check-in: 424baf1e10 user: stephan tags: trunk) | |
| 01:55 | When doing an open on a URI, verify that the working directory is not within an existing checkout prior to performing the clone. ... (check-in: 19677d7629 user: drh tags: trunk) | |
Changes
Changes to src/fossil.page.wikiedit.js.
| ︙ | ︙ | |||
15 16 17 18 19 20 21 |
{
name: string,
mimetype: mimetype string,
type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
version: UUID string or null for a sandbox page or new page,
parent: parent UUID string or null if no parent,
| > | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
{
name: string,
mimetype: mimetype string,
type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
version: UUID string or null for a sandbox page or new page,
parent: parent UUID string or null if no parent,
isEmpty: true if page has no content (is "deleted").
content: string, optional in most contexts
}
The internal docs and code frequently use the term "winfo", and such
references refer to an object with that form.
The fossil.page.wikiContent() method gets or sets the current
file content for the page.
|
| ︙ | ︙ | |||
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
name: winfo.name
});
record.mimetype = winfo.mimetype;
record.type = winfo.type;
record.parent = winfo.parent;
record.version = winfo.version;
record.stashTime = new Date().getTime();
this.storeIndex();
if(arguments.length>1){
F.storage.set(this.contentKey(key), content);
}
this._fireStashEvent();
return this;
},
/**
Returns the stashed content, if any, for the given winfo
| > > | 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
name: winfo.name
});
record.mimetype = winfo.mimetype;
record.type = winfo.type;
record.parent = winfo.parent;
record.version = winfo.version;
record.stashTime = new Date().getTime();
record.isEmpty = !!winfo.isEmpty;
this.storeIndex();
if(arguments.length>1){
if(content) delete record.isEmpty;
F.storage.set(this.contentKey(key), content);
}
this._fireStashEvent();
return this;
},
/**
Returns the stashed content, if any, for the given winfo
|
| ︙ | ︙ | |||
262 263 264 265 266 267 268 |
}
if(forceEvent){
// Force UI update
s.dispatchEvent(new Event('change',{target:s}));
}
};
| > | > > | | > > > > > > > > > > > > > > > > > > > > > | | | > | | < | > > > > > > | | > > | > | 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
}
if(forceEvent){
// Force UI update
s.dispatchEvent(new Event('change',{target:s}));
}
};
/**
Internal helper to get an edit status indicator for the given
winfo object.
*/
const getEditMarker = function f(winfo, textOnly){
const esm = F.config.editStateMarkers;
if(f.NEW===winfo){ /* force is-new */
return textOnly ? esm.isNew :
D.addClass(D.append(D.span(),esm.isNew), 'is-new');
}else if(f.MODIFIED===winfo){ /* force is-modified */
return textOnly ? esm.isModified :
D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
}else if(f.DELETED===winfo){/* force is-deleted */
return textOnly ? esm.isDeleted :
D.addClass(D.append(D.span(),esm.isDeleted), 'is-deleted');
}else if(winfo && winfo.version){ /* is existing page modified? */
if($stash.getWinfo(winfo)){
return textOnly ? esm.isModified :
D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
}
/*fall through*/
}
else if(winfo){ /* is new non-sandbox or is modified sandbox? */
if('sandbox'!==winfo.type){
return textOnly ? esm.isNew :
D.addClass(D.append(D.span(),esm.isNew), 'is-new');
}else if($stash.getWinfo(winfo)){
return textOnly ? esm.isModified :
D.addClass(D.append(D.span(),esm.isModified), 'is-modified');
}
}
return textOnly ? '' : D.span();
};
getEditMarker.NEW = 1;
getEditMarker.MODIFIED = 2;
getEditMarker.DELETED = 3;
/**
Returns true if the given winfo object appears to be "new", else
returns false.
*/
const winfoIsNew = function(winfo){
return 'sandbox'===winfo.type ? false : !winfo.version;
};
/**
Sets up and maintains the widgets for the list of wiki pages.
*/
const WikiList = {
e: {
filterCheckboxes: {
/*map of wiki page type to checkbox for list filtering purposes,
except for "sandbox" type, which is assumed to be covered by
the "normal" type filter. */},
},
cache: {
pageList: [],
optByName:{/*map of page names to OPTION object, to speed up
certain operations.*/},
names: {
/* Map of page names to "something." We don't map to their
winfo bits because those regularly get swapped out via
de/serialization. We need this map to support the add-new-page
feature, to give us a way to check for dupes without asking
the server or walking through the whole selection list.
*/}
},
/**
Updates OPTION elements to reflect whether the page has local
changes or is new/unsaved. This implementation is horribly
inefficient, in that we have to walk and validate the whole
list for each stash-level change.
If passed an argument, it is assumed to be an OPTION element
and only that element is updated, else all OPTION elements
in this.e.select are updated.
Reminder to self: in order to mark is-edited/is-new state we
have to update the OPTION element's inner text to reflect the
is-modified/is-new flags, rather than use CSS classes to tag
them, because mobile Chrome can neither restyle OPTION elements
no render ::before content on them. We *also* use CSS tags, but
they aren't sufficient for the mobile browsers.
*/
_refreshStashMarks: function callee(option){
if(!callee.eachOpt){
const self = this;
callee.eachOpt = function(keyOrOpt){
const opt = 'string'===typeof keyOrOpt ? self.e.select.options[keyOrOpt] : keyOrOpt;
const stashed = $stash.getWinfo({name:opt.value});
var prefix = '';
D.removeClass(opt, 'stashed', 'stashed-new', 'deleted');
if(stashed){
const isNew = winfoIsNew(stashed);
prefix = getEditMarker(isNew ? getEditMarker.NEW : getEditMarker.MODIFIED, true);
D.addClass(opt, isNew ? 'stashed-new' : 'stashed');
D.removeClass(opt, 'deleted');
}else if(opt.dataset.isDeleted){
prefix = getEditMarker(getEditMarker.DELETED,true);
D.addClass(opt, 'deleted');
}
opt.innerText = prefix + opt.value;
self.cache.names[opt.value] = true;
};
}
if(arguments.length){
callee.eachOpt(option);
}else{
this.cache.names = {/*must reset it to acount for local page removals*/};
Object.keys(this.e.select.options).forEach(callee.eachOpt);
}
},
/** Removes the given wiki page entry from the page selection
list, if it's in the list. */
removeEntry: function(name){
const sel = this.e.select;
var ndx = sel.selectedIndex;
sel.value = name;
if(sel.selectedIndex>-1){
if(ndx === sel.selectedIndex) ndx = -1;
sel.options.remove(sel.selectedIndex);
}
sel.selectedIndex = ndx;
delete this.cache.names[name];
delete this.cache.optByName[name];
this.cache.pageList = this.cache.pageList.filter((wi)=>name !== wi.name);
},
/**
Rebuilds the selection list. Necessary when it's loaded from
the server, we locally create a new page, or we remove a
locally-created new page.
*/
_rebuildList: function callee(){
/* Jump through some hoops to integrate new/unsaved
pages into the list of existing pages... We use a map
as an intermediary in order to filter out any local-stash
dupes from server-side copies. */
const list = this.cache.pageList;
|
| ︙ | ︙ | |||
393 394 395 396 397 398 399 400 401 402 403 |
.sort(callee.sorticase)
.forEach(function(name){
const winfo = map[name];
const opt = D.option(sel, winfo.name);
const wtype = opt.dataset.wtype =
winfo.type==='sandbox' ? 'normal' : (winfo.type||'normal');
const cb = self.e.filterCheckboxes[wtype];
if(cb && !cb.checked) D.addClass(opt, 'hidden');
});
D.enable(sel);
if(P.winfo) sel.value = P.winfo.name;
| > > > > > < < | 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 |
.sort(callee.sorticase)
.forEach(function(name){
const winfo = map[name];
const opt = D.option(sel, winfo.name);
const wtype = opt.dataset.wtype =
winfo.type==='sandbox' ? 'normal' : (winfo.type||'normal');
const cb = self.e.filterCheckboxes[wtype];
self.cache.optByName[winfo.name] = opt;
if(cb && !cb.checked) D.addClass(opt, 'hidden');
if(winfo.isEmpty){
opt.dataset.isDeleted = true;
}
self._refreshStashMarks(opt);
});
D.enable(sel);
if(P.winfo) sel.value = P.winfo.name;
},
/** Loads the page list and populates the selection list. */
loadList: function callee(){
if(!callee.onload){
const self = this;
callee.onload = function(list){
self.cache.pageList = list;
self._rebuildList();
F.message("Loaded page list.");
};
|
| ︙ | ︙ | |||
506 507 508 509 510 511 512 |
D.attr(sel, 'size', 15);
D.option(D.disable(D.clearElement(sel)), "Loading...");
/** Set up filter checkboxes for the various types
of wiki pages... */
const fsFilter = D.fieldset("Page types"),
fsFilterBody = D.div(),
| | > > > | < | > | | | | | > > > > > > > > > > > | > > > > > | > | | | > | 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 |
D.attr(sel, 'size', 15);
D.option(D.disable(D.clearElement(sel)), "Loading...");
/** Set up filter checkboxes for the various types
of wiki pages... */
const fsFilter = D.fieldset("Page types"),
fsFilterBody = D.div(),
filters = ['normal', 'branch/...', 'tag/...', 'checkin/...']
;
D.append(fsFilter, fsFilterBody);
D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
// Add filters by page type...
const self = this;
const filterByType = function(wtype, show){
sel.querySelectorAll('option[data-wtype='+wtype+']').forEach(function(opt){
if(show) opt.classList.remove('hidden');
else opt.classList.add('hidden');
});
};
filters.forEach(function(label){
const wtype = label.split('/')[0];
const cbId = 'wtype-filter-'+wtype,
lbl = D.attr(D.append(D.label(),label),
'for', cbId),
cb = D.attr(D.input('checkbox'), 'id', cbId);
D.append(fsFilterBody, D.append(D.span(), cb, lbl));
self.e.filterCheckboxes[wtype] = cb;
cb.checked = true;
filterByType(wtype, cb.checked);
cb.addEventListener(
'change',
function(ev){filterByType(wtype, ev.target.checked)},
false
);
});
{ /* add filter for "deleted" pages */
const cbId = 'wtype-filter-deleted',
lbl = D.attr(D.append(D.label(),
getEditMarker(getEditMarker.DELETED,false),
'deleted'),
'for', cbId),
cb = D.attr(D.input('checkbox'), 'id', cbId);
cb.checked = true;
D.attr(lbl, 'title',
'Fossil considers empty pages to be "deleted" in some contexts.');
D.append(fsFilterBody, D.append(D.span(), cb, lbl));
cb.addEventListener(
'change',
function(ev){
if(ev.target.checked) D.removeClass(parentElem,'hide-deleted');
else D.addClass(parentElem,'hide-deleted');
},
false);
}
/* A legend of the meanings of the symbols we use in
the OPTION elements to denote certain state. */
const fsLegend = D.fieldset("Edit status"),
fsLegendBody = D.div();
D.append(fsLegend, fsLegendBody);
D.addClass(fsLegendBody, 'flex-container', 'flex-column', 'stretch');
D.append(
fsLegendBody,
D.append(D.span(), getEditMarker(getEditMarker.NEW,false)," = page is new/unsaved"),
D.append(D.span(), getEditMarker(getEditMarker.MODIFIED,false)," = page has local edits"),
D.append(D.span(), getEditMarker(getEditMarker.DELETED,false)," = page is empty (deleted)")
);
const fsNewPage = D.fieldset("Create new page"),
fsNewPageBody = D.div(),
newPageName = D.input('text'),
newPageBtn = D.button("Add page locally")
;
|
| ︙ | ︙ | |||
580 581 582 583 584 585 586 587 588 589 590 591 592 593 |
this.loadList();
const onSelect = (e)=>P.loadPage(e.target.value);
sel.addEventListener('change', onSelect, false);
sel.addEventListener('dblclick', onSelect, false);
F.page.addEventListener('wiki-stash-updated', ()=>{
if(P.winfo) this._refreshStashMarks();
else this._rebuildList();
});
delete this.init;
}
};
/**
Widget for listing and selecting $stash entries.
| > > > > > > > > > > > > | 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 |
this.loadList();
const onSelect = (e)=>P.loadPage(e.target.value);
sel.addEventListener('change', onSelect, false);
sel.addEventListener('dblclick', onSelect, false);
F.page.addEventListener('wiki-stash-updated', ()=>{
if(P.winfo) this._refreshStashMarks();
else this._rebuildList();
});
F.page.addEventListener('wiki-page-loaded', function(ev){
/* Needed to handle the saved-an-empty-page case. */
const page = ev.detail,
opt = self.cache.optByName[page.name];
if(opt){
if(page.isEmpty) opt.dataset.isDeleted = true;
else delete opt.dataset.isDeleted;
self._refreshStashMarks(opt);
}else{
F.error("BUG: internal mis-handling of page object: missing OPTION for page "+page.name);
}
});
delete this.init;
}
};
/**
Widget for listing and selecting $stash entries.
|
| ︙ | ︙ | |||
945 946 947 948 949 950 951 952 953 954 955 956 957 958 |
P.updatePageTitle();
},
false
);
/* These init()s need to come after P's event handlers are registered */
WikiList.init( P.e.tabs.pageList.firstElementChild );
P.stashWidget.init(P.e.tabs.content.lastElementChild);
}/*F.onPageLoad()*/);
/**
Returns true if fossil.page.winfo is set, indicating that a page
has been loaded, else it reports an error and returns false.
If passed a truthy value any error message about not having
| > | 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 |
P.updatePageTitle();
},
false
);
/* These init()s need to come after P's event handlers are registered */
WikiList.init( P.e.tabs.pageList.firstElementChild );
P.stashWidget.init(P.e.tabs.content.lastElementChild);
//P.$wikiList = WikiList/*only for testing/debugging*/;
}/*F.onPageLoad()*/);
/**
Returns true if fossil.page.winfo is set, indicating that a page
has been loaded, else it reports an error and returns false.
If passed a truthy value any error message about not having
|
| ︙ | ︙ | |||
1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 |
F.message("Fetched from the local-edit storage:", stashWinfo.name);
onload({
name: stashWinfo.name,
mimetype: stashWinfo.mimetype,
type: stashWinfo.type,
version: stashWinfo.version,
parent: stashWinfo.parent,
content: $stash.stashedContent(stashWinfo)
});
return this;
}
F.message(
"Loading content..."
).fetch('wikiajax/fetch',{
| > | 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 |
F.message("Fetched from the local-edit storage:", stashWinfo.name);
onload({
name: stashWinfo.name,
mimetype: stashWinfo.mimetype,
type: stashWinfo.type,
version: stashWinfo.version,
parent: stashWinfo.parent,
isEmpty: !!stashWinfo.isEmpty,
content: $stash.stashedContent(stashWinfo)
});
return this;
}
F.message(
"Loading content..."
).fetch('wikiajax/fetch',{
|
| ︙ | ︙ |
Changes to src/style.c.
| ︙ | ︙ | |||
1453 1454 1455 1456 1457 1458 1459 |
/* fossil.config = {...various config-level options...} */
CX("window.fossil.config = {");
CX("/* Length of UUID hashes for display purposes. */");
CX("hashDigits: %d, hashDigitsUrl: %d,\n",
hash_digits(0), hash_digits(1));
CX("editStateMarkers: {"
"/*Symbolic markers to denote certain edit states.*/"
| | | 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 |
/* fossil.config = {...various config-level options...} */
CX("window.fossil.config = {");
CX("/* Length of UUID hashes for display purposes. */");
CX("hashDigits: %d, hashDigitsUrl: %d,\n",
hash_digits(0), hash_digits(1));
CX("editStateMarkers: {"
"/*Symbolic markers to denote certain edit states.*/"
"isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n");
CX("confirmerButtonTicks: 3 "
"/*default fossil.confirmer tick count.*/\n");
CX("};\n"/* fossil.config */);
#if 0
/* Is it safe to emit the CSRF token here? Some pages add it
** as a hidden form field. */
if(g.zCsrfToken[0]!=0){
|
| ︙ | ︙ |
Changes to src/style.wikiedit.css.
| ︙ | ︙ | |||
62 63 64 65 66 67 68 |
height: initial /* some skins set these to a fixed height */;
font-family: monospace;
}
body.wikiedit .WikiList select option {
margin: 0 0 0.5em 0.55em;
}
body.wikiedit .WikiList select option.stashed,
| | > > > > | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
height: initial /* some skins set these to a fixed height */;
font-family: monospace;
}
body.wikiedit .WikiList select option {
margin: 0 0 0.5em 0.55em;
}
body.wikiedit .WikiList select option.stashed,
body.wikiedit .WikiList select option.stashed-new,
body.wikiedit .WikiList select option.deleted {
margin-left: -1em;
}
body.wikiedit .WikiList.hide-deleted select option.deleted {
display: none;
}
body.wikiedit textarea {
max-width: initial;
}
body.wikiedit .tabs .tab-panel {
/* Needed for wide diffs */
overflow: auto;
|
| ︙ | ︙ | |||
140 141 142 143 144 145 146 |
font-size: 1.2em;
}
body.wikiedit #wikiedit-edit-status > span {
display: block;
}
body.wikiedit .WikiList span.is-new,
| | > | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
font-size: 1.2em;
}
body.wikiedit #wikiedit-edit-status > span {
display: block;
}
body.wikiedit .WikiList span.is-new,
body.wikiedit .WikiList span.is-modified,
body.wikiedit .WikiList span.is-deleted {
font-family: monospace;
}
body.wikiedit #wikiedit-edit-status span.links > a {
margin: 0 0.25em;
white-space: nowrap;
}
body.wikiedit #wikiedit-edit-status span.links > a::before {
|
| ︙ | ︙ |
Changes to src/wiki.c.
| ︙ | ︙ | |||
733 734 735 736 737 738 739 |
** Output JSON format:
**
** { name: "page name",
** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
** mimetype: "mime type",
** version: UUID string or null for a sandbox page,
** parent: "parent uuid" or null if no parent,
| > > | | 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 |
** Output JSON format:
**
** { name: "page name",
** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
** mimetype: "mime type",
** version: UUID string or null for a sandbox page,
** parent: "parent uuid" or null if no parent,
** isDeleted: true if the page has no content (is "deleted")
** else not set (making it "falsy" in JS),
** content: "page content" (only if includeContent is true)
** }
**
** If includeContent is false then the content member is elided.
*/
static int wiki_ajax_emit_page_object(const char *zPageName,
int includeContent){
Manifest * pWiki = 0;
|
| ︙ | ︙ | |||
776 777 778 779 780 781 782 783 784 785 786 787 788 789 |
zUuid,
pWiki->zMimetype ? pWiki->zMimetype : "text/x-fossil-wiki");
CX("\"parent\": ");
if(pWiki->nParent){
CX("%!j", pWiki->azParent[0]);
}else{
CX("null");
}
if(includeContent){
CX(", \"content\": %!j", pWiki->zWiki);
}
CX("}");
fossil_free(zUuid);
manifest_destroy(pWiki);
| > > > | 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 |
zUuid,
pWiki->zMimetype ? pWiki->zMimetype : "text/x-fossil-wiki");
CX("\"parent\": ");
if(pWiki->nParent){
CX("%!j", pWiki->azParent[0]);
}else{
CX("null");
}
if(!pWiki->zWiki || !pWiki->zWiki[0]){
CX(", \"isEmpty\": true");
}
if(includeContent){
CX(", \"content\": %!j", pWiki->zWiki);
}
CX("}");
fossil_free(zUuid);
manifest_destroy(pWiki);
|
| ︙ | ︙ |