Fossil SCM

Plug the new attachment widget into /attachaddV2, an intermediary step in updating /attachadd. It does not yet post attachments.

stephan 2026-06-02 20:25 UTC attach-v2
Commit c3c649c92cf78899350f836a1fdd3101907ef37474527c31594c7e2c8c497cb4
+140 -2
--- src/attach.c
+++ src/attach.c
@@ -453,12 +453,11 @@
453453
zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>",
454454
zTarget, zForumPost);
455455
zTo = 1
456456
? mprintf("%R/forumpost/%S", zTarget)
457457
: mprintf("%R/attachview?forumpost=%T&file=%T",
458
- zTarget, zName)
459
- /* Or we could return directly to the forum post. */;
458
+ zTarget, zName);
460459
}else if( zPage ){
461460
if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
462461
login_needed(g.anon.ApndWiki && g.anon.Attach);
463462
return;
464463
}
@@ -540,10 +539,149 @@
540539
@ <input type="submit" name="cancel" value="Cancel">
541540
@ </div>
542541
captcha_generate(0);
543542
@ </form>
544543
builtin_fossil_js_bundle_or("attach", NULL);
544
+ style_finish_page();
545
+ fossil_free(zTargetType);
546
+ fossil_free(zExtraFree);
547
+}
548
+
549
+/*
550
+** WEBPAGE: attachaddV2 hidden
551
+** Add a new attachment.
552
+**
553
+** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH
554
+** from=URL
555
+**
556
+*/
557
+void attachaddV2_page(void){
558
+ const char *zFrom = P("from");
559
+ const char *zTarget = P("target");
560
+ char * zTo = 0;
561
+ char *zTargetType = 0;
562
+ char *zExtraFree = 0;
563
+ int iTgtType = 0;
564
+ int szContent = 0;
565
+ int goodCaptcha = 1;
566
+ int szLimit = 0;
567
+ int bNeedsModeration = 0;
568
+
569
+ if( zFrom==0 ) zFrom = mprintf("%R/home");
570
+ if( P("cancel") ) cgi_redirect(zFrom);
571
+ if( 0==zTarget ){
572
+ webpage_error("Requires target=X");
573
+ }
574
+ login_check_credentials();
575
+ iTgtType = attachment_target_type(zTarget);
576
+ switch( iTgtType ){
577
+ default:
578
+ case 0:
579
+ webpage_error("Cannot resolve target=%h.", zTarget);
580
+ break;
581
+ case CFTYPE_FORUM:{
582
+ int fpid;
583
+ if( g.perm.AttachForum==0 ){
584
+ login_needed(g.anon.AttachForum);
585
+ return;
586
+ }
587
+ fpid = forumpost_head_rid2(zTarget);
588
+ if( fpid<=0 ){
589
+ webpage_error("Invalid forum post ID: %h", zTarget);
590
+ }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){
591
+ webpage_error("Only admins can attach files to other users' "
592
+ "forum posts.");
593
+ }
594
+ zTarget = zExtraFree = rid_to_uuid(fpid);
595
+ zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%.16h</a>",
596
+ zTarget, zTarget);
597
+ zTo = mprintf("%R/forumpost/%S", zTarget);
598
+ bNeedsModeration = forum_need_moderation();
599
+ break;
600
+ }
601
+ case CFTYPE_EVENT:{
602
+ if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
603
+ login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach);
604
+ return;
605
+ }
606
+ if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", zTarget) ){
607
+ zTarget = db_text(0, "SELECT substr(tagname,7) FROM tag"
608
+ " WHERE tagname GLOB 'event-%q*'", zTarget);
609
+ if( zTarget==0) fossil_redirect_home();
610
+ }
611
+ zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
612
+ zTarget, zTarget);
613
+ bNeedsModeration = 0;
614
+ break;
615
+ }
616
+ case CFTYPE_TICKET:{
617
+ if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
618
+ login_needed(g.anon.ApndTkt && g.anon.Attach);
619
+ return;
620
+ }
621
+ if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTarget) ){
622
+ zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag"
623
+ " WHERE tagname GLOB 'tkt-%q*'", zTarget);
624
+ if( zTarget==0 ) fossil_redirect_home();
625
+ }
626
+ zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
627
+ zTarget, zTarget);
628
+ bNeedsModeration = ticket_need_moderation(0);
629
+ break;
630
+ }
631
+ case CFTYPE_WIKI:{
632
+ if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
633
+ login_needed(g.anon.ApndWiki && g.anon.Attach);
634
+ return;
635
+ }
636
+ if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zTarget) ){
637
+ fossil_redirect_home();
638
+ }
639
+ zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%h\">%h</a>",
640
+ zTarget, zTarget);
641
+ bNeedsModeration = wiki_need_moderation(0);
642
+ break;
643
+ }
644
+ }
645
+
646
+ db_begin_transaction();
647
+#if 0
648
+ szLimit = db_get_int("attachment-size-limit", 0);
649
+ if( szContent<0 || (szLimit && szContent>szLimit) ){
650
+ /* This check must be done late so that zTargetType is set up. */
651
+ @ <p class="generalError">Attachment %h(zName) is too large.
652
+ @ <a href="%R/help/attachment-size-limit">Limit</a> is
653
+ @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p>
654
+ /* Fall through and render form. */
655
+ }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
656
+#if 0
657
+ attach_commit(zName, zTarget, aContent, szContent, zMimetype,
658
+ bNeedsModeration, zComment);
659
+#endif
660
+ cgi_redirect(zTo ? zTo : zFrom);
661
+ }
662
+#else
663
+ (void)bNeedsModeration;
664
+ (void)szLimit;
665
+#endif
666
+
667
+ style_set_current_feature("attach");
668
+ style_header("Add Attachment");
669
+ if( !goodCaptcha ){
670
+ @ <p class="generalError">Error: Incorrect security code.</p>
671
+ }
672
+ @ <h2>Attachments for %s(zTargetType)</h2>
673
+ attachment_list(zTarget, NULL,
674
+ ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED);
675
+ /* Form gets fleshed out and activate from fossil.attach.js. */
676
+ @ <div id='attachadd-form-wrapper'>
677
+ @ <input type="hidden" name="target" value="%h(zTarget)">
678
+ @ <input type="hidden" name="from" value="%h(zFrom)">
679
+ captcha_generate(0);
680
+ @ </div>
681
+ builtin_fossil_js_bundle_or("attach", NULL);
682
+ db_end_transaction(0);
545683
style_finish_page();
546684
fossil_free(zTargetType);
547685
fossil_free(zExtraFree);
548686
}
549687
550688
--- src/attach.c
+++ src/attach.c
@@ -453,12 +453,11 @@
453 zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>",
454 zTarget, zForumPost);
455 zTo = 1
456 ? mprintf("%R/forumpost/%S", zTarget)
457 : mprintf("%R/attachview?forumpost=%T&file=%T",
458 zTarget, zName)
459 /* Or we could return directly to the forum post. */;
460 }else if( zPage ){
461 if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
462 login_needed(g.anon.ApndWiki && g.anon.Attach);
463 return;
464 }
@@ -540,10 +539,149 @@
540 @ <input type="submit" name="cancel" value="Cancel">
541 @ </div>
542 captcha_generate(0);
543 @ </form>
544 builtin_fossil_js_bundle_or("attach", NULL);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545 style_finish_page();
546 fossil_free(zTargetType);
547 fossil_free(zExtraFree);
548 }
549
550
--- src/attach.c
+++ src/attach.c
@@ -453,12 +453,11 @@
453 zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>",
454 zTarget, zForumPost);
455 zTo = 1
456 ? mprintf("%R/forumpost/%S", zTarget)
457 : mprintf("%R/attachview?forumpost=%T&file=%T",
458 zTarget, zName);
 
459 }else if( zPage ){
460 if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
461 login_needed(g.anon.ApndWiki && g.anon.Attach);
462 return;
463 }
@@ -540,10 +539,149 @@
539 @ <input type="submit" name="cancel" value="Cancel">
540 @ </div>
541 captcha_generate(0);
542 @ </form>
543 builtin_fossil_js_bundle_or("attach", NULL);
544 style_finish_page();
545 fossil_free(zTargetType);
546 fossil_free(zExtraFree);
547 }
548
549 /*
550 ** WEBPAGE: attachaddV2 hidden
551 ** Add a new attachment.
552 **
553 ** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH
554 ** from=URL
555 **
556 */
557 void attachaddV2_page(void){
558 const char *zFrom = P("from");
559 const char *zTarget = P("target");
560 char * zTo = 0;
561 char *zTargetType = 0;
562 char *zExtraFree = 0;
563 int iTgtType = 0;
564 int szContent = 0;
565 int goodCaptcha = 1;
566 int szLimit = 0;
567 int bNeedsModeration = 0;
568
569 if( zFrom==0 ) zFrom = mprintf("%R/home");
570 if( P("cancel") ) cgi_redirect(zFrom);
571 if( 0==zTarget ){
572 webpage_error("Requires target=X");
573 }
574 login_check_credentials();
575 iTgtType = attachment_target_type(zTarget);
576 switch( iTgtType ){
577 default:
578 case 0:
579 webpage_error("Cannot resolve target=%h.", zTarget);
580 break;
581 case CFTYPE_FORUM:{
582 int fpid;
583 if( g.perm.AttachForum==0 ){
584 login_needed(g.anon.AttachForum);
585 return;
586 }
587 fpid = forumpost_head_rid2(zTarget);
588 if( fpid<=0 ){
589 webpage_error("Invalid forum post ID: %h", zTarget);
590 }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){
591 webpage_error("Only admins can attach files to other users' "
592 "forum posts.");
593 }
594 zTarget = zExtraFree = rid_to_uuid(fpid);
595 zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%.16h</a>",
596 zTarget, zTarget);
597 zTo = mprintf("%R/forumpost/%S", zTarget);
598 bNeedsModeration = forum_need_moderation();
599 break;
600 }
601 case CFTYPE_EVENT:{
602 if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
603 login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach);
604 return;
605 }
606 if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", zTarget) ){
607 zTarget = db_text(0, "SELECT substr(tagname,7) FROM tag"
608 " WHERE tagname GLOB 'event-%q*'", zTarget);
609 if( zTarget==0) fossil_redirect_home();
610 }
611 zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
612 zTarget, zTarget);
613 bNeedsModeration = 0;
614 break;
615 }
616 case CFTYPE_TICKET:{
617 if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
618 login_needed(g.anon.ApndTkt && g.anon.Attach);
619 return;
620 }
621 if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTarget) ){
622 zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag"
623 " WHERE tagname GLOB 'tkt-%q*'", zTarget);
624 if( zTarget==0 ) fossil_redirect_home();
625 }
626 zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
627 zTarget, zTarget);
628 bNeedsModeration = ticket_need_moderation(0);
629 break;
630 }
631 case CFTYPE_WIKI:{
632 if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
633 login_needed(g.anon.ApndWiki && g.anon.Attach);
634 return;
635 }
636 if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zTarget) ){
637 fossil_redirect_home();
638 }
639 zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%h\">%h</a>",
640 zTarget, zTarget);
641 bNeedsModeration = wiki_need_moderation(0);
642 break;
643 }
644 }
645
646 db_begin_transaction();
647 #if 0
648 szLimit = db_get_int("attachment-size-limit", 0);
649 if( szContent<0 || (szLimit && szContent>szLimit) ){
650 /* This check must be done late so that zTargetType is set up. */
651 @ <p class="generalError">Attachment %h(zName) is too large.
652 @ <a href="%R/help/attachment-size-limit">Limit</a> is
653 @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p>
654 /* Fall through and render form. */
655 }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
656 #if 0
657 attach_commit(zName, zTarget, aContent, szContent, zMimetype,
658 bNeedsModeration, zComment);
659 #endif
660 cgi_redirect(zTo ? zTo : zFrom);
661 }
662 #else
663 (void)bNeedsModeration;
664 (void)szLimit;
665 #endif
666
667 style_set_current_feature("attach");
668 style_header("Add Attachment");
669 if( !goodCaptcha ){
670 @ <p class="generalError">Error: Incorrect security code.</p>
671 }
672 @ <h2>Attachments for %s(zTargetType)</h2>
673 attachment_list(zTarget, NULL,
674 ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED);
675 /* Form gets fleshed out and activate from fossil.attach.js. */
676 @ <div id='attachadd-form-wrapper'>
677 @ <input type="hidden" name="target" value="%h(zTarget)">
678 @ <input type="hidden" name="from" value="%h(zFrom)">
679 captcha_generate(0);
680 @ </div>
681 builtin_fossil_js_bundle_or("attach", NULL);
682 db_end_transaction(0);
683 style_finish_page();
684 fossil_free(zTargetType);
685 fossil_free(zExtraFree);
686 }
687
688
+11 -7
--- src/default.css
+++ src/default.css
@@ -2024,18 +2024,17 @@
20242024
padding: 0.75em;
20252025
border: 1px dashed #ccc;
20262026
border-radius: 0.25em;
20272027
background-color: #fafafa;
20282028
}
2029
-
20302029
.attach-container > .attach-row > .attach-dropzone {
20312030
padding: 1em;
20322031
text-align: center;
20332032
background: #ffffff;
20342033
border: 1px solid #ddd;
20352034
cursor: pointer;
2036
- border-radius: 2px;
2035
+ border-radius: 0.25em;
20372036
transition: background-color 0.15s ease-in-out;
20382037
display: flex;
20392038
flex-direction: row;
20402039
flex-wrap: nowrap;
20412040
}
@@ -2049,38 +2048,43 @@
20492048
border-style: solid;
20502049
text-align: left;
20512050
}
20522051
.attach-container > .attach-row .attach-row-info {
20532052
font-family: monospace;
2054
- font-size: 0.9em;
20552053
flex-grow: 1;
20562054
}
20572055
.attach-container > .attach-row .attach-desc {
20582056
max-width: initial;
20592057
width: 100%;
20602058
box-sizing: border-box;
20612059
min-height: 4em;
20622060
padding: 0.5em;
20632061
font-family: inherit;
2064
- font-size: 0.9em;
20652062
resize: vertical;
20662063
}
20672064
.attach-container > .attach-row .attach-row-remove {
2068
- align-self: flex-end;
2065
+ align-self: center;
20692066
padding: 0.25em 0.75em;
2067
+ margin-left: 1em;
20702068
background-color: #d32f2f;
20712069
color: #fff;
20722070
border: none;
2073
- border-radius: 2px;
2071
+ border-radius: 0.25em;
20742072
cursor: pointer;
20752073
}
20762074
.attach-container > .attach-row .attach-row-remove:hover {
20772075
background-color: #b71c1c;
20782076
}
2079
-.attach-container > .attach-add-button {
2077
+.attach-container > .attach-controls {
2078
+ display: flex;
2079
+ flex-direction: row;
2080
+ gap: 1em;
2081
+}
2082
+.attach-container > .attach-controls .attach-add-button {
20802083
padding: 0.5em 1em;
20812084
cursor: pointer;
2085
+ flex-grow: 2;
20822086
}
20832087
20842088
/* Objects in the "desktoponly" class are invisible on mobile */
20852089
@media screen and (max-width: 600px) {
20862090
.desktoponly {
20872091
--- src/default.css
+++ src/default.css
@@ -2024,18 +2024,17 @@
2024 padding: 0.75em;
2025 border: 1px dashed #ccc;
2026 border-radius: 0.25em;
2027 background-color: #fafafa;
2028 }
2029
2030 .attach-container > .attach-row > .attach-dropzone {
2031 padding: 1em;
2032 text-align: center;
2033 background: #ffffff;
2034 border: 1px solid #ddd;
2035 cursor: pointer;
2036 border-radius: 2px;
2037 transition: background-color 0.15s ease-in-out;
2038 display: flex;
2039 flex-direction: row;
2040 flex-wrap: nowrap;
2041 }
@@ -2049,38 +2048,43 @@
2049 border-style: solid;
2050 text-align: left;
2051 }
2052 .attach-container > .attach-row .attach-row-info {
2053 font-family: monospace;
2054 font-size: 0.9em;
2055 flex-grow: 1;
2056 }
2057 .attach-container > .attach-row .attach-desc {
2058 max-width: initial;
2059 width: 100%;
2060 box-sizing: border-box;
2061 min-height: 4em;
2062 padding: 0.5em;
2063 font-family: inherit;
2064 font-size: 0.9em;
2065 resize: vertical;
2066 }
2067 .attach-container > .attach-row .attach-row-remove {
2068 align-self: flex-end;
2069 padding: 0.25em 0.75em;
 
2070 background-color: #d32f2f;
2071 color: #fff;
2072 border: none;
2073 border-radius: 2px;
2074 cursor: pointer;
2075 }
2076 .attach-container > .attach-row .attach-row-remove:hover {
2077 background-color: #b71c1c;
2078 }
2079 .attach-container > .attach-add-button {
 
 
 
 
 
2080 padding: 0.5em 1em;
2081 cursor: pointer;
 
2082 }
2083
2084 /* Objects in the "desktoponly" class are invisible on mobile */
2085 @media screen and (max-width: 600px) {
2086 .desktoponly {
2087
--- src/default.css
+++ src/default.css
@@ -2024,18 +2024,17 @@
2024 padding: 0.75em;
2025 border: 1px dashed #ccc;
2026 border-radius: 0.25em;
2027 background-color: #fafafa;
2028 }
 
2029 .attach-container > .attach-row > .attach-dropzone {
2030 padding: 1em;
2031 text-align: center;
2032 background: #ffffff;
2033 border: 1px solid #ddd;
2034 cursor: pointer;
2035 border-radius: 0.25em;
2036 transition: background-color 0.15s ease-in-out;
2037 display: flex;
2038 flex-direction: row;
2039 flex-wrap: nowrap;
2040 }
@@ -2049,38 +2048,43 @@
2048 border-style: solid;
2049 text-align: left;
2050 }
2051 .attach-container > .attach-row .attach-row-info {
2052 font-family: monospace;
 
2053 flex-grow: 1;
2054 }
2055 .attach-container > .attach-row .attach-desc {
2056 max-width: initial;
2057 width: 100%;
2058 box-sizing: border-box;
2059 min-height: 4em;
2060 padding: 0.5em;
2061 font-family: inherit;
 
2062 resize: vertical;
2063 }
2064 .attach-container > .attach-row .attach-row-remove {
2065 align-self: center;
2066 padding: 0.25em 0.75em;
2067 margin-left: 1em;
2068 background-color: #d32f2f;
2069 color: #fff;
2070 border: none;
2071 border-radius: 0.25em;
2072 cursor: pointer;
2073 }
2074 .attach-container > .attach-row .attach-row-remove:hover {
2075 background-color: #b71c1c;
2076 }
2077 .attach-container > .attach-controls {
2078 display: flex;
2079 flex-direction: row;
2080 gap: 1em;
2081 }
2082 .attach-container > .attach-controls .attach-add-button {
2083 padding: 0.5em 1em;
2084 cursor: pointer;
2085 flex-grow: 2;
2086 }
2087
2088 /* Objects in the "desktoponly" class are invisible on mobile */
2089 @media screen and (max-width: 600px) {
2090 .desktoponly {
2091
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -19,10 +19,11 @@
1919
*/
2020
class Attacher {
2121
#opt;
2222
#rows = [];
2323
#e = Object.create(null);
24
+ #events = new EventTarget();
2425
2526
/**
2627
Options:
2728
2829
opt.container: DOM element to wrap this object in.
@@ -34,13 +35,32 @@
3435
defaults to "some sensible value".
3536
3637
opt.startWith[=0]: if >0 then that many file selection widgets
3738
are automatically activated, as if the user had tapped the Add
3839
button that many times.
40
+
41
+ opt.listener = {add: func, remove: func, populate: func}: if
42
+ these are functions they are registered as listeners for
43
+ 'entry-added', 'entry-removed', and/or 'entry-populated'
44
+ events, described below.
45
+
46
+ Events:
47
+
48
+ This class fires CustomEvents for certain changes:
49
+
50
+ 'entry-added' and 'entry-removed' trigger when an attachment
51
+ entry row is added/removed. Its event.detail is {attacher:
52
+ this, row: object}.
53
+
54
+ 'entry-populated' is triggered when a visible entry gets
55
+ content attached to it.
56
+
57
+ The public structure of the row object passed to each is
58
+ currently TBD.
3959
*/
4060
constructor(opt){
41
- this.#opt = opt = Object.assign(Object.create(null),{
61
+ this.#opt = opt = F.nu({
4262
addButtonLabel: false,
4363
startWith: 0,
4464
limit: 7
4565
}, opt);
4666
const eBtnAdd = this.#e.btnAdd = D.addClass(
@@ -47,45 +67,87 @@
4767
D.button(this.#opt.addButtonLabel || 'Add attachment',
4868
()=>this.#addRow()),
4969
'attach-add-button'
5070
);
5171
eBtnAdd.type = 'button';
72
+ const eControls = this.#e.controls =
73
+ D.addClass(D.div(), 'attach-controls');
74
+ eControls.append(eBtnAdd);
5275
this.#e.list = D.addClass(D.div(), 'attach-container');
5376
opt.container.appendChild(this.#e.list);
54
- this.#e.list.appendChild(eBtnAdd);
77
+ this.#e.list.appendChild(eControls);
78
+ if( opt.listener ){
79
+ if( opt.listener.add instanceof Function ){
80
+ this.addEventListener('entry-added', opt.listener.add);
81
+ }
82
+ if( opt.listener.remove instanceof Function ){
83
+ this.addEventListener('entry-removed', opt.listener.remove);
84
+ }
85
+ if( opt.listener.populate instanceof Function ){
86
+ this.addEventListener('entry-populated', opt.listener.populate);
87
+ }
88
+ }
5589
if( opt.startWith > 0 ){
5690
for(let i = 0; i < opt.startWith; ++i ){
5791
this.#addRow();
5892
}
93
+ }else{
94
+ this.#updateControls();
95
+ }
96
+ }
97
+
98
+ addEventListener(...args){
99
+ return this.#events.addEventListener(...args);
100
+ }
101
+
102
+ removeEventListener(...args){
103
+ return this.#events.removeEventListener(...args);
104
+ }
105
+
106
+ get isPopulated(){
107
+ for(let r of this.#rows){
108
+ if( r.file ) return true;
59109
}
110
+ return false;
111
+ }
112
+
113
+ get controlsElement(){
114
+ return this.#e.controls;
60115
}
61116
62117
#removeRow(rowObj){
63118
rowObj.eRow.remove();
64119
this.#rows = this.#rows.filter(v=>v!==rowObj);
65
- this.#updateBtnAdd();
120
+ this.#updateControls();
121
+ this.#events.dispatchEvent(
122
+ new CustomEvent('entry-removed',{
123
+ detail: F.nu({
124
+ row: rowObj,
125
+ attacher: this
126
+ })
127
+ })
128
+ );
66129
if( 0===this.#rows.length
67
- && 1===this.#opt.limit
68130
&& 1===this.#opt.startWith ){
69131
/* Intended primarily for /addattach. */
70132
this.#addRow();
71133
}
72134
}
73135
74136
/**
75137
Hide or show the Add button, as appropriate.
76138
*/
77
- #updateBtnAdd(){
139
+ #updateControls(){
78140
const b = this.#e.btnAdd;
79141
if( this.#opt.limit>0 && this.#rows.length >= this.#opt.limit ){
80142
b.classList.add('hidden');
81143
//b.setAttribute('disabled','');
82144
//F.toast.warning("Attachment form limit reached.");
83145
}else{
84146
b.classList.remove('hidden');
85147
//b.removeAttribute('disabled');
86
- this.#e.list.append(b/*move to the end*/);
148
+ this.#e.list.append(this.#e.controls/*move to the end*/);
87149
}
88150
}
89151
90152
#addRow(){
91153
const id = ++idCounter;
@@ -166,11 +228,19 @@
166228
rowObj.eInfo = eInfo;
167229
rowObj.eDesc = eDesc;
168230
rowObj.eRow = eRow;
169231
this.#e.list.append(eRow);
170232
this.#rows.push( rowObj );
171
- this.#updateBtnAdd();
233
+ this.#updateControls();
234
+ this.#events.dispatchEvent(
235
+ new CustomEvent('entry-added',{
236
+ detail: F.nu({
237
+ row: rowObj,
238
+ attacher: this
239
+ })
240
+ })
241
+ );
172242
if( 0 ){
173243
/* To allow immediate ctrl-v, we need a trick...
174244
But don't do this because it will interfere with, e.g.,
175245
the forum editor. */
176246
D.attr(eRow, 'tabindex', '-1');
@@ -217,13 +287,24 @@
217287
}else if( file.size < 1000000 ){
218288
szLbl = (file.size / 1024).toFixed(2)+' KB';
219289
}else{
220290
szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB';
221291
}
222
- rowObj.eInfo.textContent = `${lbl} (${szLbl}, ${rowObj.mimeType})`;
292
+ D.append(
293
+ D.clearElement(rowObj.eInfo),
294
+ lbl, D.br(), szLbl, ' ', rowObj.mimeType || ''
295
+ );
223296
rowObj.eDropzone.classList.add('populated');
224297
rowObj.eDesc.classList.remove('hidden');
298
+ this.#events.dispatchEvent(
299
+ new CustomEvent('entry-populated',{
300
+ detail: F.nu({
301
+ row: rowObj,
302
+ attacher: this
303
+ })
304
+ })
305
+ );
225306
}
226307
227308
/**
228309
Returns an array of objects describing the currently-selected
229310
attachments.
@@ -232,11 +313,11 @@
232313
const rv = [];
233314
for(let r of this.#rows){
234315
if( !r.eDropzone?.classList?.contains?.('populated') ){
235316
continue;
236317
}
237
- rv.push(Object.assign(Object.create(null),{
318
+ rv.push(F.nu({
238319
name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
239320
content: r.file,
240321
description: r.eDesc?.value || '',
241322
mimeType: r.mimeType
242323
}));
@@ -266,9 +347,42 @@
266347
}
267348
}
268349
return i;
269350
}
270351
}/*Attacher*/;
271
-
272352
F.Attacher = Attacher;
353
+
354
+ if( document.body.classList.contains('cpage-attachaddV2') ){
355
+ const eFormDiv = document.querySelector('#attachadd-form-wrapper');
356
+ const eBtnSubmit = D.button("Submit");
357
+ eBtnSubmit.type = 'button';
358
+ const updateBtnSubmit = (attacher)=>{
359
+ if( attacher.isPopulated ){
360
+ eBtnSubmit.removeAttribute('disabled');
361
+ }else{
362
+ eBtnSubmit.setAttribute('disabled', '');
363
+ }
364
+ };
365
+ const cbAdd = (ev)=>{
366
+ const a = ev.detail.attacher;
367
+ updateBtnSubmit(a);
368
+ };
369
+ const cbRm = (ev)=>{
370
+ const a = ev.detail.attacher;
371
+ updateBtnSubmit(a);
372
+ };
373
+ const cbPopulated = (ev)=>{
374
+ const a = ev.detail.attacher;
375
+ updateBtnSubmit(a);
376
+ };
377
+ const cbSubmit = (ev)=>{
378
+ };
379
+ eBtnSubmit.addEventListener('click', cbSubmit, false);
380
+ const att = new Attacher({
381
+ container: eFormDiv,
382
+ startWith: 1,
383
+ listener: {add: cbAdd, remove: cbRm, populate: cbPopulated}
384
+ });
385
+ att.controlsElement.append(eBtnSubmit);
386
+ }/* /attachaddV2 */
273387
274388
})(window.fossil);
275389
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -19,10 +19,11 @@
19 */
20 class Attacher {
21 #opt;
22 #rows = [];
23 #e = Object.create(null);
 
24
25 /**
26 Options:
27
28 opt.container: DOM element to wrap this object in.
@@ -34,13 +35,32 @@
34 defaults to "some sensible value".
35
36 opt.startWith[=0]: if >0 then that many file selection widgets
37 are automatically activated, as if the user had tapped the Add
38 button that many times.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39 */
40 constructor(opt){
41 this.#opt = opt = Object.assign(Object.create(null),{
42 addButtonLabel: false,
43 startWith: 0,
44 limit: 7
45 }, opt);
46 const eBtnAdd = this.#e.btnAdd = D.addClass(
@@ -47,45 +67,87 @@
47 D.button(this.#opt.addButtonLabel || 'Add attachment',
48 ()=>this.#addRow()),
49 'attach-add-button'
50 );
51 eBtnAdd.type = 'button';
 
 
 
52 this.#e.list = D.addClass(D.div(), 'attach-container');
53 opt.container.appendChild(this.#e.list);
54 this.#e.list.appendChild(eBtnAdd);
 
 
 
 
 
 
 
 
 
 
 
55 if( opt.startWith > 0 ){
56 for(let i = 0; i < opt.startWith; ++i ){
57 this.#addRow();
58 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59 }
 
 
 
 
 
60 }
61
62 #removeRow(rowObj){
63 rowObj.eRow.remove();
64 this.#rows = this.#rows.filter(v=>v!==rowObj);
65 this.#updateBtnAdd();
 
 
 
 
 
 
 
 
66 if( 0===this.#rows.length
67 && 1===this.#opt.limit
68 && 1===this.#opt.startWith ){
69 /* Intended primarily for /addattach. */
70 this.#addRow();
71 }
72 }
73
74 /**
75 Hide or show the Add button, as appropriate.
76 */
77 #updateBtnAdd(){
78 const b = this.#e.btnAdd;
79 if( this.#opt.limit>0 && this.#rows.length >= this.#opt.limit ){
80 b.classList.add('hidden');
81 //b.setAttribute('disabled','');
82 //F.toast.warning("Attachment form limit reached.");
83 }else{
84 b.classList.remove('hidden');
85 //b.removeAttribute('disabled');
86 this.#e.list.append(b/*move to the end*/);
87 }
88 }
89
90 #addRow(){
91 const id = ++idCounter;
@@ -166,11 +228,19 @@
166 rowObj.eInfo = eInfo;
167 rowObj.eDesc = eDesc;
168 rowObj.eRow = eRow;
169 this.#e.list.append(eRow);
170 this.#rows.push( rowObj );
171 this.#updateBtnAdd();
 
 
 
 
 
 
 
 
172 if( 0 ){
173 /* To allow immediate ctrl-v, we need a trick...
174 But don't do this because it will interfere with, e.g.,
175 the forum editor. */
176 D.attr(eRow, 'tabindex', '-1');
@@ -217,13 +287,24 @@
217 }else if( file.size < 1000000 ){
218 szLbl = (file.size / 1024).toFixed(2)+' KB';
219 }else{
220 szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB';
221 }
222 rowObj.eInfo.textContent = `${lbl} (${szLbl}, ${rowObj.mimeType})`;
 
 
 
223 rowObj.eDropzone.classList.add('populated');
224 rowObj.eDesc.classList.remove('hidden');
 
 
 
 
 
 
 
 
225 }
226
227 /**
228 Returns an array of objects describing the currently-selected
229 attachments.
@@ -232,11 +313,11 @@
232 const rv = [];
233 for(let r of this.#rows){
234 if( !r.eDropzone?.classList?.contains?.('populated') ){
235 continue;
236 }
237 rv.push(Object.assign(Object.create(null),{
238 name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
239 content: r.file,
240 description: r.eDesc?.value || '',
241 mimeType: r.mimeType
242 }));
@@ -266,9 +347,42 @@
266 }
267 }
268 return i;
269 }
270 }/*Attacher*/;
271
272 F.Attacher = Attacher;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
274 })(window.fossil);
275
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -19,10 +19,11 @@
19 */
20 class Attacher {
21 #opt;
22 #rows = [];
23 #e = Object.create(null);
24 #events = new EventTarget();
25
26 /**
27 Options:
28
29 opt.container: DOM element to wrap this object in.
@@ -34,13 +35,32 @@
35 defaults to "some sensible value".
36
37 opt.startWith[=0]: if >0 then that many file selection widgets
38 are automatically activated, as if the user had tapped the Add
39 button that many times.
40
41 opt.listener = {add: func, remove: func, populate: func}: if
42 these are functions they are registered as listeners for
43 'entry-added', 'entry-removed', and/or 'entry-populated'
44 events, described below.
45
46 Events:
47
48 This class fires CustomEvents for certain changes:
49
50 'entry-added' and 'entry-removed' trigger when an attachment
51 entry row is added/removed. Its event.detail is {attacher:
52 this, row: object}.
53
54 'entry-populated' is triggered when a visible entry gets
55 content attached to it.
56
57 The public structure of the row object passed to each is
58 currently TBD.
59 */
60 constructor(opt){
61 this.#opt = opt = F.nu({
62 addButtonLabel: false,
63 startWith: 0,
64 limit: 7
65 }, opt);
66 const eBtnAdd = this.#e.btnAdd = D.addClass(
@@ -47,45 +67,87 @@
67 D.button(this.#opt.addButtonLabel || 'Add attachment',
68 ()=>this.#addRow()),
69 'attach-add-button'
70 );
71 eBtnAdd.type = 'button';
72 const eControls = this.#e.controls =
73 D.addClass(D.div(), 'attach-controls');
74 eControls.append(eBtnAdd);
75 this.#e.list = D.addClass(D.div(), 'attach-container');
76 opt.container.appendChild(this.#e.list);
77 this.#e.list.appendChild(eControls);
78 if( opt.listener ){
79 if( opt.listener.add instanceof Function ){
80 this.addEventListener('entry-added', opt.listener.add);
81 }
82 if( opt.listener.remove instanceof Function ){
83 this.addEventListener('entry-removed', opt.listener.remove);
84 }
85 if( opt.listener.populate instanceof Function ){
86 this.addEventListener('entry-populated', opt.listener.populate);
87 }
88 }
89 if( opt.startWith > 0 ){
90 for(let i = 0; i < opt.startWith; ++i ){
91 this.#addRow();
92 }
93 }else{
94 this.#updateControls();
95 }
96 }
97
98 addEventListener(...args){
99 return this.#events.addEventListener(...args);
100 }
101
102 removeEventListener(...args){
103 return this.#events.removeEventListener(...args);
104 }
105
106 get isPopulated(){
107 for(let r of this.#rows){
108 if( r.file ) return true;
109 }
110 return false;
111 }
112
113 get controlsElement(){
114 return this.#e.controls;
115 }
116
117 #removeRow(rowObj){
118 rowObj.eRow.remove();
119 this.#rows = this.#rows.filter(v=>v!==rowObj);
120 this.#updateControls();
121 this.#events.dispatchEvent(
122 new CustomEvent('entry-removed',{
123 detail: F.nu({
124 row: rowObj,
125 attacher: this
126 })
127 })
128 );
129 if( 0===this.#rows.length
 
130 && 1===this.#opt.startWith ){
131 /* Intended primarily for /addattach. */
132 this.#addRow();
133 }
134 }
135
136 /**
137 Hide or show the Add button, as appropriate.
138 */
139 #updateControls(){
140 const b = this.#e.btnAdd;
141 if( this.#opt.limit>0 && this.#rows.length >= this.#opt.limit ){
142 b.classList.add('hidden');
143 //b.setAttribute('disabled','');
144 //F.toast.warning("Attachment form limit reached.");
145 }else{
146 b.classList.remove('hidden');
147 //b.removeAttribute('disabled');
148 this.#e.list.append(this.#e.controls/*move to the end*/);
149 }
150 }
151
152 #addRow(){
153 const id = ++idCounter;
@@ -166,11 +228,19 @@
228 rowObj.eInfo = eInfo;
229 rowObj.eDesc = eDesc;
230 rowObj.eRow = eRow;
231 this.#e.list.append(eRow);
232 this.#rows.push( rowObj );
233 this.#updateControls();
234 this.#events.dispatchEvent(
235 new CustomEvent('entry-added',{
236 detail: F.nu({
237 row: rowObj,
238 attacher: this
239 })
240 })
241 );
242 if( 0 ){
243 /* To allow immediate ctrl-v, we need a trick...
244 But don't do this because it will interfere with, e.g.,
245 the forum editor. */
246 D.attr(eRow, 'tabindex', '-1');
@@ -217,13 +287,24 @@
287 }else if( file.size < 1000000 ){
288 szLbl = (file.size / 1024).toFixed(2)+' KB';
289 }else{
290 szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB';
291 }
292 D.append(
293 D.clearElement(rowObj.eInfo),
294 lbl, D.br(), szLbl, ' ', rowObj.mimeType || ''
295 );
296 rowObj.eDropzone.classList.add('populated');
297 rowObj.eDesc.classList.remove('hidden');
298 this.#events.dispatchEvent(
299 new CustomEvent('entry-populated',{
300 detail: F.nu({
301 row: rowObj,
302 attacher: this
303 })
304 })
305 );
306 }
307
308 /**
309 Returns an array of objects describing the currently-selected
310 attachments.
@@ -232,11 +313,11 @@
313 const rv = [];
314 for(let r of this.#rows){
315 if( !r.eDropzone?.classList?.contains?.('populated') ){
316 continue;
317 }
318 rv.push(F.nu({
319 name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
320 content: r.file,
321 description: r.eDesc?.value || '',
322 mimeType: r.mimeType
323 }));
@@ -266,9 +347,42 @@
347 }
348 }
349 return i;
350 }
351 }/*Attacher*/;
 
352 F.Attacher = Attacher;
353
354 if( document.body.classList.contains('cpage-attachaddV2') ){
355 const eFormDiv = document.querySelector('#attachadd-form-wrapper');
356 const eBtnSubmit = D.button("Submit");
357 eBtnSubmit.type = 'button';
358 const updateBtnSubmit = (attacher)=>{
359 if( attacher.isPopulated ){
360 eBtnSubmit.removeAttribute('disabled');
361 }else{
362 eBtnSubmit.setAttribute('disabled', '');
363 }
364 };
365 const cbAdd = (ev)=>{
366 const a = ev.detail.attacher;
367 updateBtnSubmit(a);
368 };
369 const cbRm = (ev)=>{
370 const a = ev.detail.attacher;
371 updateBtnSubmit(a);
372 };
373 const cbPopulated = (ev)=>{
374 const a = ev.detail.attacher;
375 updateBtnSubmit(a);
376 };
377 const cbSubmit = (ev)=>{
378 };
379 eBtnSubmit.addEventListener('click', cbSubmit, false);
380 const att = new Attacher({
381 container: eFormDiv,
382 startWith: 1,
383 listener: {add: cbAdd, remove: cbRm, populate: cbPopulated}
384 });
385 att.controlsElement.append(eBtnSubmit);
386 }/* /attachaddV2 */
387
388 })(window.fossil);
389
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -16,10 +16,14 @@
1616
after style.c:builtin_emit_script_fossil_bootstrap() has
1717
initialized that object.
1818
*/
1919
2020
const F = global.fossil;
21
+
22
+ /** Creates a prototype-less plain object with properties derived
23
+ from all of its object-type arguments. */
24
+ F.nu = (...obj)=>Object.assign(Object.create(null),...obj);
2125
2226
/**
2327
Returns the current time in something approximating
2428
ISO-8601 format.
2529
*/
2630
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -16,10 +16,14 @@
16 after style.c:builtin_emit_script_fossil_bootstrap() has
17 initialized that object.
18 */
19
20 const F = global.fossil;
 
 
 
 
21
22 /**
23 Returns the current time in something approximating
24 ISO-8601 format.
25 */
26
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -16,10 +16,14 @@
16 after style.c:builtin_emit_script_fossil_bootstrap() has
17 initialized that object.
18 */
19
20 const F = global.fossil;
21
22 /** Creates a prototype-less plain object with properties derived
23 from all of its object-type arguments. */
24 F.nu = (...obj)=>Object.assign(Object.create(null),...obj);
25
26 /**
27 Returns the current time in something approximating
28 ISO-8601 format.
29 */
30

Keyboard Shortcuts

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