| | @@ -17,11 +17,12 @@ |
| 17 | 17 | name: string, |
| 18 | 18 | mimetype: mimetype string, |
| 19 | 19 | type: "normal" | "tag" | "checkin" | "branch" | "sandbox", |
| 20 | 20 | version: UUID string or null for a sandbox page or new page, |
| 21 | 21 | 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 |
| 23 | 24 | } |
| 24 | 25 | |
| 25 | 26 | The internal docs and code frequently use the term "winfo", and such |
| 26 | 27 | references refer to an object with that form. |
| 27 | 28 | |
| | @@ -172,12 +173,14 @@ |
| 172 | 173 | record.mimetype = winfo.mimetype; |
| 173 | 174 | record.type = winfo.type; |
| 174 | 175 | record.parent = winfo.parent; |
| 175 | 176 | record.version = winfo.version; |
| 176 | 177 | record.stashTime = new Date().getTime(); |
| 178 | + record.isEmpty = !!winfo.isEmpty; |
| 177 | 179 | this.storeIndex(); |
| 178 | 180 | if(arguments.length>1){ |
| 181 | + if(content) delete record.isEmpty; |
| 179 | 182 | F.storage.set(this.contentKey(key), content); |
| 180 | 183 | } |
| 181 | 184 | this._fireStashEvent(); |
| 182 | 185 | return this; |
| 183 | 186 | }, |
| | @@ -264,24 +267,31 @@ |
| 264 | 267 | // Force UI update |
| 265 | 268 | s.dispatchEvent(new Event('change',{target:s})); |
| 266 | 269 | } |
| 267 | 270 | }; |
| 268 | 271 | |
| 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 | + */ |
| 270 | 276 | const getEditMarker = function f(winfo, textOnly){ |
| 271 | 277 | const esm = F.config.editStateMarkers; |
| 272 | | - if(1===winfo){ /* force is-new */ |
| 278 | + if(f.NEW===winfo){ /* force is-new */ |
| 273 | 279 | return textOnly ? esm.isNew : |
| 274 | 280 | 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 */ |
| 276 | 282 | return textOnly ? esm.isModified : |
| 277 | 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'); |
| 278 | 287 | }else if(winfo && winfo.version){ /* is existing page modified? */ |
| 279 | 288 | if($stash.getWinfo(winfo)){ |
| 280 | 289 | return textOnly ? esm.isModified : |
| 281 | 290 | D.addClass(D.append(D.span(),esm.isModified), 'is-modified'); |
| 282 | 291 | } |
| 292 | + /*fall through*/ |
| 283 | 293 | } |
| 284 | 294 | else if(winfo){ /* is new non-sandbox or is modified sandbox? */ |
| 285 | 295 | if('sandbox'!==winfo.type){ |
| 286 | 296 | return textOnly ? esm.isNew : |
| 287 | 297 | D.addClass(D.append(D.span(),esm.isNew), 'is-new'); |
| | @@ -290,10 +300,21 @@ |
| 290 | 300 | D.addClass(D.append(D.span(),esm.isModified), 'is-modified'); |
| 291 | 301 | } |
| 292 | 302 | } |
| 293 | 303 | return textOnly ? '' : D.span(); |
| 294 | 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 | + }; |
| 295 | 316 | |
| 296 | 317 | /** |
| 297 | 318 | Sets up and maintains the widgets for the list of wiki pages. |
| 298 | 319 | */ |
| 299 | 320 | const WikiList = { |
| | @@ -303,10 +324,12 @@ |
| 303 | 324 | except for "sandbox" type, which is assumed to be covered by |
| 304 | 325 | the "normal" type filter. */}, |
| 305 | 326 | }, |
| 306 | 327 | cache: { |
| 307 | 328 | pageList: [], |
| 329 | + optByName:{/*map of page names to OPTION object, to speed up |
| 330 | + certain operations.*/}, |
| 308 | 331 | names: { |
| 309 | 332 | /* Map of page names to "something." We don't map to their |
| 310 | 333 | winfo bits because those regularly get swapped out via |
| 311 | 334 | de/serialization. We need this map to support the add-new-page |
| 312 | 335 | feature, to give us a way to check for dupes without asking |
| | @@ -316,38 +339,49 @@ |
| 316 | 339 | /** |
| 317 | 340 | Updates OPTION elements to reflect whether the page has local |
| 318 | 341 | changes or is new/unsaved. This implementation is horribly |
| 319 | 342 | inefficient, in that we have to walk and validate the whole |
| 320 | 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. |
| 321 | 348 | |
| 322 | 349 | Reminder to self: in order to mark is-edited/is-new state we |
| 323 | 350 | have to update the OPTION element's inner text to reflect the |
| 324 | 351 | is-modified/is-new flags, rather than use CSS classes to tag |
| 325 | 352 | them, because mobile Chrome can neither restyle OPTION elements |
| 326 | 353 | no render ::before content on them. We *also* use CSS tags, but |
| 327 | 354 | they aren't sufficient for the mobile browsers. |
| 328 | 355 | */ |
| 329 | | - _refreshStashMarks: function callee(){ |
| 356 | + _refreshStashMarks: function callee(option){ |
| 330 | 357 | if(!callee.eachOpt){ |
| 331 | 358 | 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; |
| 334 | 361 | const stashed = $stash.getWinfo({name:opt.value}); |
| 335 | 362 | var prefix = ''; |
| 363 | + D.removeClass(opt, 'stashed', 'stashed-new', 'deleted'); |
| 336 | 364 | 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); |
| 339 | 367 | 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'); |
| 342 | 372 | } |
| 343 | 373 | opt.innerText = prefix + opt.value; |
| 344 | 374 | self.cache.names[opt.value] = true; |
| 345 | 375 | }; |
| 346 | 376 | } |
| 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 | + } |
| 349 | 383 | }, |
| 350 | 384 | /** Removes the given wiki page entry from the page selection |
| 351 | 385 | list, if it's in the list. */ |
| 352 | 386 | removeEntry: function(name){ |
| 353 | 387 | const sel = this.e.select; |
| | @@ -357,16 +391,18 @@ |
| 357 | 391 | if(ndx === sel.selectedIndex) ndx = -1; |
| 358 | 392 | sel.options.remove(sel.selectedIndex); |
| 359 | 393 | } |
| 360 | 394 | sel.selectedIndex = ndx; |
| 361 | 395 | delete this.cache.names[name]; |
| 396 | + delete this.cache.optByName[name]; |
| 362 | 397 | this.cache.pageList = this.cache.pageList.filter((wi)=>name !== wi.name); |
| 363 | 398 | }, |
| 364 | 399 | |
| 365 | 400 | /** |
| 366 | 401 | 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. |
| 368 | 404 | */ |
| 369 | 405 | _rebuildList: function callee(){ |
| 370 | 406 | /* Jump through some hoops to integrate new/unsaved |
| 371 | 407 | pages into the list of existing pages... We use a map |
| 372 | 408 | as an intermediary in order to filter out any local-stash |
| | @@ -395,20 +431,23 @@ |
| 395 | 431 | const winfo = map[name]; |
| 396 | 432 | const opt = D.option(sel, winfo.name); |
| 397 | 433 | const wtype = opt.dataset.wtype = |
| 398 | 434 | winfo.type==='sandbox' ? 'normal' : (winfo.type||'normal'); |
| 399 | 435 | const cb = self.e.filterCheckboxes[wtype]; |
| 436 | + self.cache.optByName[winfo.name] = opt; |
| 400 | 437 | if(cb && !cb.checked) D.addClass(opt, 'hidden'); |
| 438 | + if(winfo.isEmpty){ |
| 439 | + opt.dataset.isDeleted = true; |
| 440 | + } |
| 441 | + self._refreshStashMarks(opt); |
| 401 | 442 | }); |
| 402 | 443 | D.enable(sel); |
| 403 | 444 | if(P.winfo) sel.value = P.winfo.name; |
| 404 | | - this._refreshStashMarks(); |
| 405 | 445 | }, |
| 406 | 446 | |
| 407 | 447 | /** Loads the page list and populates the selection list. */ |
| 408 | 448 | loadList: function callee(){ |
| 409 | | - delete this.pageMap; |
| 410 | 449 | if(!callee.onload){ |
| 411 | 450 | const self = this; |
| 412 | 451 | callee.onload = function(list){ |
| 413 | 452 | self.cache.pageList = list; |
| 414 | 453 | self._rebuildList(); |
| | @@ -508,48 +547,69 @@ |
| 508 | 547 | |
| 509 | 548 | /** Set up filter checkboxes for the various types |
| 510 | 549 | of wiki pages... */ |
| 511 | 550 | const fsFilter = D.fieldset("Page types"), |
| 512 | 551 | fsFilterBody = D.div(), |
| 513 | | - filters = ['normal', 'branch', 'checkin', 'tag'] |
| 552 | + filters = ['normal', 'branch/...', 'tag/...', 'checkin/...'] |
| 514 | 553 | ; |
| 515 | 554 | D.append(fsFilter, fsFilterBody); |
| 516 | 555 | 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){ |
| 518 | 560 | sel.querySelectorAll('option[data-wtype='+wtype+']').forEach(function(opt){ |
| 519 | 561 | if(show) opt.classList.remove('hidden'); |
| 520 | 562 | else opt.classList.add('hidden'); |
| 521 | 563 | }); |
| 522 | 564 | }; |
| 523 | | - const self = this; |
| 524 | | - filters.forEach(function(wtype){ |
| 565 | + filters.forEach(function(label){ |
| 566 | + const wtype = label.split('/')[0]; |
| 525 | 567 | const cbId = 'wtype-filter-'+wtype, |
| 526 | | - lbl = D.attr(D.append(D.label(),wtype), |
| 568 | + lbl = D.attr(D.append(D.label(),label), |
| 527 | 569 | '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)); |
| 530 | 572 | self.e.filterCheckboxes[wtype] = cb; |
| 531 | 573 | cb.checked = true; |
| 532 | | - filterSelection(wtype, cb.checked); |
| 574 | + filterByType(wtype, cb.checked); |
| 533 | 575 | cb.addEventListener( |
| 534 | 576 | 'change', |
| 535 | | - function(ev){filterSelection(wtype, ev.target.checked)}, |
| 577 | + function(ev){filterByType(wtype, ev.target.checked)}, |
| 536 | 578 | false |
| 537 | 579 | ); |
| 538 | | - D.append(fsFilterBody, span); |
| 539 | 580 | }); |
| 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 | + } |
| 541 | 600 | /* A legend of the meanings of the symbols we use in |
| 542 | 601 | the OPTION elements to denote certain state. */ |
| 543 | 602 | const fsLegend = D.fieldset("Edit status"), |
| 544 | 603 | fsLegendBody = D.div(); |
| 545 | 604 | D.append(fsLegend, fsLegendBody); |
| 546 | 605 | D.addClass(fsLegendBody, 'flex-container', 'flex-column', 'stretch'); |
| 547 | 606 | D.append( |
| 548 | 607 | 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)") |
| 551 | 611 | ); |
| 552 | 612 | |
| 553 | 613 | const fsNewPage = D.fieldset("Create new page"), |
| 554 | 614 | fsNewPageBody = D.div(), |
| 555 | 615 | newPageName = D.input('text'), |
| | @@ -582,10 +642,22 @@ |
| 582 | 642 | sel.addEventListener('change', onSelect, false); |
| 583 | 643 | sel.addEventListener('dblclick', onSelect, false); |
| 584 | 644 | F.page.addEventListener('wiki-stash-updated', ()=>{ |
| 585 | 645 | if(P.winfo) this._refreshStashMarks(); |
| 586 | 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 | + } |
| 587 | 659 | }); |
| 588 | 660 | delete this.init; |
| 589 | 661 | } |
| 590 | 662 | }; |
| 591 | 663 | |
| | @@ -947,10 +1019,11 @@ |
| 947 | 1019 | false |
| 948 | 1020 | ); |
| 949 | 1021 | /* These init()s need to come after P's event handlers are registered */ |
| 950 | 1022 | WikiList.init( P.e.tabs.pageList.firstElementChild ); |
| 951 | 1023 | P.stashWidget.init(P.e.tabs.content.lastElementChild); |
| 1024 | + //P.$wikiList = WikiList/*only for testing/debugging*/; |
| 952 | 1025 | }/*F.onPageLoad()*/); |
| 953 | 1026 | |
| 954 | 1027 | /** |
| 955 | 1028 | Returns true if fossil.page.winfo is set, indicating that a page |
| 956 | 1029 | has been loaded, else it reports an error and returns false. |
| | @@ -1119,10 +1192,11 @@ |
| 1119 | 1192 | name: stashWinfo.name, |
| 1120 | 1193 | mimetype: stashWinfo.mimetype, |
| 1121 | 1194 | type: stashWinfo.type, |
| 1122 | 1195 | version: stashWinfo.version, |
| 1123 | 1196 | parent: stashWinfo.parent, |
| 1197 | + isEmpty: !!stashWinfo.isEmpty, |
| 1124 | 1198 | content: $stash.stashedContent(stashWinfo) |
| 1125 | 1199 | }); |
| 1126 | 1200 | return this; |
| 1127 | 1201 | } |
| 1128 | 1202 | F.message( |
| 1129 | 1203 | |