• Publicity: Public Only All

tree-procs.tcl

Classes for creating, manageing and rendering trees

Location:
packages/xowiki/tcl/tree-procs.tcl
Created:
2009-05-29
Author:
Gustaf Neumann
CVS Identification:
$Id: tree-procs.tcl,v 1.27 2024/10/24 17:02:23 gustafn Exp $

Procedures in this file

Detailed information

[ hide source ] | [ make this the default ]

Content File Source

::xo::library doc {
  Classes for creating, manageing and rendering trees

  @creation-date 2009-05-29
  @author Gustaf Neumann
  @cvs-id $Id: tree-procs.tcl,v 1.27 2024/10/24 17:02:23 gustafn Exp $
}

namespace eval ::xowiki {
  #
  # ::xowiki::Tree
  #
  # This class manages the creation and rendering of the nodes of the
  # tree. It provides a name and id for rending in HTML.

  Class create Tree \
      -superclass ::xo::OrderedComposite \
      -parameter {
        {name ""}
        {owner}
        {verbose 0}
        id
      }

  #
  # Class methods
  #
  Tree proc renderer {style} {
    set renderer TreeRenderer=$style
    if {![:isclass $renderer]} {
      error "No such renderer $renderer (avalialble [info commands ::xowiki::TreeRenderer=*]"
    }
    return $renderer
  }

  Tree proc include_head_entries {{-renderer mktree} args} {
    [:renderer $renderer] include_head_entries {*}$args
  }

  #
  # Instance methods
  #
  Tree instproc init {} {
    # If there is no id specified, use the name as id.
    if {![info exists :id]} {
      set :id ${:name}
    }
  }

  Tree instproc add_item {
    -category
    -orderby
    -itemobj
    {-increasing:boolean true}
    {-open_item:boolean false}
  } {
    set items ${category}::items
    if {![nsf::is object $items]} {
      ::xo::OrderedComposite create $items
      if {[info exists orderby]} {
        set direction [expr {$increasing ? "increasing" : "decreasing"}]
        $items orderby \
            -order $direction \
            -type [ad_decode $orderby page_order index dictionary] \
            $orderby
      }
    }
    $items add $itemobj
    if {$open_item} {
      $category open_tree
      $itemobj set open_item 1
    }
  }
  Tree instproc open_tree {} {;}

  Tree instproc render {
    {-style mktree}
    {-js ""}
    {-context ""}
    {-properties ""}
  } {
    set renderer [[self class] renderer $style]
    $renderer set context $context
    $renderer set js $js
    TreeNode instmixin $renderer
    set content [$renderer render -properties $properties [self]]
    TreeNode instmixin ""
    if {[$renderer set js] ne ""} {
      template::add_body_script -script [$renderer set js]
    }
    return $content
  }

  Tree instproc add_pages {
    {-full:boolean false}
    {-remove_levels:integer 0}
    {-book_mode:boolean false}
    {-open_page ""}
    {-expand_all false}
    {-properties ""}
    -owner:object
    pages:object
  } {
    #
    # Add the pages of the ordered composite to the tree.  Note that
    # it is intended to preserve the order criterion of the provided
    # ordered composite in "pages". If you want to change the order
    # set it already for the passed-in ordered composite via the
    # method "orderby" of the OrderedComposite.
    #
    set tree(-1) [self]
    set :open_node($tree(-1)) 1
    set pos 0
    if {${:verbose}} {
      :log "add_pages want to add [llength [$pages children]] pages"
    }
    set ul_class [expr {[dict exists $properties CSSclass_ul] ?
                        [dict get $properties CSSclass_ul] : ""}]
    foreach o [$pages children] {
      $o instvar page_order title name
      if {![regexp {^(.*)[.]([^.]+)} $page_order _ parent]} {
        set parent ""
      }
      set page_number [$owner page_number $page_order $remove_levels]

      set level [regsub -all -- {[.]} [$o set page_order] _ page_order_js]
      if {${:verbose}} {
        :log "... work on [$o set page_order] level $level full $full"
      }
      if {$full || [info exists :open_node($parent)] || [info exists :open_node($page_order)]} {
        set href [$owner href $book_mode $name]
        set is_current [expr {$open_page eq $name}]
        set is_open [expr {$is_current || $expand_all}]
        set c [::xowiki::TreeNode new \
                   -level $level \
                   -object $o \
                   -owner [self] \
                   -label $title \
                   -prefix $page_number \
                   -href $href \
                   -highlight $is_current \
                   -expanded $is_open \
                   -open_requests 1 \
                   -verbose ${:verbose} \
                   -ul_class $ul_class]
        set tree($level$c
        for {set l [expr {$level - 1}]} {![info exists tree($l)]} {incr l -1} {}
        $tree($l) add $c
        if {$is_open} {
          $c open_tree
        }
      }
    }
    return $tree(-1)
  }

  #
  # ::xowiki::TreeNode
  #
  # The TreeNode represents an n-ary node storing its child nodes in
  # an ordered composite.  In addition to its children, every node may
  # have items associated. For example, a tree of categories can have
  # associated categorized items, which can be added via the method
  # "add_item".
  #
  Class create TreeNode -superclass Tree -parameter {
    level label pos
    {open_requests 0}
    count
    {href ""}
    object owner li_id ul_id ul_class
    {prefix ""}
    {expanded false}
    {highlight false}
  }

  TreeNode instproc open_tree {} {
    :open_requests 1
    :expanded true
    if {[info exists :__parent]} {${:__parent} open_tree}
  }

  TreeNode instproc some_child_has_items {} {
    foreach i [:children] {
      if {[nsf::is object ${i}::items]} {return 1}
      if {[$i some_child_has_items]} {return 1}
    }
    return 0
  }

  TreeNode instproc render {} {
    set content ""
    if {[nsf::is object [self]::items]} {
      foreach i [[self]::items children] {
        append cat_content [:render_item -highlight [$i exists open_item] $i ]
      }
      foreach c [:children] {append cat_content [$c render] \n}
      append content [:render_node -open [expr {[:open_requests]>0}] $cat_content]
    } elseif {${:open_requests} > 0 || [:some_child_has_items]} {
      set cat_content ""
      foreach c [:children] {append cat_content [$c render] \n}
      append content [:render_node -open true $cat_content]

    }
    if {${:verbose}} {
      :log "TreeNode items [nsf::is object [self]::items] render open_requests ${:open_requests} -> $content"
    }
    return $content
  }

  #
  # The rendering of trees is performed via rendering classes.  All
  # renderers are created and configured via the meta-class
  # TreeRenderer. This meta-class defines the common attributes and
  # behavior of all TreeRenders.
  #
  # In particular, the TreeRenders are defined to work with xowiki's
  # page fragment caching. Via page fragment caching, the result of
  # the rendering of includelets is cached. However, the renderer
  # might require additional CSS or JavaScript code, which has to be
  # included for the cached HTML fragment as well. Therefore, the
  # method "include_head_entries" is provided, which is called
  # independently from HTML generation.
  #
  #

  Class create TreeRenderer -superclass Class \
      -parameter {
        {subtree_wrapper_class}
        {li_expanded_atts ""}
        {highlight_atts {"style = 'font-weight:bold;'"}}
      }
  TreeRenderer instproc include_head_entries {args} {
    # to be overloaded
  }
  TreeRenderer instproc render {{-properties ""} tree} {
    set content ""
    foreach c [$tree children] {append content [$c render] \n}
    return $content
  }
  TreeRenderer instproc get_property {properties property {default ""}} {
    set value $default
    if {[dict exists $properties $property]} {
      set value [dict get $properties $property]
    }
    return $value
  }

  #
  # The renderers should provide the following methods as procs
  #
  #   - include_head_entries {args}
  #   - render {tree}
  #
  # (both are optional) and the following methods as instprocs
  #
  #   - render_node {{-open:boolean false} cat_content}
  #   - render_item {{-highlight:boolean false} item}
  #
  # The last two methods are required.  Below are the currently
  # defined tree renderers. We use as naming convention
  # TreeRenderer=<style>.

  #--------------------------------------------------------------------------------
  # List-specific renderer
  #
  # This is a very common render that maps the tree structure into an
  # unordered HTML list. The rendered is specialized by e.g. the
  # mktree a yuitree render below.
  #--------------------------------------------------------------------------------

  TreeRenderer create TreeRenderer=list
  TreeRenderer=list proc include_head_entries {args} {
    # In the general case, we have nothing to include.  More
    # specialized renders could provide their head entries.
  }
  TreeRenderer=list proc render {{-properties ""} tree} {
    set CSSclass [:get_property $properties CSSclass_ul \
                      [:get_property $properties CSSclass_top_ul]]
    set my_ul_class [expr {$CSSclass ne "" ? "class='$CSSclass' " : ""}]
    return "<ul ${my_ul_class}id='[$tree id]'>[next]</ul>"
  }

  TreeRenderer=list instproc render_item {{-highlight:boolean false} item} {
    $item instvar title href
    set prefix [$item set prefix]
    set suffix [$item set suffix]
    if {![$item exists encoded(prefix)]} {set prefix [::xowiki::Includelet html_encode $prefix]}
    if {![$item exists encoded(suffix)]} {set suffix [::xowiki::Includelet html_encode $suffix]}
    append entry \
        $prefix "<a href='[ns_quotehtml $href]'>" [::xowiki::Includelet html_encode $title"</a>" $suffix
    if {$highlight} {
      return "<li class='liItem'><b>$entry</b></li>\n"
    } else {
      return "<li class='liItem'>$entry</li>\n"
    }
  }
  TreeRenderer=list instproc render_node {{-open:boolean false} cat_content} {
    #:msg "[:label] [:expanded]"
    set cl [lindex [:info precedence] 0]
    set o_atts [lindex [$cl li_expanded_atts] [expr {${:expanded} ? 0 : 1}]]
    set h_atts [lindex [$cl highlight_atts] [expr {${:highlight} ? 0 : 1}]]
    set u_atts ""

    if {[info exists :li_id]}    {append o_atts " id='${:li_id}'"}
    if {[info exists :li_atts]}  {append o_atts ${:li_atts}"}
    if {[info exists :ul_id]}    {append u_atts " id='${:ul_id}'"}
    if {[info exists :ul_atts]}  {append u_atts ${:ul_atts}"}
    if {[info exists :ul_class]} {append u_atts " class='${:ul_class}'"}

    set label [::xowiki::Includelet html_encode [:label]]
    if {[info exists :count]} {
      set entry "$label <a href='[ns_quotehtml [:href]]'>([:count])</a>"
    } else {
      if {${:href} ne ""} {
        set entry "<a href='[ns_quotehtml [:href]]'>[ns_quotehtml $label]</a>"
      } else {
        set entry ${:label}
      }
    }
    if {$cat_content ne ""} {
      set content "\n<ul $u_atts>\n$cat_content</ul>"
      if {[$cl exists subtree_wrapper_class]} {
        set content "\n<div class='[$cl subtree_wrapper_class]'>$content</div>\n"
      }
    } else {
      set content ""
    }
    return "<li $o_atts><span $h_atts>${:prefix} $entry</span>$content</li>"
  }

  #--------------------------------------------------------------------------------
  # List-specific renderer based on mktree
  #--------------------------------------------------------------------------------
  TreeRenderer create TreeRenderer=mktree \
      -superclass TreeRenderer=list \
      -li_expanded_atts [list "class='liOpen'" "class='liClosed'"]

  TreeRenderer=mktree proc include_head_entries {args} {
    ::xo::Page requireCSS  "/resources/xowiki/cattree.css"
    #::xo::Page requireJS  "/resources/acs-templating/mktree.js"
    template::add_body_script -src "/resources/acs-templating/mktree.js"
  }
  TreeRenderer=mktree proc render {{-properties ""} tree} {
    return "<ul class='mktree' id='[$tree id]'>[next]</ul>"
  }

  #--------------------------------------------------------------------------------
  # List-specific renderer based for some menus
  #--------------------------------------------------------------------------------
  TreeRenderer create TreeRenderer=samplemenu \
      -superclass TreeRenderer=list \
      -li_expanded_atts [list "class='menu-open'" "class='menu-closed'"] \
      -subtree_wrapper_class "submenu"

  TreeRenderer=samplemenu proc include_head_entries {args} {
    # add your CSS here...
  }
  TreeRenderer=samplemenu proc render {{-properties ""} tree} {
    return "<ul class='menu' id='[$tree id]'>[next]</ul>"
  }

  #--------------------------------------------------------------------------------
  # List-specific renderer based for bootstrap3 horizontal menu
  #
  # This tree renderer does not support the fancy options like
  # counting, or attached items etc, but could be certainly extended
  # to do so, if there is some application need. Also highlighting
  # should be possible via the "open" parameter as in "list", but
  # depends on the generator of the tree.
  # --------------------------------------------------------------------------------
  TreeRenderer create TreeRenderer=bootstrap3horizontal \
      -superclass TreeRenderer=list

  TreeRenderer=bootstrap3horizontal instproc render_node {{-open:boolean false} cat_content} {
    set label [::xowiki::Includelet html_encode ${:label}]

    if {${:level} == 0} {
      #
      # Top level entry: build menu-button and wrapper for dropdown menu
      #
      append entry \
          "<a class='dropdown-toggle' data-toggle='dropdown' href='#'>" \
          [ns_quotehtml $label] \
          "</a>"
      set o_atts "class='dropdown'"
      set u_atts "class='dropdown-menu'"

    } else {
      #
      # Lower level entry: build dropdown menu entries
      #
      # Probably, entries on a deeper level than 1 should be ignored
      # or differently handled.
      #
      append entry \
          "<a href='[ns_quotehtml ${:href}]'>" \
          [ns_quotehtml $label] \
          </a>
      set o_atts ""
      set u_atts ""
    }
    if {$cat_content ne ""} {
      set content "\n<ul $u_atts>\n$cat_content</ul>"
    } else {
      set content ""
    }
    return "<li $o_atts>$entry $content"
  }

  TreeRenderer=bootstrap3horizontal proc render {{-properties ""} tree} {
    set name [$tree name]
    if {$name ne ""} {
      set navbarLabel [subst {
        <div class="navbar-header">
        <a class="navbar-brand" href="#">[ns_quotehtml $name]</a>
        </div>
      }]
    } else {
      set navbarLabel ""
    }

    return [subst {
      <nav class="navbar navbar-inverse">
      <div class="container-fluid">[ns_quotehtml $navbarLabel]
      <ul class="nav navbar-nav">
      [next]
      </ul>
      </div>
      </nav>
    }]
  }


  #--------------------------------------------------------------------------------
  # List-specific renderer based on yuitree
  #--------------------------------------------------------------------------------
  TreeRenderer create TreeRenderer=yuitree \
      -superclass TreeRenderer=list \
      -li_expanded_atts [list "class='expanded'" ""]

  TreeRenderer=yuitree proc include_head_entries {{-style ""} {-ajax:boolean 1} args} {
    ::xo::Page requireCSS urn:ad:css:yui2:fonts/fonts-min
    ::xo::Page requireCSS urn:ad:css:yui2:treeview/assets/skins/sam/treeview
    if {$style ne ""} {
      # yuitree default css style files are in the assets directory
      if {$style eq "yuitree"} {
        ::xo::Page requireCSS urn:ad:css:yui2:treeview/assets/tree
      } else {
        ::xo::Page requireCSS urn:ad:css:yui2:treeview/assets/$style/tree
      }
    }
    ::xo::Page requireJS urn:ad:js:yui2:yahoo/yahoo-min
    ::xo::Page requireJS urn:ad:js:yui2:yahoo-dom-event/yahoo-dom-event

    if {$ajax} {
      ::xo::Page requireJS urn:ad:js:yui2:event/event-min
      ::xo::Page requireJS urn:ad:js:yui2:connection/connection-min
      ::xo::Page requireJS urn:ad:js:yui2:animation/animation-min
    }
    ::xo::Page requireJS urn:ad:js:yui2:treeview/treeview-min
  }
  TreeRenderer=yuitree proc render {{-properties ""} tree} {
    return "<div id='[$tree id]'><ul>[next]</ul></div>"
  }


  #--------------------------------------------------------------------------------
  # list-specific render with drag and drop functionality
  #--------------------------------------------------------------------------------
  TreeRenderer create TreeRenderer=listdnd \
      -superclass TreeRenderer=list \
      -li_expanded_atts [list "" ""]

  TreeRenderer=listdnd proc include_head_entries {args} {
    ::xo::Page requireJS "/resources/xowiki/listdnd.js"
  }
  TreeRenderer=listdnd proc min_level {} {
    if {[dict exists ${:context} min_level]} {
      return [dict get ${:context} min_level]
    }
    return ""
  }
  TreeRenderer=listdnd proc add_handler {-id -event} {
    template::add_event_listener \
        -id $id \
        -event $event \
        -preventdefault=false \
        -script "listdnd_${event}_handler(event);"
  }

  TreeRenderer=listdnd proc render {{-properties ""} tree} {
    #:log "=== TreeRenderer=listdnd render $tree"
    #
    # Do we allow reorder on the top-level?
    #
    set id [$tree id]-topul
    if {[:min_level] == -1} {
      set css_class "page_order_region"
      foreach event {drop dragover dragleave} {
        :add_handler -id $id -event $event
      }
    } else {
      set css_class "page_order_region_no_target"
    }
    #:log "=== TreeRenderer=listdnd render $tree min_level <[:min_level]>"
    if {[$tree exists owner]} {
      #
      # assume, the "owner" is an includelet.
      set owner [$tree set owner]
      set page [$owner set __including_page]
      set package_url [::[$page package_id] package_url]
      set package_url_data " data-package_url='$package_url' data-folder_id='[$page parent_id]'"
    } else {
      set package_url_data ""
    }

    return [subst {<div id='[$tree id]' $package_url_data>
      <ul id='$id' class='$css_class'>[next]
      </ul></div>
    }]
  }
  TreeRenderer=listdnd instproc render_node {{-open:boolean false} cat_content} {
    #:log "=== TreeRenderer=listdnd render_node $cat_content"
    #set open_state [expr {${:open_requests} > 0 ?"class='liOpen'" : "class='liClosed'"}]
    ${:object} instvar page_order

    set :li_id [::xowiki::Includelet js_name [${:owner} set id]_$page_order]
    set :ul_id [::xowiki::Includelet js_name [${:owner} set id]__l${:level}_$page_order]

    set min_level [[self class] min_level]
    set reorder_child [expr {$min_level ne "" && ${:level} >= $min_level}]
    set reorder_self [expr {$min_level ne "" && ${:level} > $min_level}]
    #:log "=== render_node $page_order min_level $min_level level ${:level} reorder_child $reorder_child reorder_self $reorder_self"

    if {$reorder_child} {
      foreach event {drop dragover dragleave} {
        [self class] add_handler -id ${:ul_id} -event $event
      }
      set :ul_class "page_order_region"
    } else {
      set :ul_class "page_order_region_no_target"
    }
    if {$reorder_self} {
      set :li_atts [subst {data-value='$page_order' draggable='true'}]
      [self class] add_handler -id ${:li_id} -event dragstart
    }
    set :ul_id [::xowiki::Includelet js_name [${:owner} set id]__l${:level}_$page_order]

    return [next]
  }

  #--------------------------------------------------------------------------------
  # Tree renderer based on a section structure
  #--------------------------------------------------------------------------------
  TreeRenderer create TreeRenderer=sections \
      -superclass TreeRenderer=list
  TreeRenderer=sections instproc render_item {{-highlight:boolean false} item} {
    $item instvar title href
    set prefix [$item set prefix]
    set suffix [$item set suffix]
    if {![$item exists encoded(prefix)]} {set prefix [::xowiki::Includelet html_encode $prefix]}
    if {![$item exists encoded(suffix)]} {set suffix [::xowiki::Includelet html_encode $suffix]}
    append entry \
        $prefix "<a href='[ns_quotehtml $href]'>" [::xowiki::Includelet html_encode $title"</a>" $suffix
    if {$highlight} {
      return "<b>$entry</b><br>\n"
    } else {
      return "$entry<br>\n"
    }
  }
  TreeRenderer=sections instproc render_node {{-open:boolean false} cat_content} {
    set section [expr {${:level} + 2}]
    set label [::xowiki::Includelet html_encode ${:label}]
    return "<h$section>$label</h$section>\n<p>\
       <div style='margin-left: 2em; margin-right:0px;'>$cat_content</div>\n"
  }

  #--------------------------------------------------------------------------------
  # Bootstrap tree renderer based on
  # http://jonmiles.github.io/bootstrap-treeview/
  #--------------------------------------------------------------------------------

  TreeRenderer create TreeRenderer=bootstrap3
  TreeRenderer=bootstrap3 proc include_head_entries {args} {
    ::xo::Page requireJS urn:ad:js:jquery
    ::xo::Page requireCSS urn:ad:css:bootstrap3-treeview
    ::xo::Page requireJS  urn:ad:js:bootstrap3-treeview
    security::csp::require style-src cdnjs.cloudflare.com
    security::csp::require script-src cdnjs.cloudflare.com
  }

  TreeRenderer=bootstrap3 proc render {{-properties ""} tree} {
    set jsTree [string trimright [next", \n"]
    set id [$tree id]
    set options ""
    lappend options \
        "enableLinks: true"
    template::add_body_script -script "\n\$('#$id').treeview({data: \[$jsTree\], [join $options ,] });"
    return "<div id='$id'></div>"
  }
  TreeRenderer=bootstrap3 instproc render_href {href} {
    if {${:href} ne ""} {
      set jsHref "href: '[::xowiki::Includelet js_encode $href]',"
    } else {
      set jsHref ""
    }
    return $jsHref
  }
  TreeRenderer=bootstrap3 instproc render_item {{-highlight:boolean false} item} {
    :log "======UNTESTED============ highlight $highlight item $item"
    $item instvar title href prefix suffix
    set label  [::xowiki::Includelet js_encode "$prefix$title$suffix"]
    set jsHref [:render_href $href]
    set selected [expr {$highlight ? "true" : "false"}]
    return "\n{text: '$label', $jsHref state: {selected: $selected}},"
  }
  TreeRenderer=bootstrap3 instproc render_node {{-open:boolean false} cat_content} {
    if {${:verbose}} {:log "======bootstrap3==render_node========== ${:label}"}
    if {${:verbose}} {:log "open $open cat_content $cat_content"}

    if {[info exists :count]} {
      set jsTags "tags: \['${:count}'\],"
    } else {
      set jsTags ""
    }
    set jsHref [:render_href ${:href}]
    if {$cat_content ne ""} {
      set cat_content [string trimright $cat_content ", \n"]
      set content ", \nnodes: \[$cat_content\]\n"
    } else {
      set content ""
    }
    set label [::xowiki::Includelet js_encode ${:label}]
    set expanded [expr {${:expanded} ? "true" : "false"}]
    set selected [expr {${:highlight} ? "true" : "false"}]
    return "\n{text: '${label}', $jsTags $jsHref state: {expanded: $expanded, selected: $selected$content},"
  }

  #--------------------------------------------------------------------------------
  # Bootstrap3 tree renderer with folder structure
  #--------------------------------------------------------------------------------
  TreeRenderer create TreeRenderer=bootstrap3-folders -superclass TreeRenderer=bootstrap3
  TreeRenderer=bootstrap3-folders proc render {{-properties ""} tree} {
    set jsTree [string trimright [next", \n"]
    set id [$tree id]
    set options [list "enableLinks: true"]
    # see list of possible icons: https://github.com/jonmiles/bootstrap-treeview
    if {[::template::CSS toolkit] eq "bootstrap5"} {
      lappend options \
          "expandIcon: 'glyphicon glyphicon-none'" \
          "collapseIcon: 'bi bi-folder2-open'"
      #"expandIcon: 'bi bi-folder'"
    } else {
      lappend options \
          "expandIcon: 'glyphicon glyphicon-none'" \
          "collapseIcon: 'glyphicon glyphicon-folder-open'"
    }
    template::add_body_script -script "\n\$('#$id').treeview({data: \[$jsTree\], [join $options ,] });"
    return "<div id='$id'></div>"
  }

}
::xo::library source_dependent

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