diff --git a/javascript/tree/LICENSE b/javascript/tree/LICENSE new file mode 100644 index 000000000..e6bcfb1d9 --- /dev/null +++ b/javascript/tree/LICENSE @@ -0,0 +1,24 @@ +* Copyright (c) 2008, Silverstripe Ltd. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* * Neither the name of the nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY Silverstripe Ltd. ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL Silverstripe Ltd. BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/javascript/tree/README.md b/javascript/tree/README.md new file mode 100644 index 000000000..ecadea00e --- /dev/null +++ b/javascript/tree/README.md @@ -0,0 +1,104 @@ +# JavaScript Tree Control + +## Maintainers + + * Sam Minnee (sam at silverstripe dot com) + +## Features + + * Build trees using semantic HTML and unobtrusive JavaScript. + * Style the tree to suit your application you with CSS. + * Demo: http://www.silverstripe.org/assets/tree/demo.html + +## Usage + +The first thing to do is include the appropriate JavaScript and CSS files: + + + + + + +Then, create the HTML for you tree. This is basically a nested set of bullet pointed links. The "tree" class at the top is what the script will look for. Note that you can make a tree node closed to begin with by adding `class="closed"`. + +Here's the HTML code that I inserted to create the demo tree above. + + + + + +Your tree is now complete! + +## How it works + +Obviously, this isn't a complete detail of everything that's going on, but it gives you an insight into the overall process. + +### Starting the script + +In simple situations, creating an auto-loading script is a simple matter of setting window.onload to a function. But what if there's more than one script? To this end, we created an appendLoader() function that will execute multiple loader functions, including a previously defined loader function + +### Finding the tree content + +Rather than write a piece of script to define where your tree is, we've tried to make the script as automatic as possible - it finds all ULs with a class name containing "tree". + +### Augmenting the HTML + +Unfortunately, an LI containing an A isn't sufficient for doing all of the necessary tree styling. Rather than force people to put non-semantic HTML into their file, the script generates extra `` tags. + +So, the following HTML: + + +
  • + My item +
  • +
    + +Is turned into the more ungainly, and yet more easily styled: + + +
  • + + My item + +
  • +
    + +Additionally, some helper classes are applied to the `
  • ` and `` elements: + + * `"last"` is applied to the last node of any subtree. + * `"children"` is applied to any node that has children. + +### Styling it up + +Why the heck do we need 5 styling elements? Basically, because there are 5 background-images to apply: + + * li: A repeating vertical line is shown. Nested
  • tags give us the multiple vertical lines that we need. + * span.a: We overlay the vertical line with 'L' and 'T' elements as needed. + * span.b: We overlay '+' or '-' signs on nodes with children. + * span.c: This is needed to fix up the vertical line. + * a: Finally, we apply the page icon. + +### Opening / closing nodes + +Having come this far, the "dynamic" aspect of the tree control is very trivial. We set a "closed" class on the `
  • ` and `` elements, and our CSS takes care of hiding the children, changing the - to a + and changing the folder icon. \ No newline at end of file diff --git a/javascript/tree/images/i-bottom.gif b/javascript/tree/images/i-bottom.gif new file mode 100644 index 000000000..f07fa991d Binary files /dev/null and b/javascript/tree/images/i-bottom.gif differ diff --git a/javascript/tree/images/i-repeater.gif b/javascript/tree/images/i-repeater.gif new file mode 100644 index 000000000..d5ab0890b Binary files /dev/null and b/javascript/tree/images/i-repeater.gif differ diff --git a/javascript/tree/images/insertBetween.gif b/javascript/tree/images/insertBetween.gif new file mode 100644 index 000000000..bc375da30 Binary files /dev/null and b/javascript/tree/images/insertBetween.gif differ diff --git a/javascript/tree/images/l.gif b/javascript/tree/images/l.gif new file mode 100644 index 000000000..1e8c7079e Binary files /dev/null and b/javascript/tree/images/l.gif differ diff --git a/javascript/tree/images/minus.gif b/javascript/tree/images/minus.gif new file mode 100644 index 000000000..7a7fd3bb6 Binary files /dev/null and b/javascript/tree/images/minus.gif differ diff --git a/javascript/tree/images/page-closedfolder.gif b/javascript/tree/images/page-closedfolder.gif new file mode 100644 index 000000000..d26f2dc9a Binary files /dev/null and b/javascript/tree/images/page-closedfolder.gif differ diff --git a/javascript/tree/images/page-closedfolder.png b/javascript/tree/images/page-closedfolder.png new file mode 100644 index 000000000..d26f2dc9a Binary files /dev/null and b/javascript/tree/images/page-closedfolder.png differ diff --git a/javascript/tree/images/page-file.gif b/javascript/tree/images/page-file.gif new file mode 100644 index 000000000..d3bb119a1 Binary files /dev/null and b/javascript/tree/images/page-file.gif differ diff --git a/javascript/tree/images/page-file.png b/javascript/tree/images/page-file.png new file mode 100644 index 000000000..d3bb119a1 Binary files /dev/null and b/javascript/tree/images/page-file.png differ diff --git a/javascript/tree/images/page-openfolder.gif b/javascript/tree/images/page-openfolder.gif new file mode 100644 index 000000000..8d00c394b Binary files /dev/null and b/javascript/tree/images/page-openfolder.gif differ diff --git a/javascript/tree/images/page-openfolder.png b/javascript/tree/images/page-openfolder.png new file mode 100644 index 000000000..8d00c394b Binary files /dev/null and b/javascript/tree/images/page-openfolder.png differ diff --git a/javascript/tree/images/plus.gif b/javascript/tree/images/plus.gif new file mode 100644 index 000000000..3530f5957 Binary files /dev/null and b/javascript/tree/images/plus.gif differ diff --git a/javascript/tree/images/t.gif b/javascript/tree/images/t.gif new file mode 100644 index 000000000..c7d9f226f Binary files /dev/null and b/javascript/tree/images/t.gif differ diff --git a/javascript/tree/tree.css b/javascript/tree/tree.css new file mode 100644 index 000000000..b49b03a79 --- /dev/null +++ b/javascript/tree/tree.css @@ -0,0 +1,160 @@ +/* + * Default CSS for tree-view + */ +ul.tree{ + width: auto; + padding-left: 0; + margin-left: 0; +} + +ul.tree img { + border: none; +} + +ul.tree, ul.tree ul { + padding-left: 0; +} + +ul.tree ul { + margin-left: 16px; +} + +ul.tree li.closed ul { + display: none; +} + +ul.tree li { + list-style: none; + background: url(images/i-repeater.gif) repeat-y 1 0; + display: block; + width: auto; +} + ul.tree li.last { + list-style: none; + background-image: none; + } + +/* Span-A: I/L/I glpyhs */ +ul.tree span.a { + background: url(images/t.gif) no-repeat 0 50%; + display: block; +} +ul.tree .a.last { + background: url(images/l.gif) no-repeat 0 50%; +} + +/* Span-B: Plus/Minus icon */ +ul.tree span.b { +} +ul.tree span.a.children span.b { + display: inline-block; + background: url(images/minus.gif) no-repeat 0 50%; + cursor: pointer; +} +ul.tree li.closed span.a span.b, ul.tree span.a.unexpanded span.b { + display: inline-block; + background: url(images/plus.gif) no-repeat 0 50%; + cursor: pointer; +} + +/* Span-C: Spacing and extending tree line below the icon */ +ul.tree span.c { + margin-left: 16px; +} +ul.tree span.a.children span.c, ul.tree span.a.spanClosed span.c { + background: url(images/i-bottom.gif) no-repeat 0 50%; +} +ul.tree span.a.spanClosed span.c, ul.tree span.a.unexpanded span.c { + background-image: none; +} + +/* Anchor tag: Page icon */ +ul.tree span.c { + white-space: nowrap; +} +ul.tree a { + display: inline-block; /* IE needs this */ + white-space: pre; + overflow: hidden; + padding: 3px 0 1px 19px; + line-height: 16px; + background: url(images/page-file.png) no-repeat 0 50%; + background-position: 0 50% !important; + text-decoration: none; + outline: none; + font-size: 11px; +} + ul.tree a * { + font-size: 11px; + } + ul.tree a:hover { + text-decoration: underline; + } + +ul.tree span.a.children a { + background-image: url(images/page-openfolder.png); +} +ul.tree span.a.spanClosed a, ul.tree span.a.unexpanded a { + background-image: url(images/page-closedfolder.png); +} + +/* Unformatted tree */ +ul.tree.unformatted li { + background-image: none; + padding-left: 16px; +} +ul.tree.unformatted li li { + background-image: none; + padding-left: 0; +} + +/* + Hover / Link tags +*/ +ul.tree a:hover{ + text-decoration : none; +} + +/* + * Divs, by default store vertically aligned data + */ +/* As inside DIVs should be treated normally */ +ul.tree div a { + padding: 0; + background-image: none; + min-height: 0; + height: auto; +} + +ul.tree li a:link, +ul.tree li a:hover, +ul.tree li a:visited { + color: #111; +} + +/* + * Drag and drop styling + */ +ul.tree div.droppable { + float: none; + margin: -7px 0px -7px 16px; + height: 10px; + font-size: 1px; + z-index: 1000; +} +html > body ul.tree div.droppable { + margin: -5px 0px -5px 16px; +} + +ul.tree div.droppable.dragOver { + background: url(images/insertBetween.gif) no-repeat 50% 0; +} + +ul.tree a.dragOver, ul.tree li.dragOver a, ul.tree li.dragOver li.dragOver a { + border: 3px solid #0074C6; + margin: -3px; +} +ul.tree li.dragOver li a { + border-style: none; + margin: 0; +} \ No newline at end of file diff --git a/javascript/tree/tree.js b/javascript/tree/tree.js new file mode 100644 index 000000000..47cff9a31 --- /dev/null +++ b/javascript/tree/tree.js @@ -0,0 +1,957 @@ +/* + * Content-separated javascript tree widget + * + * Usage: + * behaveAs(someUL, Tree) + * OR behaveAs(someUL, DraggableTree) + * + * Extended by Steven J. DeRose, deroses@mail.nih.gov, sderose@acm.org. + * + * INPUT REQUIREMENTS: + * Put class="tree" on topmost UL(s). + * Can put class="closed" on LIs to have them collapsed on startup. + * + * The structure we build is: + * li class="children last closed" <=== original li from source + * children: there's a UL child + * last: no following LI + * closed: is collapsed (may be in src) + * span class="a children spanClosed" <=== contains children before UL + * children: there's a UL (now a sib) + * spanClosed: is collapsed + * span class="b" <=== +/- click is caught here + * span class="c" <=== for spacing and lines + * a href="..." <=== original pre-UL stuff (e.g., a) + * ul... + * + */ + +Function.prototype.create = function(item, arg1, arg2, arg3, arg4, arg5, arg6) { + return behaveAs(item, this, arg1, arg2, arg3, arg4, arg5, arg6); +} + +Tree = Class.create(); +Tree.prototype = { + /* + * Initialise a tree node, converting all its LIs appropriately. + * This means go through all li children, and move the content of each + * (before any UL child) down into 3 intermediate spans, classes a/b/c. + */ + initialize: function(options) { + this.isDraggable = false; + var i,li; + + this.options = options ? options : {}; + if(!this.options.tree) this.options.tree = this; + + this.tree = this.options.tree; + + // Set up observer + if(this == this.tree) Observable.applyTo(this); + + // Find all LIs to process + // Don't let it re-do a node it's already done. + for(i=0;i tag into a suitable tree node + */ + castAsTreeNode: function(li) { + behaveAs(li, TreeNode, this.options); + }, + + getIdxOf : function(el) { + if(!el.treeNode) el.treeNode = el; + // Special case for TreeMultiselectField + if(el.treeNode.id.match(/^selector-([^-]+)-([0-9]+)$/)) return RegExp.$2; + // Other case for LHS tree of CMS + if(el.treeNode.id.match(/([^-]+)-(.+)$/)) return RegExp.$1; + else return el.treeNode.id; + }, + + childTreeNodes: function() { + var i,item, children = []; + for(i=0;item=this.childNodes[i];i++) { + if(item.tagName && item.tagName.toLowerCase() == 'li') children.push(item); + } + return children; + }, + hasChildren: function() { + return this.childTreeNodes().length > 0; + }, + + /** + * Turn a normal tree into a draggable one. + */ + makeDraggable: function() { + this.isDraggable = true; + var i,item,x; + + var trees = this.getElementsByTagName('ul'); + for(x in DraggableTree.prototype) this[x] = DraggableTree.prototype[x]; + DraggableTree.prototype.setUpDragability.apply(this); + + var nodes = this.getElementsByTagName('li'); + for(i=0;item=nodes[i];i++) { + for(x in DraggableTreeNode.prototype) item[x] = DraggableTreeNode.prototype[x]; + } + for(i=0;item=trees[i];i++) { + for(x in DraggableTree.prototype) item[x] = DraggableTree.prototype[x]; + } + for(i=0;item=nodes[i];i++) { + DraggableTreeNode.prototype.setUpDragability.apply(item); + } + for(i=0;item=trees[i];i++) { + DraggableTree.prototype.setUpDragability.apply(item); + } + }, + + /** + * Add the given child node to this tree node. + * If 'before' is specified, then it will be inserted before that. + */ + appendTreeNode : function(child, before) { + // Remove from the old parent node - this will ensure that the classes of the old tree + // item are updated accordingly + if(child.parentTreeNode) { + var oldParent = child.parentTreeNode; + oldParent.removeTreeNode(child); + } + var lastNode, i, holder = this; + if(lastNode = this.lastTreeNode()) lastNode.removeNodeClass('last'); + + // Do the actual moving + if(before) { + child.removeNodeClass('last'); + + if(holder != before.parentNode) { + throw("TreeNode.appendTreeNode: 'before' not contained within the holder"); + holder.appendChild(child); + } else { + holder.insertBefore(child, before); + } + } else { + holder.appendChild(child); + } + + if(this.parentNode && this.parentNode.fixDragHelperDivs) this.parentNode.fixDragHelperDivs(); + if(oldParent && oldParent.fixDragHelperDivs) oldParent.fixDragHelperDivs(); + + // Update the helper classes + if(this.parentNode && this.parentNode.tagName.toLowerCase() == 'li') { + if(this.parentNode.className.indexOf('closed') == -1) this.parentNode.addNodeClass('children'); + this.lastTreeNode().addNodeClass('last'); + } + + // Update the helper variables + if(this.parentNode.tagName.toLowerCase() == 'li') child.parentTreeNode = this.parentNode; + else child.parentTreeNode = null; + + if(this.isDraggable) { + for(x in DraggableTreeNode.prototype) child[x] = DraggableTreeNode.prototype[x]; + DraggableTreeNode.prototype.setUpDragability.apply(child); + } + }, + + lastTreeNode : function() { + var i, holder = this; + for(i=holder.childNodes.length-1;i>=0;i--) { + if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == 'li') return holder.childNodes[i]; + } + }, + + /** + * Remove the given child node from this tree node. + */ + removeTreeNode : function(child) { + // Remove the child + var holder = this; + try { holder.removeChild(child); } catch(er) { } + + // Look for remaining children + var i, hasChildren = false; + for(i=0;i would give a recordID of 6 + if(this.id && this.id.match(/([^-]+)-(.+)$/)) + this.recordID = RegExp.$1; + + // Create our extra spans + spanA = document.createElement('span'); + spanB = document.createElement('span'); + spanC = document.createElement('span'); + spanA.appendChild(spanB); + spanB.appendChild(spanC); + spanA.className = 'a ' + li.className.replace('closed','spanClosed'); + spanB.className = 'b'; + spanB.onclick = TreeNode_bSpan_onclick; + spanC.className = 'c'; + + this.castAsSpanA(spanA); + + // Add +/- icon to select node that has children + if (li.hasChildren() && li.className.indexOf('current') > -1) { + li.className = li.className + ' children'; + spanA.className = spanA.className + ' children'; + } + + // Find the UL within the LI, if it exists + stoppingPoint = li.childNodes.length; + startingPoint = 0; + childUL = null; + for(j=0;j startingPoint) li.insertBefore(spanA, li.childNodes[startingPoint]); + else li.appendChild(spanA); + + // Create appropriate node references; + if(li.parentNode && li.parentNode.parentNode && li.parentNode.parentNode.tagName.toLowerCase() == 'li') { + li.parentTreeNode = li.parentNode.parentNode; + } + li.aSpan = spanA; + li.bSpan = spanB; + li.cSpan = spanC; + li.treeNode = spanA.treeNode = spanB.treeNode = spanC.treeNode = li; + var aTag = spanC.getElementsByTagName('a')[0]; + if(aTag) { + aTag.treeNode = li; + li.aTag = aTag; + + } else { + throw("Tree creation: A tree needs tags inside the
  • s to work properly."); + } + + + aTag.onclick = TreeNode_aTag_onclick.bindAsEventListener(aTag); + + + // Process the children + if(childUL != null) { + if(this.castAsTree(childUL)) { /* ***** RECURSE ***** */ + if(this.className.indexOf('closed') == -1) { + this.addNodeClass('children'); + } + } + } else { + this.removeNodeClass('closed'); + } + + this.setIconByClass(); + }, + + destroy: function() { + // Debug.show(this); + + this.tree = null; + this.treeNode = null; + this.parentTreeNode = null; + + if(this.options) this.options.tree = null; + this.options = null; + + if(this.aTag) { + this.aTag.treeNode = null; + this.aTag.onclick = null; + } + if(this.aSpan) { + this.aSpan.treeNode = null; + this.aSpan.onmouseover = null; + this.aSpan.onmouseout = null; + } + if(this.bSpan) { + this.bSpan.treeNode = null; + this.bSpan.onclick = null; + } + if(this.cSpan) this.cSpan.treeNode = null; + + this.aSpan = null; + this.bSpan = null; + this.cSpan = null; + this.aTag = null; + }, + + /** + * Cast the given span as the item for this tree. + */ + castAsSpanA: function(spanA) { + var x; + for(x in TreeNode_SpanA) spanA[x] = TreeNode_SpanA[x]; + }, + /** + * Cast the child