Index: openacs-4/packages/xowiki/tcl/bootstrap-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowiki/tcl/bootstrap-procs.tcl,v diff -u -r1.11 -r1.12 --- openacs-4/packages/xowiki/tcl/bootstrap-procs.tcl 17 Dec 2018 10:39:11 -0000 1.11 +++ openacs-4/packages/xowiki/tcl/bootstrap-procs.tcl 3 Sep 2024 15:37:55 -0000 1.12 @@ -8,12 +8,14 @@ } ::xo::library require menu-procs +::xo::library require -package xotcl-core 30-widget-procs namespace eval ::xowiki { - # minimal implementation of Bootstrap "navbar" + # + # Minimal implementation of Bootstrap "navbar" # currently only "dropdown" elements are supported within the navbar - # TODO: add support to include: - # - forms + # TODO: add support to include: + # - forms # - buttons # - text # - Non-nav links @@ -26,194 +28,264 @@ -parameter { {autorender false} {menubar} - {containerClass "container-fluid"} - {navbarClass "navbar navbar-default navbar-static-top"} + {containerClass "container-fluid px-0"} + {navbarClass "navbar navbar-expand-lg navbar-default navbar-static-top mx-2 p-0"} } - + BootstrapNavbar instproc init {} { ::xo::Page requireJS urn:ad:js:jquery - # - # TODO: We should dynamically be able to determine (some of) the - # CSP directives. However, for the time being, the URLs below are - # trusted. - # - security::csp::require script-src maxcdn.bootstrapcdn.com - security::csp::require style-src maxcdn.bootstrapcdn.com - security::csp::require font-src maxcdn.bootstrapcdn.com - - ::xo::Page requireCSS urn:ad:css:bootstrap3 - ::xo::Page requireJS urn:ad:js:bootstrap3 + ::xowiki::CSS require_toolkit -css -js next } - + BootstrapNavbar ad_instproc render {} { http://getbootstrap.com/components/#navbar } { - html::nav -class [:navbarClass] -role "navigation" { - # - # Render the pull down menues - # - html::div -class [:containerClass] { - set rightMenuEntries {} - foreach entry [:children] { - if {[$entry istype ::xowiki::BootstrapNavbarDropdownMenu]} { - $entry render - } else { - lappend rightMenuEntries $entry - } - } - if {[llength $rightMenuEntries] > 0} { - html::ul -class "nav navbar-nav navbar-right" { - foreach entry $rightMenuEntries { - $entry render + html::nav \ + -class [xowiki::CSS classes ${:navbarClass}] \ + -role "navigation" \ + -style "background-color: #f8f9fa;" { + # + # Render the pull down menus + # + html::div -class ${:containerClass} { + set rightMenuEntries {} + html::ul -class "nav navbar-nav px-3" { + foreach entry [:children] { + if {[$entry istype ::xowiki::BootstrapNavbarDropdownMenu]} { + $entry render + } else { + lappend rightMenuEntries $entry + } + } } + if {[llength $rightMenuEntries] > 0} { + html::ul -class "nav navbar-nav [::xowiki::CSS class navbar-right]" { + foreach entry $rightMenuEntries { + $entry render + } + } + } } } - } - } - } - + } + # # BootstrapNavbarDropdownMenu - # + # ::xo::tdom::Class create BootstrapNavbarDropdownMenu \ -superclass Menu \ -parameter { text header {brand false} - } + } BootstrapNavbarDropdownMenu ad_instproc render {} {doku} { # TODO: Add support for group-headers # get group header set group " " - - html::ul -class "nav navbar-nav" { - html::li -class "dropdown" { - set class "dropdown-toggle" - if {[:brand]} {lappend class "navbar-brand"} - html::a -href "\#" -class $class -data-toggle "dropdown" { - html::t [:text] - html::b -class "caret" - } - html::ul -class "dropdown-menu" { - foreach dropdownmenuitem [:children] { - if {[$dropdownmenuitem set group] ne "" && [$dropdownmenuitem set group] ne $group } { - if {$group ne " "} { - html::li -class "divider" - } - set group [$dropdownmenuitem set group] + + html::li -class "nav-item dropdown" { + set class "nav-link dropdown-toggle" + if {${:brand}} { + lappend class "navbar-brand" + } + set data_attribute [expr {[::xowiki::CSS toolkit] eq "bootstrap5" ? "data-bs" : "data"}] + html::a -href "\#" -class $class -$data_attribute-toggle "dropdown" { + html::t ${:text} + } + html::ul -class "dropdown-menu" { + foreach dropdownmenuitem [:children] { + if {[$dropdownmenuitem set group] ne "" + && [$dropdownmenuitem set group] ne $group + } { + if {$group ne " "} { + html::li -class "divider dropdown-divider" } - $dropdownmenuitem render + set group [$dropdownmenuitem set group] } + $dropdownmenuitem render } } } } - + # # BootstrapNavbarDropdownMenuItem - # + # ::xo::tdom::Class create BootstrapNavbarDropdownMenuItem \ -superclass MenuItem \ -parameter { {href "#"} helptext - } - + } + BootstrapNavbarDropdownMenuItem ad_instproc render {} {doku} { - html::li -class [expr {${:href} eq "" ? "disabled": ""}] { - html::a [:get_attributes target href title id] { + set disabledClass [expr {${:href} eq "" ? "disabled" : ""}] + html::li -class [string trimright "nav-item $disabledClass"] { + set :CSSclass [string trimright "dropdown-item $disabledClass"] + html::a [:get_attributes target href title id {CSSclass class}] { html::t ${:text} } } + html::t \n if {[info exists :listener] && ${:listener} ne ""} { lassign ${:listener} type body template::add_event_listener -event $type -id ${:id} \ -preventdefault=false -script $body } } - + # # BootstrapNavbarDropzone - # + # ::xo::tdom::Class create BootstrapNavbarDropzone \ -superclass MenuComponent \ -parameter { + {label "DropZone"} {href "#"} - text - uploader + {text ""} + {disposition File} + {file_name_prefix ""} + } \ + -ad_doc { + + Dropzone widget for drag and drop of files, e.g. in the + menubar. The widget provides added support for updating the + current page with feedback of the dropped files. + + @param href URL for POST request + @param label Text to be displayed at the place where files are + dropped to + @param file_name_prefix prefix for files being uploaded + (used e.g. by the online exam). + @param disposition define, what happens after the file was + uploaded, e.g. whether the content has to be + transformed, stored and displayed later. } - BootstrapNavbarDropzone instproc js {-uploadlink:required} { - html::script -type "text/javascript" -nonce $::__csp_nonce { - html::t [subst -nocommands { + BootstrapNavbarDropzone instproc js {} { + html::script -type "text/javascript" -nonce [security::csp::nonce] { + html::t { + function($) { 'use strict'; var dropZone = document.getElementById('drop-zone'); var uploadForm = document.getElementById('js-upload-form'); var progressBar = document.getElementById('dropzone-progress-bar'); + var dropZoneResponse = document.getElementById('thumbnail-files-wrapper'); var uploadFileRunning = 0; - - var startUpload = function(files, csrf) { + var uploadFilesStatus = []; + var uploadFilesResponse = []; + + var startUpload = function(files, disposition, url, prefix, csrf) { + //console.log("files " + files + " dispo '"+ disposition + "' url " + url + " prefix " + prefix); if (typeof files !== "undefined") { for (var i=0, l=files.length; i thumbnail_files_setup(el)); + } if (uploadFileRunning < 1) { - location.reload(true); + if (dropZoneResponse) { + + // We are done with all uploads. When the response is + // provided, it was updated above already in the web + // page, but we have still to reset the progress bar + // to indicate that we are done. + + progressBar.style.width = '0%'; + + } else { + // Reload the page to trigger a refresh + location.reload(true); + } } }, false); - xhr.open("post", url, true); + xhr.open("post", fullUrl, true); formData.append("upload", file); formData.append("__csrf_token", csrf); uploadFileRunning++; xhr.send(formData); } uploadForm.addEventListener('submit', function(e) { + // + // Input handler for classical form submit + // var input = document.getElementById('js-upload-files'); var uploadFiles = input.files; var csrf = input.form.elements["__csrf_token"].value; e.preventDefault(); - startUpload(input.files, csrf) + //console.log("Submit handler"); + startUpload(input.files, + input.dataset.disposition ?? 'File', + input.dataset.url, + input.dataset.file_name_prefix ?? '', + csrf); }) dropZone.ondrop = function(e) { + // + // Input handler for drag & drop + // e.preventDefault(); this.className = 'upload-drop-zone'; var form = document.getElementById('js-upload-files').form; var csrf = form.elements["__csrf_token"].value; - startUpload(e.dataTransfer.files, csrf) + var input = document.getElementById('js-upload-files'); + //console.log("Drop handler"); + startUpload(e.dataTransfer.files, + input.dataset.disposition ?? 'File', + input.dataset.url, + input.dataset.file_name_prefix ?? '', + csrf); } dropZone.ondragover = function() { @@ -226,11 +298,11 @@ return false; } } (jQuery); - }] + } } } - + BootstrapNavbarDropzone ad_instproc render {} {doku} { if {${:href} ni {"" "#"}} { html::li { @@ -239,32 +311,37 @@ -id "js-upload-form" { html::div -class "form-inline" { html::div -class "form-group" { - html::input -type "file" -name {files[]} -id "js-upload-files" -multiple multiple + html::input \ + -type "file" \ + -name {files[]} \ + -id "js-upload-files" \ + -data-file_name_prefix ${:file_name_prefix} \ + -data-url ${:href} \ + -data-disposition ${:disposition} \ + -multiple multiple } html::button -type "submit" -class "btn btn-sm btn-primary" -id "js-upload-submit" { html::t ${:text} } ::html::CSRFToken } } - } - html::li { html::div -class "upload-drop-zone" -id "drop-zone" { - html::t "DropZone" + html::span {html::t ${:label}} html::div -class "progress" { html::div -style "width: 0%;" -class "progress-bar" -id dropzone-progress-bar { html::span -class "sr-only" {html::t ""} } } } } - :js -uploadlink ${:href}&uploader=${:uploader} + :js } } # # BootstrapNavbarModeButton - # + # ::xo::tdom::Class create BootstrapNavbarModeButton \ -superclass MenuItem \ -parameter { @@ -273,14 +350,14 @@ {button} {CSSclass "checkbox-slider--b-flat"} {spanStyle "padding-left: 6px; padding-right: 6px;"} - } + } BootstrapNavbarModeButton instproc js {} { # # In the current implementation, the page refreshes itself after # successful mode change. This could be made configurable. # - html::script -type "text/javascript" -nonce $::__csp_nonce { + html::script -type "text/javascript" -nonce [security::csp::nonce] { html::t { function mode_button_ajax_submit(form) { $.ajax({ @@ -293,20 +370,20 @@ }; } html t [subst { - document.getElementById('[:id]').addEventListener('click', function (event) { + document.getElementById('${:id}').addEventListener('click', function (event) { mode_button_ajax_submit(this.form); }); }] } } - - BootstrapNavbarModeButton ad_instproc render {} {doku} { + + BootstrapNavbarModeButton instproc render {} { html::li { html::form -class "form" -method "POST" -action ${:href} { html::div -class "checkbox ${:CSSclass}" { html::label -class "checkbox-inline" { set checked [expr {${:on} ? {-checked true} : ""}] - html::input -id [:id] -class "debug form-control" -name "debug" -type "checkbox" {*}$checked + html::input -id ${:id} -class "debug form-control" -name "debug" -type "checkbox" {*}$checked html::span -style ${:spanStyle} {html::t ${:text}} html::input -name "modebutton" -type "hidden" -value "${:button}" } @@ -316,6 +393,32 @@ } } + ::xo::tdom::Class create BootstrapCollapseButton \ + -parameter { + {id:required} + {toggle:required} + {direction:required} + {label:required} + } + + BootstrapCollapseButton instproc render {} { + switch [::xowiki::CSS toolkit] { + "bootstrap" { + template::add_script -src urn:ad:js:bootstrap3 + ::html::button -type button -class "btn btn-xs" -data-toggle ${:toggle} -data-target "#${:id}" { + ::html::span -class "glyphicon glyphicon-chevron-${:direction}" {::html::t ${:label}} + } + } + "bootstrap5" { + template::add_script -src urn:ad:js:bootstrap5 + ::html::button -type button -class "btn btn-sm" -data-bs-toggle ${:toggle} -data-bs-target "#${:id}" { + ::html::i -class "bi bi-chevron-${:direction}" {::html::t ${:label}} + } + } + } + } + + # ======================================================= # ::xo::library doc { # ... styling for bootstrap menubar ... @@ -343,8 +446,8 @@ # # ::xo::library source_dependent # ======================================================= - - + + # -------------------------------------------------------------------------- # Render MenuBar in bootstrap fashion # -------------------------------------------------------------------------- @@ -360,10 +463,11 @@ ::xowiki::BootstrapNavbarDropzone \ -text [:get_prop $value label] \ -href [:get_prop $value url] \ - -uploader [:get_prop $value uploader] {} + -disposition [:get_prop $value disposition File] {} } "ModeButton" { - template::head::add_css -href "/resources/xotcl-core/titatoggle/titatoggle-dist.css" + template::head::add_css \ + -href "/resources/xotcl-core/titatoggle/titatoggle-dist.css" ::xowiki::BootstrapNavbarModeButton \ -text [:get_prop $value label] \ @@ -413,14 +517,14 @@ :render_with BootstrapTableRenderer $trn_mixin next } - + Class create BootstrapTableRenderer \ -superclass TABLE3 \ -instproc init_renderer {} { next set :css.table-class "table table-striped" - set :css.tr.even-class even - set :css.tr.odd-class odd + set :css.tr.even-class "align-middle" + set :css.tr.odd-class "align-middle" set :id [::xowiki::Includelet js_name [::xowiki::Includelet html_id [self]]] } @@ -433,14 +537,24 @@ } } } - set children [:children] + ad_try { + set children [:children] + } on error {errorMsg} { + html::div -class "alert alert-danger" { + html::span -class danger { + html::t $errorMsg + } + } + return + } html::tbody { foreach line [:children] { html::tr -class [expr {[incr :__rowcount]%2 ? ${:css.tr.odd-class} : ${:css.tr.even-class} }] { foreach field [[self]::__columns children] { if {[$field hide]} continue if {[$field istype HiddenField]} continue - html::td [concat [list class list] [$field html]] { + set CSSclass [list "list" {*}[$field CSSclass]] + html::td [concat [list class $CSSclass] [$field html]] { $field render-data $line } } @@ -451,73 +565,80 @@ BootstrapTableRenderer instproc render-bulkactions {} { set bulkactions [[self]::__bulkactions children] - html::div -class "btn-group" -role group -aria-label "Bulk actions" { - html::t "Bulk-Actions:" - set bulkaction_container [[lindex $bulkactions 0] set __parent] - set name [$bulkaction_container set __identifier] + if {[llength $bulkactions] > 0} { + html::div -class "btn-group align-items-center" -role group -aria-label "Bulk actions" { + html::span -class "bulk-action-label" { + html::t "#xotcl-core.Bulk_actions#:" + } - foreach ba $bulkactions { - set id [::xowiki::Includelet html_id $ba] html::ul -class compact { - html::li { - # For some reason, btn-secondary seems not to be available - # for the "a" tag, so we set the border-color manually. - html::a -class "btn btn-secondary" -rule button \ - -title [$ba tooltip] -href # \ - -style "border-color: #ccc;" \ - -id $id { - html::t [$ba label] + set bulkaction_container [[lindex $bulkactions 0] set __parent] + set name [$bulkaction_container set __identifier] + + foreach bulk_action $bulkactions { + set id [::xowiki::Includelet html_id $bulk_action] + html::li { + html::a -class [::xowiki::CSS class bulk-action] -rule button \ + -title [$bulk_action tooltip] -href # \ + -id $id { + html::t [$bulk_action label] + } + } + set script [subst { + acs_ListBulkActionClick("$name","[$bulk_action url]"); + }] + if {[$bulk_action confirm_message] ne ""} { + set script [subst { + if (confirm('[$bulk_action confirm_message]')) { + $script } + }] + } + template::add_event_listener \ + -id $id \ + -preventdefault=false \ + -script $script } } - template::add_body_script -script [subst { - document.getElementById('$id').addEventListener('click', function (event) { - acs_ListBulkActionClick("$name","[$ba url]"); - }, false); - }] } } } BootstrapTableRenderer instproc render {} { - ::xo::Page requireCSS urn:ad:css:bootstrap3 - security::csp::require style-src maxcdn.bootstrapcdn.com - security::csp::require font-src maxcdn.bootstrapcdn.com - - if {![:isobject [self]::__actions]} {:actions {}} - if {![:isobject [self]::__bulkactions]} {:__bulkactions {}} + ::xowiki::CSS require_toolkit -css + + if {![nsf::is object [self]::__actions]} {:actions {}} + if {![nsf::is object [self]::__bulkactions]} {:__bulkactions {}} set bulkactions [[self]::__bulkactions children] - if {[llength $bulkactions]>0} { + if {[[self]::__bulkactions exists __identifier]} { set name [[self]::__bulkactions set __identifier] - } else { - set name [::xowiki::Includelet js_name [self]] - } - if {[llength $bulkactions]>0} { html::div -id ${:id}_wrapper -class "table-responsive" { - html::form -name $name -id $name -method POST { + html::form -name $name -id $name -method POST { html::div -id ${:id}_container { html::table -id ${:id} -class ${:css.table-class} { :render-actions :render-body } - if {[llength $bulkactions]>0} { :render-bulkactions } + :render-bulkactions } } } } else { - #nesting forms inside a xowf page will place the action buttons at the wrong place! + set name [::xowiki::Includelet js_name [self]] + # + # Nesting forms inside an xowf page will place the action + # buttons at the wrong place! + # html::div -id ${:id}_wrapper -class "table-responsive" { html::div -id ${:id}_container { html::table -id ${:id} -class ${:css.table-class} { :render-actions :render-body } - if {[llength $bulkactions]>0} { :render-bulkactions } } } } } - #Class create BootstrapTableRenderer::AnchorField -superclass TABLE::AnchorField Class create BootstrapTableRenderer::AnchorField \ @@ -530,18 +651,28 @@ " \ -instproc render-data {line} { - set __name [:name] - if {[$line exists $__name.href] && - [set href [$line set $__name.href]] ne ""} { - # use the CSS class rather from the Field than not the line + set __name ${:name} + if {[$line exists $__name.href] + && [set href [$line set $__name.href]] ne "" + } { $line instvar [list $__name.title title] [list $__name.target target] if {[$line exists $__name.onclick]} { set id [::xowiki::Includelet html_id $line] template::add_event_listener \ -id $id \ -script "[$line set $__name.onclick];" } + # + # The default class is from the field definition. Append to this value + # the class coming from the entry line. + # set CSSclass ${:CSSclass} + if {[$line exists $__name.CSSclass]} { + set lineCSSclass [$line set $__name.CSSclass] + if {$lineCSSclass ne ""} { + append CSSclass " " $lineCSSclass + } + } html::a [:get_local_attributes href title {CSSclass class} target id] { return [next] } @@ -557,6 +688,139 @@ Class create BootstrapTableRenderer::BulkAction -superclass TABLE::BulkAction } +namespace eval ::xowiki::bootstrap { + + ad_proc ::xowiki::bootstrap::card { + -title:required + -body:required + {-CSSclass ""} + } { + Render a Bootstrap Card. + + @return HTML + } { + return [ns_trim -delimiter | [subst { + |
+ |
$title
+ |
$body
+ |
+ }]] + } + + ad_proc ::xowiki::bootstrap::icon { + -name:required + -style + -CSSclass + } { + Render a Bootstrap Icon. + + @return HTML + } { + # + set name [xowiki::CSS class $name] + set styleAtt [expr {[info exists style] ? "style='$style'" : ""}] + set CSSclass [expr {[info exists CSSclass] ? " $CSSclass" : ""}] + switch [::xowiki::CSS toolkit] { + "bootstrap" { + return [subst {}] + } + default { + return [subst {}] + } + } + } + + + ad_proc ::xowiki::bootstrap::modal_dialog { + -id:required + -title:required + {-subtitle ""} + -body:required + } { + Generic modal dialog wrapper. + @param id + @param title HTML markup for the modal title (can contain tags) + @param subtitle HTML markup for the modal subtitle (can contain tags) + @param body HTML markup for the modal body (can contain tags) + + @return HTML markup + } { + if {$subtitle ne ""} { + set subtitle [subst {}] + } + if {[::xowiki::CSS toolkit] eq "bootstrap5"} { + set data_attribute "data-bs" + ::security::csp::require img-src data: + set close_button_label "" + set before_close "" + set after_close "" + } else { + set data_attribute "data" + set close_button_label {} + set before_close "" + set after_close "" + } + + return [ns_trim -delimiter | [subst { + | + }]] + } + + + + ad_proc ::xowiki::bootstrap::modal_dialog_popup_button { + -target:required + -label:required + {-title ""} + {-CSSclass ""} + } { + Generic modal dialog wrapper. + @param target ID of the target modal dialog + @param title title for the anchor (help popup), plain text + @param label HTML markup for the modal popup label (can contain tags) + + @return HTML markup + } { + if {[::xowiki::CSS toolkit] eq "bootstrap5"} { + set data_attribute "data-bs" + } else { + set data_attribute "data" + } + return [ns_trim -delimiter | [subst { + | + | $label + | + }]] + } +} + + + ::xo::library source_dependent #