bootstrap-procs.tcl

bootstrap procs: provide some (initial) support for bootstrap library

Location:
packages/xowiki/tcl/bootstrap-procs.tcl
Created:
2014-04-14
Authors:
Günter Ernst
Gustaf Neumann
CVS Identification:
$Id: bootstrap-procs.tcl,v 1.16 2024/10/27 18:23:48 gustafn Exp $

Procedures in this file

Detailed information

xowiki::bootstrap::card (public)

 xowiki::bootstrap::card -title title -body body [ -CSSclass CSSclass ]

Render a Bootstrap Card.

Switches:
-title (required)
-body (required)
-CSSclass (optional)
Returns:
HTML

Partial Call Graph (max 5 caller/called nodes):
%3 Class ::xowf::test_item::Answer_manager Class ::xowf::test_item::Answer_manager (public) xowiki::bootstrap::card xowiki::bootstrap::card Class ::xowf::test_item::Answer_manager->xowiki::bootstrap::card Class ::xowf::test_item::Question_manager Class ::xowf::test_item::Question_manager (public) Class ::xowf::test_item::Question_manager->xowiki::bootstrap::card xowf::test_item::Answer_manager instproc answers_panel xowf::test_item::Answer_manager instproc answers_panel (public) xowf::test_item::Answer_manager instproc answers_panel->xowiki::bootstrap::card xowf::test_item::Answer_manager instproc waiting_room_message xowf::test_item::Answer_manager instproc waiting_room_message (public) xowf::test_item::Answer_manager instproc waiting_room_message->xowiki::bootstrap::card xowf::test_item::Question_manager instproc hint_box xowf::test_item::Question_manager instproc hint_box (protected) xowf::test_item::Question_manager instproc hint_box->xowiki::bootstrap::card

Testcases:
No testcase defined.

xowiki::bootstrap::icon (public)

 xowiki::bootstrap::icon -name name [ -style style ] \
    [ -CSSclass CSSclass ]

Render a Bootstrap Icon.

Switches:
-name (required)
-style (optional)
-CSSclass (optional)
Returns:
HTML

Partial Call Graph (max 5 caller/called nodes):
%3 Class ::xowf::test_item::Answer_manager Class ::xowf::test_item::Answer_manager (public) xowiki::bootstrap::icon xowiki::bootstrap::icon Class ::xowf::test_item::Answer_manager->xowiki::bootstrap::icon Class ::xowf::test_item::Question_manager Class ::xowf::test_item::Question_manager (public) Class ::xowf::test_item::Question_manager->xowiki::bootstrap::icon xowf::test_item::Answer_manager instproc render_answers xowf::test_item::Answer_manager instproc render_answers (public) xowf::test_item::Answer_manager instproc render_answers->xowiki::bootstrap::icon xowf::test_item::Answer_manager instproc render_print_button xowf::test_item::Answer_manager instproc render_print_button (protected) xowf::test_item::Answer_manager instproc render_print_button->xowiki::bootstrap::icon xowf::test_item::Question_manager instproc aggregated_form xowf::test_item::Question_manager instproc aggregated_form (public) xowf::test_item::Question_manager instproc aggregated_form->xowiki::bootstrap::icon

Testcases:
No testcase defined.

xowiki::bootstrap::modal_dialog (public)

 xowiki::bootstrap::modal_dialog -id id -title title \
    [ -subtitle subtitle ] -body body

Generic modal dialog wrapper.

Switches:
-id (required)
-title (required)
HTML markup for the modal title (can contain tags)
-subtitle (optional)
HTML markup for the modal subtitle (can contain tags)
-body (required)
HTML markup for the modal body (can contain tags)
Returns:
HTML markup

Partial Call Graph (max 5 caller/called nodes):
%3 Class ::xowf::test_item::Answer_manager Class ::xowf::test_item::Answer_manager (public) xowiki::bootstrap::modal_dialog xowiki::bootstrap::modal_dialog Class ::xowf::test_item::Answer_manager->xowiki::bootstrap::modal_dialog Class ::xowf::test_item::Question_manager Class ::xowf::test_item::Question_manager (public) Class ::xowf::test_item::Question_manager->xowiki::bootstrap::modal_dialog xowf::test_item::Answer_manager instproc grading_dialog_setup xowf::test_item::Answer_manager instproc grading_dialog_setup (public) xowf::test_item::Answer_manager instproc grading_dialog_setup->xowiki::bootstrap::modal_dialog xowf::test_item::Question_manager instproc exam_configuration_popup xowf::test_item::Question_manager instproc exam_configuration_popup (public) xowf::test_item::Question_manager instproc exam_configuration_popup->xowiki::bootstrap::modal_dialog security::csp::require security::csp::require (public) xowiki::bootstrap::modal_dialog->security::csp::require

Testcases:
No testcase defined.

xowiki::bootstrap::modal_dialog_popup_button (public)

 xowiki::bootstrap::modal_dialog_popup_button -target target \
    -label label [ -title title ] [ -CSSclass CSSclass ]

Generic modal dialog wrapper.

Switches:
-target (required)
ID of the target modal dialog
-label (required)
HTML markup for the modal popup label (can contain tags)
-title (optional)
title for the anchor (help popup), plain text
-CSSclass (optional)
Returns:
HTML markup

Partial Call Graph (max 5 caller/called nodes):
%3 Class ::xowf::test_item::Question_manager Class ::xowf::test_item::Question_manager (public) xowiki::bootstrap::modal_dialog_popup_button xowiki::bootstrap::modal_dialog_popup_button Class ::xowf::test_item::Question_manager->xowiki::bootstrap::modal_dialog_popup_button xowf::test_item::Question_manager instproc exam_configuration_popup xowf::test_item::Question_manager instproc exam_configuration_popup (public) xowf::test_item::Question_manager instproc exam_configuration_popup->xowiki::bootstrap::modal_dialog_popup_button

Testcases:
No testcase defined.
[ hide source ] | [ make this the default ]

Content File Source

::xo::library doc {
  bootstrap procs: provide some (initial) support for bootstrap library

  @creation-date 2014-04-14
  @author Günter Ernst
  @author Gustaf Neumann
  @cvs-id $Id: bootstrap-procs.tcl,v 1.16 2024/10/27 18:23:48 gustafn Exp $
}

::xo::library require menu-procs
::xo::library require -package xotcl-core 30-widget-procs

namespace eval ::xowiki {
  #
  # Minimal implementation of Bootstrap "navbar"
  # currently only "dropdown" elements are supported within the navbar
  # TODO: add support to include:
  # - forms
  # - buttons
  # - text
  # - Non-nav links
  # - component alignment
  # - navbar positioning
  # - navbar inverting

  ::xo::tdom::Class create BootstrapNavbar \
      -superclass Menu \
      -parameter {
        {autorender false}
        {menubar}
        {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
    ::template::CSS require_toolkit -css -js
    next
  }


  BootstrapNavbar ad_instproc render {} {
    http://getbootstrap.com/components/#navbar
  } {
    html::nav \
        -class [template::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 [::template::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::li -class "nav-item dropdown" {
      set class "nav-link dropdown-toggle"
      if {${:brand}} {
        lappend class "navbar-brand"
      }
      set data_attribute [expr {[::template::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"
            }
            set group [$dropdownmenuitem set group]
          }
          $dropdownmenuitem render
        }
      }
    }
  }

  #
  # BootstrapNavbarDropdownMenuItem
  #
  ::xo::tdom::Class create BootstrapNavbarDropdownMenuItem \
      -superclass MenuItem \
      -parameter {
        {href "#"}
        helptext
      }

  BootstrapNavbarDropdownMenuItem ad_instproc render {} {doku} {
    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 ""}
        {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 {} {
    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 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<l; i++) {
                // Send the file as multiple single requests and
                // not as a single post containing all entries. This
                // gives users with older NaviServers or AOLserver the chance
                // drop multiple files.
                uploadFile(files[i], disposition, url, prefix, csrf);
             }

            } else {
              alert("No support for the File API in this web browser");
            }
          }

          var uploadFile = function(file, disposition, url, prefix, csrf) {
            var xhr;
            var formData = new FormData();
            var fullName = (prefix == "" ? file.name : prefix + '/' + file.name);
            var fullUrl = url
            + "&disposition=" + encodeURIComponent(disposition)
            + "&name=" + encodeURIComponent(fullName);

            xhr = new XMLHttpRequest();
            xhr.upload.addEventListener("progress", function (evt) {
              if (evt.lengthComputable) {

                // For multiple drop files, we should probably we
                // should sum up the sizes.  However, since the
                // uploads are in parallel, this is already useful.

                progressBar.style.width = (evt.loaded / evt.total) * 100 + "%";
              } else {
                // No data to calculate on
              }
            }, false);
            xhr.addEventListener("load", function (event) {
              uploadFileRunning--;
              uploadFilesStatus.push(event.currentTarget.status);
              uploadFilesResponse.push(event.currentTarget.response);
              //console.log("ended with status " + event.currentTarget.status);
              //console.log("running: " + uploadFileRunning);
              if (dropZoneResponse) {

                // We have a dropzone response and update this in the
                // web page.

                dropZoneResponse.innerHTML = uploadFilesResponse[uploadFilesResponse.length-1];
                dropZoneResponse.querySelectorAll('.thumbnail-file').forEach(el => thumbnail_files_setup(el));
              }
              if (uploadFileRunning < 1) {
                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", 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();
            //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;
            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() {
            this.className = 'upload-drop-zone drop';
            return false;
          }

          dropZone.ondragleave = function() {
            this.className = 'upload-drop-zone';
            return false;
          }
        } (jQuery);
      }
    }
  }


  BootstrapNavbarDropzone ad_instproc render {} {doku} {
    if {${:href} ni {"" "#"}} {
      html::li {
        html::form -method "post" -enctype "multipart/form-data" \
            -style "display: none;" \
            -id "js-upload-form" {
              html::div -class "form-inline" {
                html::div -class "form-group" {
                  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::div -class "upload-drop-zone" -id "drop-zone" {
          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
    }
  }

  #
  # BootstrapNavbarModeButton
  #
  ::xo::tdom::Class create BootstrapNavbarModeButton \
      -superclass MenuItem \
      -parameter {
        {href "#"}
        {on:boolean false}
        {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 [security::csp::nonce] {
      html::t {
        function mode_button_ajax_submit(form) {
          $.ajax({
            type: "POST",
            url: $(form).attr('action'),
            data: $(form).serialize(),
            success: function(msg) { location.reload(true); },
            error: function(){alert("failure");}
          });
        };
      }
      html t [subst {
        document.getElementById('${:id}').addEventListener('click', function (event) {
          mode_button_ajax_submit(this.form);
        });
      }]
    }
  }

  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::span -style ${:spanStyle} {html::t ${:text}}
            html::input -name "modebutton" -type "hidden" -value "${:button}"
          }
        }
      }
      :js
    }
  }

  ::xo::tdom::Class create BootstrapCollapseButton \
      -parameter {
        {id:required}
        {toggle:required}
        {direction:required}
        {label:required}
      }

  BootstrapCollapseButton instproc render {} {
    switch [::template::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 ...
  # }
  #
  # ::xo::db::require package xowiki
  # ::xo::library require -package xowiki bootstrap-procs
  #
  # namespace eval ::mystyle {
  #   #
  #   # Define mixins for the classes. One can overload e.g. parameters
  #   # via the constructor, or one can e.g. overload the full render
  #   # method.
  #   #
  #   ::xo::tdom::Class create ::mystyle::BootstrapNavbarModeButton \
  #       -superclass ::xowiki::MenuItem
  #
  #   ::xowiki::BootstrapNavbarModeButton instproc init args {
  #     set :CSSclass checkbox-slider--a
  #     set :spanStyle "padding-left: 4ex; padding-right: 2ex;"
  #     next
  #   }
  #   ::xowiki::BootstrapNavbarModeButton instmixin ::mystyle::BootstrapNavbarModeButton
  # }
  #
  # ::xo::library source_dependent
  # =======================================================


  # --------------------------------------------------------------------------
  # Render MenuBar in bootstrap fashion
  # --------------------------------------------------------------------------
  ::xowiki::MenuBar instproc render-bootstrap {} {
    set dict [:content]
    set mb [::xowiki::BootstrapNavbar \
                -id [:get_prop $dict id] \
                -menubar [self] {
                  foreach {att value} $dict {
                    if {$att eq "id"} continue
                    switch [:get_prop $value kind] {
                      "DropZone" {
                        ::xowiki::BootstrapNavbarDropzone \
                            -text [:get_prop $value label] \
                            -href [:get_prop $value url] \
                            -disposition [:get_prop $value disposition File] {}
                      }
                      "ModeButton" {
                        template::head::add_css \
                            -href "/resources/xotcl-core/titatoggle/titatoggle-dist.css"

                        ::xowiki::BootstrapNavbarModeButton \
                            -text [:get_prop $value label] \
                            -href [:get_prop $value url] \
                            -button [:get_prop $value button admin] \
                            -on [:get_prop $value on] {}
                      }
                      "MenuButton" {
                        # render erverthing as a dropdown
                        ::xowiki::BootstrapNavbarDropdownMenu \
                            -text [:get_prop $value label] {
                              #ns_log notice "... dropdown att $att menu $value"
                              foreach {item_att item} $value {
                                if {[string match {[a-z]*} $item_att]} continue
                                ::xowiki::BootstrapNavbarDropdownMenuItem \
                                    -text [:get_prop $item label] \
                                    -href [:get_prop $item url] \
                                    -group [:get_prop $item group] \
                                    -listener [:get_prop $item listener] \
                                    {}
                              }
                            }
                      }
                    }
                  }}]
    #ns_log notice "call menubar asHTML"
    return [$mb asHTML]
  }
}

###############################################################################
#   Bootstrap table
###############################################################################

# TODO Allow renderers from other namespaces in 30-widget-procs

namespace eval ::xo::Table {

  Class create ::xowiki::BootstrapTable \
      -superclass ::xo::Table \
      -parameter {
        skin
      }

  ::xowiki::BootstrapTable instproc init {} {
    set trn_mixin [expr {[lang::util::translator_mode_p] ?"::xo::TRN-Mode" : ""}]
    :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 "align-middle"
        set :css.tr.odd-class "align-middle"
        set :id [::xowiki::Includelet js_name [::xowiki::Includelet html_id [self]]]
      }

  BootstrapTableRenderer instproc render-body {} {
    html::thead {
      html::tr -class list-header {
        foreach o [[self]::__columns children] {
          if {[$o hide]} continue
          $o render
        }
      }
    }
    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
            set CSSclass [list "list" {*}[$field CSSclass]]
            html::td [concat [list class $CSSclass] [$field html]] {
              $field render-data $line
            }
          }
        }
      }
    }
  }

  BootstrapTableRenderer instproc render-bulkactions {} {
    set bulkactions [[self]::__bulkactions children]
    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#:"
        }

        html::ul -class compact {
          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 [::template::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
          }
        }
      }
    }
  }

  BootstrapTableRenderer instproc render {} {
    ::template::CSS require_toolkit -css

    if {![nsf::is object [self]::__actions]} {:actions {}}
    if {![nsf::is object [self]::__bulkactions]} {:__bulkactions {}}
    set bulkactions [[self]::__bulkactions children]
    if {[[self]::__bulkactions exists __identifier]} {
      set name [[self]::__bulkactions set __identifier]
      html::div -id ${:id}_wrapper -class "table-responsive" {
        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
            }
            :render-bulkactions
          }
        }
      }
    } else {
      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
          }
        }
      }
    }
  }
  #Class create BootstrapTableRenderer::AnchorField -superclass TABLE::AnchorField

  Class create BootstrapTableRenderer::AnchorField \
      -superclass TABLE::Field \
      -ad_doc "
            In addition to the standard TableWidget's AnchorField, we also allow the attributes
            <ul>
                <li>onclick
                <li>target
            </ul>
        " \
      -instproc render-data {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]
          }
        }
        next
      }

  Class create BootstrapTableRenderer::Action -superclass TABLE::Action
  Class create BootstrapTableRenderer::Field -superclass TABLE::Field
  Class create BootstrapTableRenderer::HiddenField -superclass TABLE::HiddenField
  Class create BootstrapTableRenderer::ImageField -superclass TABLE::ImageField
  Class create BootstrapTableRenderer::ImageAnchorField -superclass TABLE::ImageAnchorField
  Class create BootstrapTableRenderer::BulkAction -superclass TABLE::BulkAction
}

namespace eval ::xowiki::bootstrap {

  d_proc ::xowiki::bootstrap::card {
    -title:required
    -body:required
    {-CSSclass ""}
  } {
    Render a Bootstrap Card.

    @return HTML
  } {
    return [ns_trim -delimiter | [subst {
      |<div class="[template::CSS class card] $CSSclass">
      |  <div class="[template::CSS class card-header]">$title</div>
      |  <div class="[template::CSS class card-body]">$body</div>
      |</div>
    }]]
  }

  d_proc ::xowiki::bootstrap::icon {
    -name:required
    -style
    -CSSclass
  } {
    Render a Bootstrap Icon.

    @return HTML
  } {
    #<span class="glyphicon glyphicon-cog" aria-hidden="true" style="float: right;"></span>
    set name [template::CSS class $name]
    set styleAtt [expr {[info exists style] ? "style='$style'" : ""}]
    set CSSclass [expr {[info exists CSSclass] ? $CSSclass" : ""}]
    switch [::template::CSS toolkit] {
      "bootstrap" {
        return [subst {<span class="glyphicon glyphicon-$name$CSSclass" aria-hidden="true" $styleAtt></span>}]
      }
      default {
        return [subst {<i class="bi bi-$name$CSSclass" aria-hidden="true" $styleAtt></i>}]
      }
    }
  }


  d_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 {<p class="modal-subtitle">$subtitle</p>}]
    }
    if {[::template::CSS toolkit] eq "bootstrap5"} {
      set data_attribute "data-bs"
      ::security::csp::require img-src data:
      set close_button_label ""
      set before_close  "<h4 class='modal-title' id='configurationModalTitle'>$title</h4>"
      set after_close  ""
    } else {
      set data_attribute "data"
      set close_button_label {<span aria-hidden="true">&#215;</span>}
      set before_close  ""
      set after_close  "<h4 class='modal-title' id='configurationModalTitle'>$title</h4>"
    }

    return [ns_trim -delimiter | [subst {
      |<div class="modal fade" id="$id" tabindex="-1" role="dialog"
      |     aria-labelledby="$id-label" aria-hidden="true">
      |  <div class="modal-dialog" role="document">
      |    <div class="modal-content">
      |      <div class="modal-header">
      |        $before_close<adp:button type="button" class="close"
      |           data-dismiss="modal" aria-label="Close">$close_button_label
      |        </adp:button>$after_close
      |      </div>
      |      <div class="modal-body">$subtitle
      |        <form class="form-horizontal" id="configuration-form" role="form" action="#" method="post">
      |        $body
      |        </form>
      |      </div>
      |      <div class="modal-footer">
      |        <adp:button type="button" class="btn btn-default"
      |                data-dismiss="modal">#acs-kernel.common_Cancel#
      |        </adp:button>
      |        <adp:button id="$id-confirm" type="button" class="btn btn-primary confirm"
      |                data-dismiss="modal">#acs-subsite.Confirm#
      |        </adp:button>
      |      </div>
      |    </div>
      |  </div>
      |</div>
    }]]
  }



  d_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 {[::template::CSS toolkit] eq "bootstrap5"} {
      set data_attribute "data-bs"
    } else {
      set data_attribute "data"
    }
    return [ns_trim -delimiter | [subst {
      |<a class="$CSSclass" href="#" title="$title"
      |  $data_attribute-toggle="modal" $data_attribute-target='#$target'>
      |  $label
      |</a>
    }]]
  }
}



::xo::library source_dependent

#
# Local variables:
#    mode: tcl
#    tcl-indent-level: 2
#    indent-tabs-mode: nil
# End: