]> git.mjollnir.org Git - s9y.git/commitdiff
Moved drag+drop and YahooUI library into templates/default/ to be used in shared...
authorgarvinhicking <garvinhicking>
Mon, 22 Jan 2007 12:11:49 +0000 (12:11 +0000)
committergarvinhicking <garvinhicking>
Mon, 22 Jan 2007 12:11:49 +0000 (12:11 +0000)
docs/NEWS
include/admin/plugins.inc.php
templates/default/YahooUI/treeview/YAHOO.js [new file with mode: 0644]
templates/default/YahooUI/treeview/license.txt [new file with mode: 0644]
templates/default/YahooUI/treeview/treeview.js [new file with mode: 0644]
templates/default/admin/media_choose.tpl
templates/default/dragdrop.js [new file with mode: 0644]
templates/default/imgedit.js [new file with mode: 0644]

index 6b64436f275fd1973afc46a2a472fa07dcd449c4..0efadfdcfa6dd89c9737773b5e8c73aca207f04c 100644 (file)
--- a/docs/NEWS
+++ b/docs/NEWS
@@ -3,6 +3,9 @@
 Version 1.2 ()
 ------------------------------------------------------------------------
 
+    * Moved drag+drop and YahooUI library into templates/default/ to
+      be used in shared installation environments (garvinhicking)
+
     * Improve WP importer by only fetching real posts (attachments/static
       only optional) and splitting a post into extended/normal entry.
       Thanks to jtb!
index beab2c8d36e57f8fa259fc39e346d2f037737fa7..baa669f5f0f3a20dc7c577a1d456c9f2bf537306 100644 (file)
@@ -472,7 +472,7 @@ if (isset($_GET['serendipity']['plugin_to_conf'])) {
     <div><?php echo BELOW_IS_A_LIST_OF_INSTALLED_PLUGINS ?></div>
 <?php
     if (!isset($serendipity['eyecandy']) || serendipity_db_bool($serendipity['eyecandy'])) {
-        echo '<script src="bundled-libs/dragdrop.js" type="text/javascript"></script>';
+        echo '<script src="' . serendipity_getTemplateFile('dragdrop.js') . '" type="text/javascript"></script>';
         echo '<div class="warning js_warning"><em>' . PREFERENCE_USE_JS_WARNING . '</em></div>';
     }
 
diff --git a/templates/default/YahooUI/treeview/YAHOO.js b/templates/default/YahooUI/treeview/YAHOO.js
new file mode 100644 (file)
index 0000000..33e3ff4
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+Copyright (c) 2006 Yahoo! Inc. All rights reserved.
+version 0.9.0
+*/
+
+/**
+ * @class The Yahoo global namespace
+ */
+var YAHOO = function() {
+
+    return {
+
+        /**
+         * Yahoo presentation platform utils namespace
+         */
+        util: {},
+
+        /**
+         * Yahoo presentation platform widgets namespace
+         */
+        widget: {},
+
+        /**
+         * Yahoo presentation platform examples namespace
+         */
+        example: {},
+
+        /**
+         * Returns the namespace specified and creates it if it doesn't exist
+         *
+         * YAHOO.namespace("property.package");
+         * YAHOO.namespace("YAHOO.property.package");
+         *
+         * Either of the above would create YAHOO.property, then
+         * YAHOO.property.package
+         *
+         * @param  {String} sNameSpace String representation of the desired
+         *                             namespace
+         * @return {Object}            A reference to the namespace object
+         */
+        namespace: function( sNameSpace ) {
+
+            if (!sNameSpace || !sNameSpace.length) {
+                return null;
+            }
+
+            var levels = sNameSpace.split(".");
+
+            var currentNS = YAHOO;
+
+            // YAHOO is implied, so it is ignored if it is included
+            for (var i=(levels[0] == "YAHOO") ? 1 : 0; i<levels.length; ++i) {
+                currentNS[levels[i]] = currentNS[levels[i]] || {};
+                currentNS = currentNS[levels[i]];
+            }
+
+            return currentNS;
+
+        }
+    };
+
+} ();
+
diff --git a/templates/default/YahooUI/treeview/license.txt b/templates/default/YahooUI/treeview/license.txt
new file mode 100644 (file)
index 0000000..03ff644
--- /dev/null
@@ -0,0 +1,35 @@
+Software License Agreement (BSD License)\r
+\r
+Copyright (c) 2006, Yahoo! Inc.\r
+All rights reserved.\r
+\r
+Redistribution and use of this software in source and binary forms, with \r
+or without modification, are permitted provided that the following \r
+conditions are met:\r
+\r
+* Redistributions of source code must retain the above\r
+  copyright notice, this list of conditions and the\r
+  following disclaimer.\r
+\r
+* Redistributions in binary form must reproduce the above\r
+  copyright notice, this list of conditions and the\r
+  following disclaimer in the documentation and/or other\r
+  materials provided with the distribution.\r
+\r
+* Neither the name of Yahoo! Inc. nor the names of its\r
+  contributors may be used to endorse or promote products\r
+  derived from this software without specific prior\r
+  written permission of Yahoo! Inc.\r
+\r
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS \r
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED \r
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A \r
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER \r
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, \r
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, \r
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR \r
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF \r
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING \r
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS \r
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+\r
diff --git a/templates/default/YahooUI/treeview/treeview.js b/templates/default/YahooUI/treeview/treeview.js
new file mode 100644 (file)
index 0000000..d80b9ab
--- /dev/null
@@ -0,0 +1,1490 @@
+/*
+Copyright (c) 2006 Yahoo! Inc. All rights reserved.
+version 0.9.0
+*/
+
+/**
+ * @class Contains the tree view metadata and the root node.  This is an
+ * ordered tree; child nodes will be displayed in the order created, and
+ * there currently is no way to change this.
+ *
+ * @constructor
+ * @todo prune, graft, reload, repaint
+ * @param {string} id The id of the element that the tree will be inserted
+ * into.
+ */
+YAHOO.widget.TreeView = function(id) {
+       if (id) { this.init(id); }
+};
+
+YAHOO.widget.TreeView.prototype = {
+
+    /**
+     * The id of tree container element
+     *
+     * @type String
+     */
+    id: null,
+
+    /**
+     * Flat collection of all nodes in this tree
+     *
+     * @type YAHOO.widget.Node[]
+     * @private
+     */
+    _nodes: null,
+
+    /**
+     * We lock the tree control while waiting for the dynamic loader to return
+     *
+     * @type boolean
+     */
+    locked: false,
+
+    /**
+     * The animation to use for expanding children, if any
+     *
+     * @type string
+     * @private
+     */
+    _expandAnim: null,
+
+    /**
+     * The animation to use for collapsing children, if any
+     *
+     * @type string
+     * @private
+     */
+    _collapseAnim: null,
+
+    /**
+     * The current number of animations that are executing
+     *
+     * @type int
+     * @private
+     */
+    _animCount: 0,
+
+    /**
+     * The maximum number of animations to run at one time.
+     *
+     * @type int
+     */
+    _maxAnim: 2,
+
+    /**
+     * Sets up the animation for expanding children
+     *
+     * @param {string} the type of animation (acceptable constants in YAHOO.widget.TVAnim)
+     */
+    setExpandAnim: function(type) {
+        if (YAHOO.widget.TVAnim.isValid(type)) {
+            this._expandAnim = type;
+        }
+    },
+
+    /**
+     * Sets up the animation for collapsing children
+     *
+     * @param {string} the type of animation (acceptable constants in YAHOO.widget.TVAnim)
+     */
+    setCollapseAnim: function(type) {
+        if (YAHOO.widget.TVAnim.isValid(type)) {
+            this._collapseAnim = type;
+        }
+    },
+
+    /**
+     * Perform the expand animation if configured, or just show the
+     * element if not configured or too many animations are in progress
+     *
+     * @param el {HTMLElement} the element to animate
+     * @return {boolean} true if animation could be invoked, false otherwise
+     */
+    animateExpand: function(el) {
+
+        if (this._expandAnim && this._animCount < this._maxAnim) {
+            // this.locked = true;
+            var tree = this;
+            var a = YAHOO.widget.TVAnim.getAnim(this._expandAnim, el,
+                            function() { tree.expandComplete(); });
+            if (a) {
+                ++this._animCount;
+                a.animate();
+            }
+
+            return true;
+        }
+
+        return false;
+    },
+
+    /**
+     * Perform the collapse animation if configured, or just show the
+     * element if not configured or too many animations are in progress
+     *
+     * @param el {HTMLElement} the element to animate
+     * @return {boolean} true if animation could be invoked, false otherwise
+     */
+    animateCollapse: function(el) {
+
+        if (this._collapseAnim && this._animCount < this._maxAnim) {
+            // this.locked = true;
+            var tree = this;
+            var a = YAHOO.widget.TVAnim.getAnim(this._collapseAnim, el,
+                            function() { tree.collapseComplete(); });
+            if (a) {
+                ++this._animCount;
+                a.animate();
+            }
+
+            return true;
+        }
+
+        return false;
+    },
+
+    /**
+     * Function executed when the expand animation completes
+     */
+    expandComplete: function() {
+        --this._animCount;
+        // this.locked = false;
+    },
+
+    /**
+     * Function executed when the collapse animation completes
+     */
+    collapseComplete: function() {
+        --this._animCount;
+        // this.locked = false;
+    },
+
+    /**
+     * Initializes the tree
+     *
+     * @parm {string} id the id of the element that will hold the tree
+     * @private
+     */
+    init: function(id) {
+
+        this.id = id;
+        this._nodes = new Array();
+
+        // store a global reference
+        YAHOO.widget.TreeView.trees[id] = this;
+
+        // Set up the root node
+        this.root = new YAHOO.widget.RootNode(this);
+
+
+    },
+
+    /**
+     * Renders the tree boilerplate and visible nodes
+     */
+    draw: function() {
+        var html = this.root.getHtml();
+        document.getElementById(this.id).innerHTML = html;
+        this.firstDraw = false;
+    },
+
+    /**
+     * Nodes register themselves with the tree instance when they are created.
+     *
+     * @param node {YAHOO.widget.Node} the node to register
+     * @private
+     */
+    regNode: function(node) {
+        this._nodes[node.index] = node;
+    },
+
+    /**
+     * Returns the root node of this tree
+     *
+     * @return {YAHOO.widget.Node} the root node
+     */
+    getRoot: function() {
+        return this.root;
+    },
+
+    /**
+     * Configures this tree to dynamically load all child data
+     *
+     * @param {function} fnDataLoader the function that will be called to get the data
+     */
+    setDynamicLoad: function(fnDataLoader) {
+        // this.root.dataLoader = fnDataLoader;
+        // this.root._dynLoad = true;
+        this.root.setDynamicLoad(fnDataLoader);
+    },
+
+    /**
+     * Expands all child nodes.  Note: this conflicts with the "multiExpand"
+     * node property.  If expand all is called in a tree with nodes that
+     * do not allow multiple siblings to be displayed, only the last sibling
+     * will be expanded.
+     */
+    expandAll: function() {
+        if (!this.locked) {
+            this.root.expandAll();
+        }
+    },
+
+    /**
+     * Collapses all expanded child nodes in the entire tree.
+     */
+    collapseAll: function() {
+        if (!this.locked) {
+            this.root.collapseAll();
+        }
+    },
+
+    /**
+     * Returns a node in the tree that has the specified index (this index
+     * is created internally, so this function probably will only be used
+     * in html generated for a given node.)
+     *
+     * @param {int} nodeIndex the index of the node wanted
+     * @return {YAHOO.widget.Node} the node with index=nodeIndex, null if no match
+     */
+    getNodeByIndex: function(nodeIndex) {
+        var n = this._nodes[nodeIndex];
+        return (n) ? n : null;
+    },
+
+    /**
+     * Returns a node that has a matching property and value in the data
+     * object that was passed into its constructor.  Provides a flexible
+     * way for the implementer to get a particular node.
+     *
+     * @param {object} property the property to search (usually a string)
+     * @param {object} value the value we want to find (usuall an int or string)
+     * @return {YAHOO.widget.Node} the matching node, null if no match
+     */
+    getNodeByProperty: function(property, value) {
+        for (var i in this._nodes) {
+            var n = this._nodes[i];
+            if (n.data && value == n.data[property]) {
+                return n;
+            }
+        }
+
+        return null;
+    },
+
+    /**
+     * Abstract method that is executed when a node is expanded
+     *
+     * @param node {YAHOO.widget.Node} the node that was expanded
+     */
+    onExpand: function(node) { },
+
+    /**
+     * Abstract method that is executed when a node is collapsed
+     *
+     * @param node {YAHOO.widget.Node} the node that was collapsed.
+     */
+    onCollapse: function(node) { }
+
+};
+
+/**
+ * Global cache of tree instances
+ *
+ * @type Array
+ * @private
+ */
+YAHOO.widget.TreeView.trees = [];
+
+/**
+ * Global method for getting a tree by its id.  Used in the generated
+ * tree html.
+ *
+ * @param treeId {String} the id of the tree instance
+ * @return {TreeView} the tree instance requested, null if not found.
+ */
+YAHOO.widget.TreeView.getTree = function(treeId) {
+       var t = YAHOO.widget.TreeView.trees[treeId];
+       return (t) ? t : null;
+};
+
+YAHOO.widget.TreeView.nodeCount = 0;
+
+/**
+ * Global method for getting a node by its id.  Used in the generated
+ * tree html.
+ *
+ * @param treeId {String} the id of the tree instance
+ * @param nodeIndex {String} the index of the node to return
+ * @param return {YAHOO.widget.Node} the node instance requested, null if not found
+ */
+YAHOO.widget.TreeView.getNode = function(treeId, nodeIndex) {
+       var t = YAHOO.widget.TreeView.getTree(treeId);
+       return (t) ? t.getNodeByIndex(nodeIndex) : null;
+};
+
+/**
+ * Adds an event.  Replace with event manager when available
+ *
+ * @param el the elment to bind the handler to
+ * @param {string} sType the type of event handler
+ * @param {function} fn the callback to invoke
+ * @param {boolean} capture if true event is capture phase, bubble otherwise
+ */
+YAHOO.widget.TreeView.addHandler = function (el, sType, fn, capture) {
+       capture = (capture) ? true : false;
+       if (el.addEventListener) {
+               el.addEventListener(sType, fn, capture);
+       } else if (el.attachEvent) {
+               el.attachEvent("on" + sType, fn);
+       } else {
+               el["on" + sType] = fn;
+       }
+};
+
+/**
+ * Attempts to preload the images defined in the styles used to draw the tree by
+ * rendering off-screen elements that use the styles.
+ */
+YAHOO.widget.TreeView.preload = function() {
+
+       var styles = [
+               "ygtvtn",
+               "ygtvtm",
+               "ygtvtmh",
+               "ygtvtp",
+               "ygtvtph",
+               "ygtvln",
+               "ygtvlm",
+               "ygtvlmh",
+               "ygtvlp",
+               "ygtvlph",
+               "ygtvloading"
+               ];
+
+       var sb = [];
+
+       for (var i = 0; i < styles.length; ++i) {
+               sb[sb.length] = '<span class="' + styles[i] + '">&nbsp;</span>';
+       }
+
+       var f = document.createElement("div");
+       var s = f.style;
+       s.position = "absolute";
+       s.top = "-1000px";
+       s.left = "-1000px";
+       f.innerHTML = sb.join("");
+
+       document.body.appendChild(f);
+};
+
+YAHOO.widget.TreeView.addHandler(window,
+                "load", YAHOO.widget.TreeView.preload);
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * @class Abstract node class
+ * @constructor
+ * @param oData {object} a string or object containing the data that will
+ * be used to render this node
+ * @param oParent {YAHOO.widget.Node} this node's parent node
+ * @param expanded {boolean} the initial expanded/collapsed state
+ */
+YAHOO.widget.Node = function(oData, oParent, expanded) {
+       if (oParent) { this.init(oData, oParent, expanded); }
+};
+
+YAHOO.widget.Node.prototype = {
+
+    /**
+     * The index for this instance obtained from global counter in YAHOO.widget.TreeView.
+     *
+     * @type int
+     */
+    index: 0,
+
+    /**
+     * This node's child node collection.
+     *
+     * @type YAHOO.widget.Node[]
+     */
+    children: null,
+
+    /**
+     * Tree instance this node is part of
+     *
+     * @type YAHOO.widget.TreeView
+     */
+    tree: null,
+
+    /**
+     * The data linked to this node.  This can be any object or primitive
+     * value, and the data can be used in getNodeHtml().
+     *
+     * @type object
+     */
+    data: null,
+
+    /**
+     * Parent node
+     *
+     * @type YAHOO.widget.Node
+     */
+    parent: null,
+
+    /**
+     * The depth of this node.  We start at -1 for the root node.
+     *
+     * @type int
+     */
+    depth: -1,
+
+    /**
+     * The href for the node's label.  If one is not specified, the href will
+     * be set so that it toggles the node.
+     *
+     * @type string
+     */
+    href: null,
+
+    /**
+     * The label href target, defaults to current window
+     *
+     * @type string
+     */
+    target: "_self",
+
+    /**
+     * The node's expanded/collapsed state
+     *
+     * @type boolean
+     */
+    expanded: false,
+
+    /**
+     * Can multiple children be expanded at once?
+     *
+     * @type boolean
+     */
+    multiExpand: true,
+
+    /**
+     * Should we render children for a collapsed node?  It is possible that the
+     * implementer will want to render the hidden data...  @todo verify that we
+     * need this, and implement it if we do.
+     *
+     * @type boolean
+     */
+    renderHidden: false,
+
+    /**
+     * Flag that is set to true the first time this node's children are rendered.
+     *
+     * @type boolean
+     */
+    childrenRendered: false,
+
+    /**
+     * This node's previous sibling
+     *
+     * @type YAHOO.widget.Node
+     */
+    previousSibling: null,
+
+    /**
+     * This node's next sibling
+     *
+     * @type YAHOO.widget.Node
+     */
+    nextSibling: null,
+
+    /**
+     * We can set the node up to call an external method to get the child
+     * data dynamically.
+     *
+     * @type boolean
+     * @private
+     */
+    _dynLoad: false,
+
+    /**
+     * Function to execute when we need to get this node's child data.
+     *
+     * @type function
+     */
+    dataLoader: null,
+
+    /**
+     * This is true for dynamically loading nodes while waiting for the
+     * callback to return.
+     *
+     * @type boolean
+     */
+    isLoading: false,
+
+    /**
+     * The toggle/branch icon will not show if this is set to false.  This
+     * could be useful if the implementer wants to have the child contain
+     * extra info about the parent, rather than an actual node.
+     *
+     * @type boolean
+     */
+    hasIcon: true,
+
+    /**
+     * Initializes this node, gets some of the properties from the parent
+     *
+     * @param oData {object} a string or object containing the data that will
+     * be used to render this node
+     * @param oParent {YAHOO.widget.Node} this node's parent node
+     * @param expanded {boolean} the initial expanded/collapsed state
+     */
+    init: function(oData, oParent, expanded) {
+        this.data              = oData;
+        this.children  = [];
+        this.index             = YAHOO.widget.TreeView.nodeCount;
+        ++YAHOO.widget.TreeView.nodeCount;
+        this.expanded  = expanded;
+
+        // oParent should never be null except when we create the root node.
+        if (oParent) {
+            this.tree                  = oParent.tree;
+            this.parent                        = oParent;
+            this.href                  = "javascript:" + this.getToggleLink();
+            this.depth                 = oParent.depth + 1;
+            this.multiExpand   = oParent.multiExpand;
+
+            oParent.appendChild(this);
+        }
+    },
+
+    /**
+     * Appends a node to the child collection.
+     *
+     * @param node {YAHOO.widget.Node} the new node
+     * @return {YAHOO.widget.Node} the child node
+     * @private
+     */
+    appendChild: function(node) {
+        if (this.hasChildren()) {
+            var sib = this.children[this.children.length - 1];
+            sib.nextSibling = node;
+            node.previousSibling = sib;
+        }
+
+        this.tree.regNode(node);
+        this.children[this.children.length] = node;
+        return node;
+
+    },
+
+    /**
+     * Returns a node array of this node's siblings, null if none.
+     *
+     * @return YAHOO.widget.Node[]
+     */
+    getSiblings: function() {
+        return this.parent.children;
+    },
+
+    /**
+     * Shows this node's children
+     */
+    showChildren: function() {
+        if (!this.tree.animateExpand(this.getChildrenEl())) {
+            if (this.hasChildren()) {
+                this.getChildrenEl().style.display = "";
+            }
+        }
+    },
+
+    /**
+     * Hides this node's children
+     */
+    hideChildren: function() {
+
+        if (!this.tree.animateCollapse(this.getChildrenEl())) {
+            this.getChildrenEl().style.display = "none";
+        }
+    },
+
+    /**
+     * Returns the id for this node's container div
+     *
+     * @return {string} the element id
+     */
+    getElId: function() {
+        return "ygtv" + this.index;
+    },
+
+    /**
+     * Returns the id for this node's children div
+     *
+     * @return {string} the element id for this node's children div
+     */
+    getChildrenElId: function() {
+        return "ygtvc" + this.index;
+    },
+
+    /**
+     * Returns the id for this node's toggle element
+     *
+     * @return {string} the toggel element id
+     */
+    getToggleElId: function() {
+        return "ygtvt" + this.index;
+    },
+
+    /**
+     * Returns this node's container html element
+     *
+     * @return {Object} the container html element
+     */
+    getEl: function() {
+        return document.getElementById(this.getElId());
+    },
+
+    /**
+     * Returns the div that was generated for this node's children
+     *
+     * @return {Object} this node's children div
+     */
+    getChildrenEl: function() {
+        return document.getElementById(this.getChildrenElId());
+    },
+
+    /**
+     * Returns the element that is being used for this node's toggle.
+     *
+     * @return {Object} this node's toggel html element
+     */
+    getToggleEl: function() {
+        return document.getElementById(this.getToggleElId());
+    },
+
+    /**
+     * Generates the link that will invoke this node's toggle method
+     *
+     * @return {string} the javascript url for toggling this node
+     */
+    getToggleLink: function() {
+        return "YAHOO.widget.TreeView.getNode(\'" + this.tree.id + "\'," +
+            this.index + ").toggle()";
+    },
+
+    /**
+     * Hides this nodes children (creating them if necessary), changes the
+     * toggle style.
+     */
+    collapse: function() {
+        // Only collapse if currently expanded
+        if (!this.expanded) { return; }
+
+        if (!this.getEl()) {
+            this.expanded = false;
+            return;
+        }
+
+        // hide the child div
+        this.hideChildren();
+        this.expanded = false;
+
+        if (this.hasIcon) {
+            this.getToggleEl().className = this.getStyle();
+        }
+
+        // fire the collapse event handler
+        this.tree.onCollapse(this);
+    },
+
+    /**
+     * Shows this nodes children (creating them if necessary), changes the
+     * toggle style, and collapses its siblings if multiExpand is not set.
+     */
+    expand: function() {
+        // Only expand if currently collapsed.
+        if (this.expanded) { return; }
+
+        if (!this.getEl()) {
+            this.expanded = true;
+            return;
+        }
+
+        if (! this.childrenRendered) {
+            this.getChildrenEl().innerHTML = this.renderChildren();
+        }
+
+        this.expanded = true;
+        if (this.hasIcon) {
+            this.getToggleEl().className = this.getStyle();
+        }
+
+        // We do an extra check for children here because the lazy
+        // load feature can expose nodes that have no children.
+
+        // if (!this.hasChildren()) {
+        if (this.isLoading) {
+            this.expanded = false;
+            return;
+        }
+
+        if (! this.multiExpand) {
+            var sibs = this.getSiblings();
+            for (var i=0; i<sibs.length; ++i) {
+                if (sibs[i] != this && sibs[i].expanded) {
+                    sibs[i].collapse();
+                }
+            }
+        }
+
+        this.showChildren();
+
+        // fire the expand event handler
+        this.tree.onExpand(this);
+    },
+
+    /**
+     * Returns the css style name for the toggle
+     *
+     * @return {string} the css class for this node's toggle
+     */
+    getStyle: function() {
+        if (this.isLoading) {
+            return "ygtvloading";
+        } else {
+            // location top or bottom, middle nodes also get the top style
+            var loc = (this.nextSibling) ? "t" : "l";
+
+            // type p=plus(expand), m=minus(collapase), n=none(no children)
+            var type = "n";
+            if (this.hasChildren(true) || this.isDynamic()) {
+                type = (this.expanded) ? "m" : "p";
+            }
+
+            return "ygtv" + loc + type;
+        }
+    },
+
+    /**
+     * Returns the hover style for the icon
+     * @return {string} the css class hover state
+     */
+    getHoverStyle: function() {
+        var s = this.getStyle();
+        if (this.hasChildren(true) && !this.isLoading) {
+            s += "h";
+        }
+        return s;
+    },
+
+    /**
+     * Recursively expands all of this node's children.
+     */
+    expandAll: function() {
+        for (var i=0;i<this.children.length;++i) {
+            var c = this.children[i];
+            if (c.isDynamic()) {
+                alert("Not supported (lazy load + expand all)");
+                break;
+            } else if (! c.multiExpand) {
+                alert("Not supported (no multi-expand + expand all)");
+                break;
+            } else {
+                c.expand();
+                c.expandAll();
+            }
+        }
+    },
+
+    /**
+     * Recursively collapses all of this node's children.
+     */
+    collapseAll: function() {
+        for (var i=0;i<this.children.length;++i) {
+            this.children[i].collapse();
+            this.children[i].collapseAll();
+        }
+    },
+
+    /**
+     * Configures this node for dynamically obtaining the child data
+     * when the node is first expanded.
+     *
+     * @param fmDataLoader {function} the function that will be used to get the data.
+     */
+    setDynamicLoad: function(fnDataLoader) {
+        this.dataLoader = fnDataLoader;
+        this._dynLoad = true;
+    },
+
+    /**
+     * Evaluates if this node is the root node of the tree
+     *
+     * @return {boolean} true if this is the root node
+     */
+    isRoot: function() {
+        return (this == this.tree.root);
+    },
+
+    /**
+     * Evaluates if this node's children should be loaded dynamically.  Looks for
+     * the property both in this instance and the root node.  If the tree is
+     * defined to load all children dynamically, the data callback function is
+     * defined in the root node
+     *
+     * @return {boolean} true if this node's children are to be loaded dynamically
+     */
+    isDynamic: function() {
+        var lazy = (!this.isRoot() && (this._dynLoad || this.tree.root._dynLoad));
+        return lazy;
+    },
+
+    /**
+     * Checks if this node has children.  If this node is lazy-loading and the
+     * children have not been rendered, we do not know whether or not there
+     * are actual children.  In most cases, we need to assume that there are
+     * children (for instance, the toggle needs to show the expandable
+     * presentation state).  In other times we want to know if there are rendered
+     * children.  For the latter, "checkForLazyLoad" should be false.
+     *
+     * @param checkForLazyLoad {boolean} should we check for unloaded children?
+     * @return {boolean} true if this has children or if it might and we are
+     * checking for this condition.
+     */
+    hasChildren: function(checkForLazyLoad) {
+        return ( this.children.length > 0 ||
+                (checkForLazyLoad && this.isDynamic() && !this.childrenRendered) );
+    },
+
+    /**
+     * Expands if node is collapsed, collapses otherwise.
+     */
+    toggle: function() {
+        if (!this.tree.locked && ( this.hasChildren(true) || this.isDynamic()) ) {
+            if (this.expanded) { this.collapse(); } else { this.expand(); }
+        }
+    },
+
+    /**
+     * Returns the markup for this node and its children.
+     *
+     * @return {string} the markup for this node and its expanded children.
+     */
+    getHtml: function() {
+        var sb = [];
+        sb[sb.length] = '<div class="ygtvitem" id="' + this.getElId() + '">';
+        sb[sb.length] = this.getNodeHtml();
+        sb[sb.length] = this.getChildrenHtml();
+        sb[sb.length] = '</div>';
+        return sb.join("");
+    },
+
+    /**
+     * Called when first rendering the tree.  We always build the div that will
+     * contain this nodes children, but we don't render the children themselves
+     * unless this node is expanded.
+     *
+     * @return {string} the children container div html and any expanded children
+     * @private
+     */
+    getChildrenHtml: function() {
+        var sb = [];
+        sb[sb.length] = '<div class="ygtvchildren"';
+        sb[sb.length] = ' id="' + this.getChildrenElId() + '"';
+        if (!this.expanded) {
+            sb[sb.length] = ' style="display:none;"';
+        }
+        sb[sb.length] = '>';
+
+        // Don't render the actual child node HTML unless this node is expanded.
+        if (this.hasChildren(true) && this.expanded) {
+            sb[sb.length] = this.renderChildren();
+        }
+
+        sb[sb.length] = '</div>';
+
+        return sb.join("");
+    },
+
+    /**
+     * Generates the markup for the child nodes.  This is not done until the node
+     * is expanded.
+     *
+     * @return {string} the html for this node's children
+     * @private
+     */
+    renderChildren: function() {
+
+
+        var node = this;
+
+        if (this.isDynamic() && !this.childrenRendered) {
+            this.isLoading = true;
+            this.tree.locked = true;
+
+            if (this.dataLoader) {
+                setTimeout(
+                    function() {
+                        node.dataLoader(node,
+                            function() {
+                                node.loadComplete();
+                            });
+                    }, 10);
+
+            } else if (this.tree.root.dataLoader) {
+
+                setTimeout(
+                    function() {
+                        node.tree.root.dataLoader(node,
+                            function() {
+                                node.loadComplete();
+                            });
+                    }, 10);
+
+            } else {
+                return "Error: data loader not found or not specified.";
+            }
+
+            return "";
+
+        } else {
+            return this.completeRender();
+        }
+    },
+
+    /**
+     * Called when we know we have all the child data.
+     * @return {string} children html
+     */
+    completeRender: function() {
+        var sb = [];
+
+        for (var i=0; i < this.children.length; ++i) {
+            sb[sb.length] = this.children[i].getHtml();
+        }
+
+        this.childrenRendered = true;
+
+        return sb.join("");
+    },
+
+    /**
+     * Load complete is the callback function we pass to the data provider
+     * in dynamic load situations.
+     */
+    loadComplete: function() {
+        this.getChildrenEl().innerHTML = this.completeRender();
+        this.isLoading = false;
+        this.expand();
+        this.tree.locked = false;
+    },
+
+    /**
+     * Returns this node's ancestor at the specified depth.
+     *
+     * @param {int} depth the depth of the ancestor.
+     * @return {YAHOO.widget.Node} the ancestor
+     */
+    getAncestor: function(depth) {
+        if (depth >= this.depth || depth < 0)  {
+            return null;
+        }
+
+        var p = this.parent;
+
+        while (p.depth > depth) {
+            p = p.parent;
+        }
+
+        return p;
+    },
+
+    /**
+     * Returns the css class for the spacer at the specified depth for
+     * this node.  If this node's ancestor at the specified depth
+     * has a next sibling the presentation is different than if it
+     * does not have a next sibling
+     *
+     * @param {int} depth the depth of the ancestor.
+     * @return {string} the css class for the spacer
+     */
+    getDepthStyle: function(depth) {
+        return (this.getAncestor(depth).nextSibling) ?
+            "ygtvdepthcell" : "ygtvblankdepthcell";
+    },
+
+    /**
+     * Get the markup for the node.  This is designed to be overrided so that we can
+     * support different types of nodes.
+     *
+     * @return {string} the html for this node
+     */
+    getNodeHtml: function() {
+        return "";
+    }
+
+};
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * @class A custom YAHOO.widget.Node that handles the unique nature of
+ * the virtual, presentationless root node.
+ *
+ * @extends YAHOO.widget.Node
+ * @constructor
+ */
+YAHOO.widget.RootNode = function(oTree) {
+       // Initialize the node with null params.  The root node is a
+       // special case where the node has no presentation.  So we have
+       // to alter the standard properties a bit.
+       this.init(null, null, true);
+
+       /**
+        * For the root node, we get the tree reference from as a param
+        * to the constructor instead of from the parent element.
+        *
+        * @type YAHOO.widget.TreeView
+        */
+       this.tree = oTree;
+};
+YAHOO.widget.RootNode.prototype = new YAHOO.widget.Node();
+
+// overrides YAHOO.widget.Node
+YAHOO.widget.RootNode.prototype.getNodeHtml = function() {
+       return "";
+};
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * @class The default node presentation.  The first parameter should be
+ * either a string that will be used as the node's label, or an object
+ * that has a string propery called label.  By default, the clicking the
+ * label will toggle the expanded/collapsed state of the node.  By
+ * changing the href property of the instance, this behavior can be
+ * changed so that the label will go to the specified href.
+ *
+ * @extends YAHOO.widget.Node
+ * @constructor
+ * @param oData {object} a string or object containing the data that will
+ * be used to render this node
+ * @param oParent {YAHOO.widget.Node} this node's parent node
+ * @param expanded {boolean} the initial expanded/collapsed state
+ */
+YAHOO.widget.TextNode = function(oData, oParent, expanded) {
+       if (oParent) {
+               this.init(oData, oParent, expanded);
+               this.setUpLabel(oData);
+       }
+};
+
+YAHOO.widget.TextNode.prototype = new YAHOO.widget.Node();
+
+/**
+ * The CSS class for the label href.  Defaults to ygtvlabel, but can be
+ * overridden to provide a custom presentation for a specific node.
+ *
+ * @type string
+ */
+YAHOO.widget.TextNode.prototype.labelStyle = "ygtvlabel";
+
+/**
+ * The derived element id of the label for this node
+ *
+ * @type string
+ */
+YAHOO.widget.TextNode.prototype.labelElId = null;
+
+/**
+ * The text for the label.  It is assumed that the oData parameter will
+ * either be a string that will be used as the label, or an object that
+ * has a property called "label" that we will use.
+ *
+ * @type string
+ */
+YAHOO.widget.TextNode.prototype.label = null;
+
+/**
+ * Sets up the node label
+ *
+ * @param oData string containing the label, or an object with a label property
+ */
+YAHOO.widget.TextNode.prototype.setUpLabel = function(oData) {
+       if (typeof oData == "string") {
+               oData = { label: oData };
+       }
+       this.label = oData.label;
+
+       // update the link
+       if (oData.href) {
+               this.href = oData.href;
+       }
+
+       // set the target
+       if (oData.target) {
+               this.target = oData.target;
+       }
+
+       this.labelElId = "ygtvlabelel" + this.index;
+};
+
+/**
+ * Returns the label element
+ *
+ * @return {object} the element
+ */
+YAHOO.widget.TextNode.prototype.getLabelEl = function() {
+       return document.getElementById(this.labelElId);
+};
+
+// overrides YAHOO.widget.Node
+YAHOO.widget.TextNode.prototype.getNodeHtml = function() {
+       var sb = new Array();
+
+       sb[sb.length] = '<table border="0" cellpadding="0" cellspacing="0">';
+       sb[sb.length] = '<tr>';
+
+       for (i=0;i<this.depth;++i) {
+               // sb[sb.length] = '<td class="ygtvdepthcell">&nbsp;</td>';
+               sb[sb.length] = '<td class="' + this.getDepthStyle(i) + '">&nbsp;</td>';
+       }
+
+       var getNode = 'YAHOO.widget.TreeView.getNode(\'' +
+                                       this.tree.id + '\',' + this.index + ')';
+
+       sb[sb.length] = '<td';
+       // sb[sb.length] = ' onselectstart="return false"';
+       sb[sb.length] = ' id="' + this.getToggleElId() + '"';
+       sb[sb.length] = ' class="' + this.getStyle() + '"';
+       if (this.hasChildren(true)) {
+               sb[sb.length] = ' onmouseover="this.className=';
+               sb[sb.length] = getNode + '.getHoverStyle()"';
+               sb[sb.length] = ' onmouseout="this.className=';
+               sb[sb.length] = getNode + '.getStyle()"';
+       }
+       sb[sb.length] = ' onclick="javascript:' + this.getToggleLink() + '">&nbsp;';
+       sb[sb.length] = '</td>';
+       sb[sb.length] = '<td>';
+       sb[sb.length] = '<a';
+       sb[sb.length] = ' id="' + this.labelElId + '"';
+       sb[sb.length] = ' class="' + this.labelStyle + '"';
+       sb[sb.length] = ' href="' + this.href + '"';
+       sb[sb.length] = ' target="' + this.target + '"';
+       if (this.hasChildren(true)) {
+               sb[sb.length] = ' onmouseover="document.getElementById(\'';
+               sb[sb.length] = this.getToggleElId() + '\').className=';
+               sb[sb.length] = getNode + '.getHoverStyle()"';
+               sb[sb.length] = ' onmouseout="document.getElementById(\'';
+               sb[sb.length] = this.getToggleElId() + '\').className=';
+               sb[sb.length] = getNode + '.getStyle()"';
+       }
+       sb[sb.length] = ' >';
+       sb[sb.length] = this.label;
+       sb[sb.length] = '</a>';
+       sb[sb.length] = '</td>';
+       sb[sb.length] = '</tr>';
+       sb[sb.length] = '</table>';
+
+       return sb.join("");
+};
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * @class menu-specific implementation that differs in that only one sibling
+ * can be expanded at a time.
+ * @extends YAHOO.widget.TextNode
+ * @constructor
+ */
+YAHOO.widget.MenuNode = function(oData, oParent, expanded) {
+       if (oParent) {
+               this.init(oData, oParent, expanded);
+               this.setUpLabel(oData);
+       }
+
+       // Menus usually allow only one branch to be open at a time.
+       this.multiExpand = false;
+
+};
+
+YAHOO.widget.MenuNode.prototype = new YAHOO.widget.TextNode();
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * @class This implementation takes either a string or object for the
+ * oData argument.  If is it a string, we will use it for the display
+ * of this node (and it can contain any html code).  If the parameter
+ * is an object, we look for a parameter called "html" that will be
+ * used for this node's display.
+ *
+ * @extends YAHOO.widget.Node
+ * @constructor
+ * @param oData {object} a string or object containing the data that will
+ * be used to render this node
+ * @param oParent {YAHOO.widget.Node} this node's parent node
+ * @param expanded {boolean} the initial expanded/collapsed state
+ * @param hasIcon {boolean} specifies whether or not leaf nodes should
+ * have an icon
+ */
+YAHOO.widget.HTMLNode = function(oData, oParent, expanded, hasIcon) {
+       if (oParent) {
+               this.init(oData, oParent, expanded);
+               this.initContent(oData, hasIcon);
+       }
+};
+
+YAHOO.widget.HTMLNode.prototype = new YAHOO.widget.Node();
+
+/**
+ * The CSS class for the label href.  Defaults to ygtvlabel, but can be
+ * overridden to provide a custom presentation for a specific node.
+ *
+ * @type string
+ */
+YAHOO.widget.HTMLNode.prototype.contentStyle = "ygtvhtml";
+
+/**
+ * The generated id that will contain the data passed in by the implementer.
+ *
+ * @type string
+ */
+YAHOO.widget.HTMLNode.prototype.contentElId = null;
+
+/**
+ * The HTML content to use for this node's display
+ *
+ * @type string
+ */
+YAHOO.widget.HTMLNode.prototype.content = null;
+
+/**
+ * Sets up the node label
+ *
+ * @param {object} html string or object containing a html field
+ * @param {boolean} hasIcon determines if the node will be rendered with an
+ * icon or not
+ */
+YAHOO.widget.HTMLNode.prototype.initContent = function(oData, hasIcon) {
+       if (typeof oData == "string") {
+               oData = { html: oData };
+       }
+
+       this.html = oData.html;
+       this.contentElId = "ygtvcontentel" + this.index;
+       this.hasIcon = hasIcon;
+};
+
+/**
+ * Returns the outer html element for this node's content
+ *
+ * @return {Object} the element
+ */
+YAHOO.widget.HTMLNode.prototype.getContentEl = function() {
+       return document.getElementById(this.contentElId);
+};
+
+// overrides YAHOO.widget.Node
+YAHOO.widget.HTMLNode.prototype.getNodeHtml = function() {
+       var sb = new Array();
+
+       sb[sb.length] = '<table border="0" cellpadding="0" cellspacing="0">';
+       sb[sb.length] = '<tr>';
+
+       for (i=0;i<this.depth;++i) {
+               sb[sb.length] = '<td class="' + this.getDepthStyle(i) + '">&nbsp;</td>';
+       }
+
+       if (this.hasIcon) {
+               sb[sb.length] = '<td';
+               sb[sb.length] = ' id="' + this.getToggleElId() + '"';
+               sb[sb.length] = ' class="' + this.getStyle() + '"';
+               sb[sb.length] = ' onclick="javascript:' + this.getToggleLink() + '">&nbsp;';
+               if (this.hasChildren(true)) {
+                       sb[sb.length] = ' onmouseover="this.className=';
+                       sb[sb.length] = 'YAHOO.widget.TreeView.getNode(\'';
+                       sb[sb.length] = this.tree.id + '\',' + this.index +  ').getHoverStyle()"';
+                       sb[sb.length] = ' onmouseout="this.className=';
+                       sb[sb.length] = 'YAHOO.widget.TreeView.getNode(\'';
+                       sb[sb.length] = this.tree.id + '\',' + this.index +  ').getStyle()"';
+               }
+               sb[sb.length] = '</td>';
+       }
+
+       sb[sb.length] = '<td';
+       sb[sb.length] = ' id="' + this.contentElId + '"';
+       sb[sb.length] = ' class="' + this.contentStyle + '"';
+       sb[sb.length] = ' >';
+       sb[sb.length] = this.html;
+       sb[sb.length] = '</td>';
+       sb[sb.length] = '</tr>';
+       sb[sb.length] = '</table>';
+
+       return sb.join("");
+};
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * Static factory class for tree view expand/collapse animations
+ */
+YAHOO.widget.TVAnim = new function() {
+       /**
+        * Constant for the fade in animation
+        *
+        * @type string
+        */
+       this.FADE_IN  = "YAHOO.widget.TVFadeIn";
+
+       /**
+        * Constant for the fade out animation
+        *
+        * @type string
+        */
+       this.FADE_OUT = "YAHOO.widget.TVFadeOut";
+
+       /**
+        * Returns a ygAnim instance of the given type
+        *
+        * @param type {string} the type of animation
+        * @param el {HTMLElement} the element to element (probably the children div)
+        * @param callback {function} function to invoke when the animation is done.
+        * @return {ygAnim} the animation instance
+        */
+       this.getAnim = function(type, el, callback) {
+               switch (type) {
+                       case this.FADE_IN:      return new YAHOO.widget.TVFadeIn(el, callback);
+                       case this.FADE_OUT:     return new YAHOO.widget.TVFadeOut(el, callback);
+                       default:                        return null;
+               }
+       };
+
+       /**
+        * Returns true if the specified animation class is available
+        *
+        * @param type {string} the type of animation
+        * @return {boolean} true if valid, false if not
+        */
+       this.isValid = function(type) {
+               return ( "undefined" != eval("typeof " + type) );
+       };
+};
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * 1/2 second fade-in
+ *
+ * @constructor
+ * @param el {HTMLElement} the element to animate
+ * @param callback {function} function to invoke when the animation is finished
+ */
+YAHOO.widget.TVFadeIn = function(el, callback) {
+       /**
+        * The animation dom ref
+        */
+       this.el = el;
+
+       /**
+        * the callback to invoke when the animation is complete
+        *
+        * @type function
+        */
+       this.callback = callback;
+
+       /**
+        * @private
+        */
+};
+
+/**
+ * Performs the animation
+ */
+YAHOO.widget.TVFadeIn.prototype = {
+    animate: function() {
+        var tvanim = this;
+
+        var s = this.el.style;
+        s.opacity = 0.1;
+        s.filter = "alpha(opacity=10)";
+        s.display = "";
+
+        // var dur = ( navigator.userAgent.match(/msie/gi) ) ? 0.05 : 0.4;
+        var dur = 0.4;
+        // var a = new ygAnim_Fade(this.el, dur, 1);
+        // a.setStart(0.1);
+        // a.onComplete = function() { tvanim.onComplete(); };
+
+        // var a = new YAHOO.util.Anim(this.el, 'opacity', 0.1, 1);
+        var a = new YAHOO.util.Anim(this.el, {opacity: {from: 0.1, to: 1, unit:""}}, dur);
+        a.onComplete.subscribe( function() { tvanim.onComplete(); } );
+        a.animate();
+    },
+
+    /**
+     * Clean up and invoke callback
+     */
+    onComplete: function() {
+        this.callback();
+    }
+};
+
+/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
+
+/**
+ * 1/2 second fade out
+ *
+ * @constructor
+ * @param el {HTMLElement} the element to animate
+ * @param callback {Function} function to invoke when the animation is finished
+ */
+YAHOO.widget.TVFadeOut = function(el, callback) {
+       /**
+        * The animation dom ref
+        */
+       this.el = el;
+
+       /**
+        * the callback to invoke when the animation is complete
+        *
+        * @type function
+        */
+       this.callback = callback;
+
+       /**
+        * @private
+        */
+};
+
+/**
+ * Performs the animation
+ */
+YAHOO.widget.TVFadeOut.prototype = {
+    animate: function() {
+        var tvanim = this;
+        // var dur = ( navigator.userAgent.match(/msie/gi) ) ? 0.05 : 0.4;
+        var dur = 0.4;
+        // var a = new ygAnim_Fade(this.el, dur, 0.1);
+        // a.onComplete = function() { tvanim.onComplete(); };
+
+        // var a = new YAHOO.util.Anim(this.el, 'opacity', 1, 0.1);
+        var a = new YAHOO.util.Anim(this.el, {opacity: {from: 1, to: 0.1, unit:""}}, dur);
+        a.onComplete.subscribe( function() { tvanim.onComplete(); } );
+        a.animate();
+    },
+
+    /**
+     * Clean up and invoke callback
+     */
+    onComplete: function() {
+        var s = this.el.style;
+        s.display = "none";
+        // s.opacity = 1;
+        s.filter = "alpha(opacity=100)";
+        this.callback();
+    }
+};
+
index 5b7cf47034302593f9f152a0acc130e6a301c1dd..00c451b3ebd7af8da9efcd5a279895aedd4142f3 100644 (file)
 
 
         </style>
-        <script type="text/javascript" src="bundled-libs/dragdrop.js" ></script>
-        <script type="text/javascript" src="bundled-libs/imgedit.js" ></script>
+        <script type="text/javascript" src="{serendipity_getFile file='dragdrop.js'}" ></script>
+        <script type="text/javascript" src="{serendipity_getFile file='imgedit.js'}" ></script>
         {/if}
 
-        <script type="text/javascript" src="bundled-libs/YahooUI/treeview/YAHOO.js"></script>
-        <script type="text/javascript" src="bundled-libs/YahooUI/treeview/treeview.js" ></script>
+        <script type="text/javascript" src="{serendipity_getFile file='YahooUI/treeview/YAHOO.js'}"></script>
+        <script type="text/javascript" src="{serendipity_getFile file='YahooUI/treeview/treeview.js'}"></script>
     </head>
 
     <script type="text/javascript">
diff --git a/templates/default/dragdrop.js b/templates/default/dragdrop.js
new file mode 100644 (file)
index 0000000..a0520ee
--- /dev/null
@@ -0,0 +1,802 @@
+/**********************************************************
+ Very minorly modified from the example by Tim Taylor
+ http://tool-man.org/examples/sorting.html
+
+ Added Coordinate.prototype.inside( northwest, southeast );
+
+    Copyright (c) 2005 Tim Taylor Consulting <http://tool-man.org/>
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included
+    in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+    OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+ **********************************************************/
+
+var Coordinates = {
+    ORIGIN : new Coordinate(0, 0),
+
+    northwestPosition : function(element) {
+        var x = parseInt(element.style.left);
+        var y = parseInt(element.style.top);
+
+        return new Coordinate(isNaN(x) ? 0 : x, isNaN(y) ? 0 : y);
+    },
+
+    southeastPosition : function(element) {
+        return Coordinates.northwestPosition(element).plus(
+                new Coordinate(element.offsetWidth, element.offsetHeight));
+    },
+
+    northwestOffset : function(element, isRecursive) {
+        var offset = new Coordinate(element.offsetLeft, element.offsetTop);
+
+        if (!isRecursive) return offset;
+
+        var parent = element.offsetParent;
+        while (parent) {
+            offset = offset.plus(
+                    new Coordinate(parent.offsetLeft, parent.offsetTop));
+            parent = parent.offsetParent;
+        }
+        return offset;
+    },
+
+    southeastOffset : function(element, isRecursive) {
+        return Coordinates.northwestOffset(element, isRecursive).plus(
+                new Coordinate(element.offsetWidth, element.offsetHeight));
+    },
+
+    fixEvent : function(event) {
+        event.windowCoordinate = new Coordinate(event.clientX, event.clientY);
+    }
+};
+
+function Coordinate(x, y) {
+    this.x = x;
+    this.y = y;
+}
+
+Coordinate.prototype.toString = function() {
+    return "(" + this.x + "," + this.y + ")";
+}
+
+Coordinate.prototype.plus = function(that) {
+    return new Coordinate(this.x + that.x, this.y + that.y);
+}
+
+Coordinate.prototype.minus = function(that) {
+    return new Coordinate(this.x - that.x, this.y - that.y);
+}
+
+Coordinate.prototype.distance = function(that) {
+    var deltaX = this.x - that.x;
+    var deltaY = this.y - that.y;
+
+    return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
+}
+
+Coordinate.prototype.max = function(that) {
+    var x = Math.max(this.x, that.x);
+    var y = Math.max(this.y, that.y);
+    return new Coordinate(x, y);
+}
+
+Coordinate.prototype.constrain = function(min, max) {
+    if (min.x > max.x || min.y > max.y) return this;
+
+    var x = this.x;
+    var y = this.y;
+
+    if (min.x != null) x = Math.max(x, min.x);
+    if (max.x != null) x = Math.min(x, max.x);
+    if (min.y != null) y = Math.max(y, min.y);
+    if (max.y != null) y = Math.min(y, max.y);
+
+    return new Coordinate(x, y);
+}
+
+Coordinate.prototype.reposition = function(element) {
+    element.style["top"] = this.y + "px";
+    element.style["left"] = this.x + "px";
+}
+
+Coordinate.prototype.equals = function(that) {
+    if (this == that) return true;
+    if (!that || that == null) return false;
+
+    return this.x == that.x && this.y == that.y;
+}
+
+// returns true of this point is inside specified box
+Coordinate.prototype.inside = function(northwest, southeast) {
+    if ((this.x >= northwest.x) && (this.x <= southeast.x) &&
+        (this.y >= northwest.y) && (this.y <= southeast.y)) {
+
+        return true;
+    }
+    return false;
+}
+
+/*
+ * drag.js - click & drag DOM elements
+ *
+ * originally based on Youngpup's dom-drag.js, www.youngpup.net
+ */
+
+/**********************************************************
+ Further modified from the example by Tim Taylor
+ http://tool-man.org/examples/sorting.html
+
+ Changed onMouseMove where it calls group.onDrag and then
+ adjusts the offset for changes to the DOM.  If the item
+ being moved changed parents it would be off so changed to
+ get the absolute offset (recursive northwestOffset).
+
+ **********************************************************/
+
+var Drag = {
+    BIG_Z_INDEX : 10000,
+    group : null,
+    isDragging : false,
+
+    makeDraggable : function(group) {
+        group.handle = group;
+        group.handle.group = group;
+
+        group.minX = null;
+        group.minY = null;
+        group.maxX = null;
+        group.maxY = null;
+        group.threshold = 0;
+        group.thresholdY = 0;
+        group.thresholdX = 0;
+
+        group.onDragStart = new Function();
+        group.onDragEnd = new Function();
+        group.onDrag = new Function();
+
+        // TODO: use element.prototype.myFunc
+        group.setDragHandle = Drag.setDragHandle;
+        group.setDragThreshold = Drag.setDragThreshold;
+        group.setDragThresholdX = Drag.setDragThresholdX;
+        group.setDragThresholdY = Drag.setDragThresholdY;
+        group.constrain = Drag.constrain;
+        group.constrainVertical = Drag.constrainVertical;
+        group.constrainHorizontal = Drag.constrainHorizontal;
+
+        group.onmousedown = Drag.onMouseDown;
+    },
+
+    constrainVertical : function() {
+        var nwOffset = Coordinates.northwestOffset(this, true);
+        this.minX = nwOffset.x;
+        this.maxX = nwOffset.x;
+    },
+
+    constrainHorizontal : function() {
+        var nwOffset = Coordinates.northwestOffset(this, true);
+        this.minY = nwOffset.y;
+        this.maxY = nwOffset.y;
+    },
+
+    constrain : function(nwPosition, sePosition) {
+        this.minX = nwPosition.x;
+        this.minY = nwPosition.y;
+        this.maxX = sePosition.x;
+        this.maxY = sePosition.y;
+    },
+
+    setDragHandle : function(handle) {
+        if (handle && handle != null) {
+            this.handle = handle;
+        } else {
+            this.handle = this;
+        }
+
+        this.handle.group = this;
+        this.onmousedown = null;
+        // this.onclick = Drag.toggleMe;
+        // this.handle.onclick = Drag.toggleMe;
+        this.handle.onmousedown = Drag.onMouseDown;
+    },
+
+    toggleMe : function(str) {
+        alert('Toggle: ' + str);
+    },
+
+    setDragThreshold : function(threshold) {
+        if (isNaN(parseInt(threshold))) return;
+
+        this.threshold = threshold;
+    },
+
+    setDragThresholdX : function(threshold) {
+        if (isNaN(parseInt(threshold))) return;
+
+        this.thresholdX = threshold;
+    },
+
+    setDragThresholdY : function(threshold) {
+        if (isNaN(parseInt(threshold))) return;
+
+        this.thresholdY = threshold;
+    },
+
+    onMouseDown : function(event) {
+        event = Drag.fixEvent(event);
+        Drag.group = this.group;
+
+        var group = this.group;
+        var mouse = event.windowCoordinate;
+        var nwOffset = Coordinates.northwestOffset(group, true);
+        var nwPosition = Coordinates.northwestPosition(group);
+        var sePosition = Coordinates.southeastPosition(group);
+        var seOffset = Coordinates.southeastOffset(group, true);
+
+        group.originalOpacity = group.style.opacity;
+        group.originalZIndex = group.style.zIndex;
+        group.initialWindowCoordinate = mouse;
+        // TODO: need a better name, but don't yet understand how it
+        // participates in the magic while dragging
+        group.dragCoordinate = mouse;
+
+        Drag.showStatus(mouse, nwPosition, sePosition, nwOffset, seOffset);
+
+        group.onDragStart(nwPosition, sePosition, nwOffset, seOffset);
+
+        // TODO: need better constraint API
+        if (group.minX != null)
+            group.minMouseX = mouse.x - nwPosition.x +
+                    group.minX - nwOffset.x;
+        if (group.maxX != null)
+            group.maxMouseX = group.minMouseX + group.maxX - group.minX;
+
+        if (group.minY != null)
+            group.minMouseY = mouse.y - nwPosition.y +
+                    group.minY - nwOffset.y;
+        if (group.maxY != null)
+            group.maxMouseY = group.minMouseY + group.maxY - group.minY;
+
+        group.mouseMin = new Coordinate(group.minMouseX, group.minMouseY);
+        group.mouseMax = new Coordinate(group.maxMouseX, group.maxMouseY);
+
+        document.onmousemove = Drag.onMouseMove;
+        document.onmouseup = Drag.onMouseUp;
+
+        return false;
+    },
+
+    showStatus : function(mouse, nwPosition, sePosition, nwOffset, seOffset) {
+        /*window.status =
+                "mouse: " + mouse.toString() + "    " +
+                "NW pos: " + nwPosition.toString() + "    " +
+                "SE pos: " + sePosition.toString() + "    " +
+                "NW offset: " + nwOffset.toString() + "    " +
+                "SE offset: " + seOffset.toString();*/
+    },
+
+    onMouseMove : function(event) {
+        event = Drag.fixEvent(event);
+        var group = Drag.group;
+        var mouse = event.windowCoordinate;
+        var nwOffset = Coordinates.northwestOffset(group, true);
+        var nwPosition = Coordinates.northwestPosition(group);
+        var sePosition = Coordinates.southeastPosition(group);
+        var seOffset = Coordinates.southeastOffset(group, true);
+
+        Drag.showStatus(mouse, nwPosition, sePosition, nwOffset, seOffset);
+
+        if (!Drag.isDragging) {
+            if (group.threshold > 0) {
+                var distance = group.initialWindowCoordinate.distance(
+                        mouse);
+                if (distance < group.threshold) return true;
+            } else if (group.thresholdY > 0) {
+                var deltaY = Math.abs(group.initialWindowCoordinate.y - mouse.y);
+                if (deltaY < group.thresholdY) return true;
+            } else if (group.thresholdX > 0) {
+                var deltaX = Math.abs(group.initialWindowCoordinate.x - mouse.x);
+                if (deltaX < group.thresholdX) return true;
+            }
+
+            Drag.isDragging = true;
+            group.style["zIndex"] = Drag.BIG_Z_INDEX;
+            group.style["opacity"] = 0.75;
+        }
+
+        // TODO: need better constraint API
+        var adjusted = mouse.constrain(group.mouseMin, group.mouseMax);
+        nwPosition = nwPosition.plus(adjusted.minus(group.dragCoordinate));
+        nwPosition.reposition(group);
+        group.dragCoordinate = adjusted;
+
+        // once dragging has started, the position of the group
+        // relative to the mouse should stay fixed.  They can get out
+        // of sync if the DOM is manipulated while dragging, so we
+        // correct the error here
+        //
+        // TODO: what we really want to do is find the offset from
+        // our corner to the mouse coordinate and adjust to keep it
+        // the same
+
+        // changed to be recursive/use absolute offset for corrections
+        var offsetBefore = Coordinates.northwestOffset(group, true);
+        group.onDrag(nwPosition, sePosition, nwOffset, seOffset);
+        var offsetAfter = Coordinates.northwestOffset(group, true);
+
+        if (!offsetBefore.equals(offsetAfter)) {
+            var errorDelta = offsetBefore.minus(offsetAfter);
+            nwPosition = Coordinates.northwestPosition(group).plus(errorDelta);
+            nwPosition.reposition(group);
+        }
+
+        return false;
+    },
+
+    onMouseUp : function(event) {
+        event = Drag.fixEvent(event);
+        var group = Drag.group;
+
+        var mouse = event.windowCoordinate;
+        var nwOffset = Coordinates.northwestOffset(group, true);
+        var nwPosition = Coordinates.northwestPosition(group);
+        var sePosition = Coordinates.southeastPosition(group);
+        var seOffset = Coordinates.southeastOffset(group, true);
+
+        document.onmousemove = null;
+        document.onmouseup   = null;
+        group.onDragEnd(nwPosition, sePosition, nwOffset, seOffset);
+
+        if (Drag.isDragging) {
+            // restoring zIndex before opacity avoids visual flicker in Firefox
+            group.style["zIndex"] = group.originalZIndex;
+            group.style["opacity"] = group.originalOpacity;
+        }
+
+        Drag.group = null;
+        Drag.isDragging = false;
+
+        return false;
+    },
+
+    fixEvent : function(event) {
+        if (typeof event == 'undefined') event = window.event;
+        Coordinates.fixEvent(event);
+
+        return event;
+    }
+};
+
+/**********************************************************
+ Adapted from the sortable lists example by Tim Taylor
+ http://tool-man.org/examples/sorting.html
+ Modified by Tom Westcott : http://www.cyberdummy.co.uk
+ **********************************************************/
+
+var DragDrop = {
+    firstContainer : null,
+    lastContainer : null,
+    parent_id : null,
+    parent_group : null,
+    makeListContainer : function(list, group) {
+        // each container becomes a linked list node
+        if (this.firstContainer == null) {
+            this.firstContainer = this.lastContainer = list;
+            list.previousContainer = null;
+            list.nextContainer = null;
+        } else {
+            list.previousContainer = this.lastContainer;
+            list.nextContainer = null;
+            this.lastContainer.nextContainer = list;
+            this.lastContainer = list;
+        }
+
+        // these functions are called when an item is draged over
+        // a container or out of a container bounds.  onDragOut
+        // is also called when the drag ends with an item having
+        // been added to the container
+        list.onDragOver = new Function();
+        list.onDragOut = new Function();
+                list.onDragDrop = new Function();
+                list.group = group;
+
+        var items = list.getElementsByTagName( "li" );
+
+        for (var i = 0; i < items.length; i++) {
+            DragDrop.makeItemDragable(items[i]);
+        }
+    },
+
+    serData : function ( group, theid ) {
+        var container = DragDrop.firstContainer;
+        var j = 0;
+        var string = "";
+
+        while (container != null) {
+            if(theid != null && container.id != theid) {
+              container = container.nextContainer;
+              continue;
+            }
+
+            if(group != null && container.group != group) {
+              container = container.nextContainer;
+              continue;
+            }
+
+            j ++;
+            if (j > 1) {
+              string += ":";
+            }
+            string += container.id;
+
+            var items = container.getElementsByTagName( "li" );
+            string += "(";
+            for (var i = 0; i < items.length; i++) {
+                if(i > 0) {
+                    string += ",";
+                }
+
+                string += items[i].id;
+            }
+            string += ")";
+
+            container = container.nextContainer;
+        }
+
+        return string;
+    },
+
+    makeItemDragable : function(item) {
+        Drag.makeDraggable(item);
+        item.setDragThreshold(5);
+
+        // tracks if the item is currently outside all containers
+        item.isOutside = false;
+
+        item.onDragStart = DragDrop.onDragStart;
+        item.onDrag = DragDrop.onDrag;
+        item.onDragEnd = DragDrop.onDragEnd;
+
+        item.setDragHandle(document.getElementById('g' + item.id));
+    },
+
+    onDragStart : function(nwPosition, sePosition, nwOffset, seOffset) {
+        // update all container bounds, since they may have changed
+        // on a previous drag
+        //
+        // could be more smart about when to do this
+        var container = DragDrop.firstContainer;
+        while (container != null) {
+            container.northwest = Coordinates.northwestOffset( container, true );
+            container.southeast = Coordinates.southeastOffset( container, true );
+            container = container.nextContainer;
+        }
+
+        // item starts out over current parent
+        this.parentNode.onDragOver();
+        parent_id = this.parentNode.id;
+        parent_group = this.parentNode.group;
+    },
+
+    onDrag : function(nwPosition, sePosition, nwOffset, seOffset) {
+        // check if we were nowhere
+        if (this.isOutside) {
+            // check each container to see if in its bounds
+            var container = DragDrop.firstContainer;
+            while (container != null) {
+
+                if ((nwOffset.inside( container.northwest, container.southeast ) ||
+                    seOffset.inside( container.northwest, container.southeast )) && container.group == parent_group) {
+                    // we're inside this one
+                    container.onDragOver();
+                    this.isOutside = false;
+
+                    // since isOutside was true, the current parent is a
+                    // temporary clone of some previous container node and
+                    // it needs to be removed from the document
+                    var tempParent = this.parentNode;
+                    tempParent.removeChild( this );
+                    container.appendChild( this );
+                    tempParent.parentNode.removeChild( tempParent );
+                    break;
+                }
+                container = container.nextContainer;
+            }
+            // we're still not inside the bounds of any container
+            if (this.isOutside)
+                return;
+
+        // check if we're outside our parent's bounds
+        } else if (!(nwOffset.inside( this.parentNode.northwest, this.parentNode.southeast ) ||
+            seOffset.inside( this.parentNode.northwest, this.parentNode.southeast ))) {
+
+            this.parentNode.onDragOut();
+            this.isOutside = true;
+
+            // check if we're inside a new container's bounds
+            var container = DragDrop.firstContainer;
+            while (container != null) {
+                if ((nwOffset.inside( container.northwest, container.southeast ) ||
+                    seOffset.inside( container.northwest, container.southeast )) && container.group == parent_group) {
+                    // we're inside this one
+                    container.onDragOver();
+                    this.isOutside = false;
+                    this.parentNode.removeChild( this );
+                    container.appendChild( this );
+                    break;
+                }
+                container = container.nextContainer;
+            }
+            // if we're not in any container now, make a temporary clone of
+            // the previous container node and add it to the document
+            if (this.isOutside) {
+                var tempParent = this.parentNode.cloneNode( false );
+                this.parentNode.removeChild( this );
+                tempParent.appendChild( this );
+                // body puts a border or item at bottom of page if do not have this
+                                tempParent.style.border = 0;
+                document.getElementsByTagName( "body" ).item(0).appendChild( tempParent );
+                return;
+            }
+        }
+
+        // if we get here, we're inside some container bounds, so we do
+        // everything the original dragsort script did to swap us into the
+        // correct position
+
+        var parent = this.parentNode;
+
+        var item = this;
+        var next = DragUtils.nextItem(item);
+        while (next != null && this.offsetTop >= next.offsetTop - 2) {
+            var item = next;
+            var next = DragUtils.nextItem(item);
+        }
+        if (this != item) {
+            DragUtils.swap(this, next);
+            return;
+        }
+
+        var item = this;
+        var previous = DragUtils.previousItem(item);
+        while (previous != null && this.offsetTop <= previous.offsetTop + 2) {
+            var item = previous;
+            var previous = DragUtils.previousItem(item);
+        }
+        if (this != item) {
+            DragUtils.swap(this, item);
+            return;
+        }
+    },
+
+    onDragEnd : function(nwPosition, sePosition, nwOffset, seOffset) {
+        // if the drag ends and we're still outside all containers
+        // it's time to remove ourselves from the document or add
+                // to the trash bin
+        if (this.isOutside) {
+            var container = DragDrop.firstContainer;
+            while (container != null) {
+               if (container.id == parent_id) {
+                 break;
+               }
+               container = container.nextContainer;
+            }
+            this.isOutside = false;
+            this.parentNode.removeChild( this );
+            container.appendChild( this );
+            this.style["top"] = "0px";
+            this.style["left"] = "0px";
+            //var container = DragDrop.firstContainer;
+            //container.appendChild( this );
+            return;
+        }
+        this.parentNode.onDragOut();
+        this.parentNode.onDragDrop();
+        this.style["top"] = "0px";
+        this.style["left"] = "0px";
+    }
+};
+
+var DragUtils = {
+    swap : function(item1, item2) {
+        var parent = item1.parentNode;
+        parent.removeChild(item1);
+        parent.insertBefore(item1, item2);
+
+        item1.style["top"] = "0px";
+        item1.style["left"] = "0px";
+    },
+
+    nextItem : function(item) {
+        var sibling = item.nextSibling;
+        while (sibling != null) {
+            if (sibling.nodeName == item.nodeName) return sibling;
+            sibling = sibling.nextSibling;
+        }
+        return null;
+    },
+
+    previousItem : function(item) {
+        var sibling = item.previousSibling;
+        while (sibling != null) {
+            if (sibling.nodeName == item.nodeName) return sibling;
+            sibling = sibling.previousSibling;
+        }
+        return null;
+    }
+};
+
+/*************************
+ * Custom Serendipity code
+ *************************/
+
+function pluginMoverInit() {
+    var list = document.getElementById("left_col");
+    DragDrop.makeListContainer(list, 'g1');
+    list.onDragOver = function() { this.style["border"] = "1px solid #4d759b"; };
+    list.onDragOut = function() { this.style["border"] = "none"; };
+
+    list = document.getElementById("hide_col");
+    DragDrop.makeListContainer(list, 'g1');
+    list.onDragOver = function() { this.style["border"] = "1px solid #4d759b"; };
+    list.onDragOut = function() {this.style["border"] = "none"; };
+
+    list = document.getElementById("right_col");
+    DragDrop.makeListContainer(list, 'g1');
+    list.onDragOver = function() { this.style["border"] = "1px solid #4d759b"; };
+    list.onDragOut = function() {this.style["border"] = "none"; };
+}
+
+function pluginMoverInitEvent() {
+    var list = document.getElementById("event_col");
+    DragDrop.makeListContainer(list, 'g1');
+    list.onDragOver = function() { this.style["border"] = "1px solid #4d759b"; };
+    list.onDragOut = function() { this.style["border"] = "none"; };
+
+    list = document.getElementById("eventh_col");
+    DragDrop.makeListContainer(list, 'g1');
+    list.onDragOver = function() { this.style["border"] = "1px solid #4d759b"; };
+    list.onDragOut = function() {this.style["border"] = "none"; };
+}
+
+function pluginMovergetSort() {
+  order = document.getElementById("order");
+  order.value = DragDrop.serData('g1', null);
+  return order.value;
+}
+
+function pluginMovergetSortEvent() {
+  order = document.getElementById("eventorder");
+  order.value = DragDrop.serData('g1', null);
+  return order.value;
+}
+
+/**************************************************
+ * dom-drag.js
+ * 09.25.2001
+ * www.youngpup.net
+ **************************************************
+ * 10.28.2001 - fixed minor bug where events
+ * sometimes fired off the handle, not the root.
+ **************************************************/
+
+var DOMDrag = {
+
+    obj : null,
+
+    init : function(o, oRoot, minX, maxX, minY, maxY, bSwapHorzRef, bSwapVertRef, fXMapper, fYMapper) {
+        o.onmousedown = DOMDrag.start;
+
+        o.hmode       = bSwapHorzRef ? false : true ;
+        o.vmode       = bSwapVertRef ? false : true ;
+
+        o.root = oRoot && oRoot != null ? oRoot : o ;
+
+        if (o.hmode  && isNaN(parseInt(o.root.style.left  ))) o.root.style.left   = "0px";
+        if (o.vmode  && isNaN(parseInt(o.root.style.top   ))) o.root.style.top    = "0px";
+        if (!o.hmode && isNaN(parseInt(o.root.style.right ))) o.root.style.right  = "0px";
+        if (!o.vmode && isNaN(parseInt(o.root.style.bottom))) o.root.style.bottom = "0px";
+
+        o.minX  = typeof minX != 'undefined' ? minX : null;
+        o.minY  = typeof minY != 'undefined' ? minY : null;
+        o.max   = typeof maxX != 'undefined' ? maxX : null;
+        o.maxY  = typeof maxY != 'undefined' ? maxY : null;
+
+        o.xMapper = fXMapper ? fXMapper : null;
+        o.yMapper = fYMapper ? fYMapper : null;
+
+        o.root.onDragStart  = new Function();
+        o.root.onDragEnd    = new Function();
+        o.root.onDrag       = new Function();
+    },
+
+    start : function(e) {
+        var o = DOMDrag.obj = this;
+        e = DOMDrag.fixE(e);
+        var y = parseInt(o.vmode ? o.root.style.top  : o.root.style.bottom);
+        var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right );
+        o.root.onDragStart(x, y);
+
+        o.lastMouseX = e.clientX;
+        o.lastMouseY = e.clientY;
+
+        if (o.hmode) {
+            if (o.minX != null) o.minMouseX = e.clientX - x + o.minX;
+            if (o.maxX != null) o.maxMouseX = o.minMouseX + o.maxX - o.minX;
+        } else {
+            if (o.minX != null) o.maxMouseX = -o.minX + e.clientX + x;
+            if (o.maxX != null) o.minMouseX = -o.maxX + e.clientX + x;
+        }
+
+        if (o.vmode) {
+            if (o.minY != null) o.minMouseY    = e.clientY - y + o.minY;
+            if (o.maxY != null) o.maxMouseY    = o.minMouseY + o.maxY - o.minY;
+        } else {
+            if (o.minY != null) o.maxMouseY = -o.minY + e.clientY + y;
+            if (o.maxY != null) o.minMouseY = -o.maxY + e.clientY + y;
+        }
+
+        document.onmousemove = DOMDrag.drag;
+        document.onmouseup   = DOMDrag.end;
+
+        return false;
+    },
+
+    drag : function(e) {
+        e = DOMDrag.fixE(e);
+        var o = DOMDrag.obj;
+
+        var ey = e.clientY;
+        var ex = e.clientX;
+        var y  = parseInt(o.vmode ? o.root.style.top  : o.root.style.bottom);
+        var x  = parseInt(o.hmode ? o.root.style.left : o.root.style.right );
+        var nx, ny;
+
+        if (o.minX != null) ex = o.hmode ? Math.max(ex, o.minMouseX) : Math.min(ex, o.maxMouseX);
+        if (o.maxX != null) ex = o.hmode ? Math.min(ex, o.maxMouseX) : Math.max(ex, o.minMouseX);
+        if (o.minY != null) ey = o.vmode ? Math.max(ey, o.minMouseY) : Math.min(ey, o.maxMouseY);
+        if (o.maxY != null) ey = o.vmode ? Math.min(ey, o.maxMouseY) : Math.max(ey, o.minMouseY);
+
+        nx = x + ((ex - o.lastMouseX) * (o.hmode ? 1 : -1));
+        ny = y + ((ey - o.lastMouseY) * (o.vmode ? 1 : -1));
+
+        if (o.xMapper)      nx = o.xMapper(y)
+        else if (o.yMapper) ny = o.yMapper(x)
+
+        DOMDrag.obj.root.style[o.hmode ? "left" : "right"] = nx + "px";
+        DOMDrag.obj.root.style[o.vmode ? "top" : "bottom"] = ny + "px";
+        DOMDrag.obj.lastMouseX = ex;
+        DOMDrag.obj.lastMouseY = ey;
+
+        DOMDrag.obj.root.onDrag(nx, ny);
+        return false;
+    },
+
+    end : function() {
+        document.onmousemove = null;
+        document.onmouseup   = null;
+        DOMDrag.obj.root.onDragEnd(    parseInt(DOMDrag.obj.root.style[DOMDrag.obj.hmode ? "left" : "right"]),
+                                    parseInt(DOMDrag.obj.root.style[DOMDrag.obj.vmode ? "top" : "bottom"]));
+        DOMDrag.obj = null;
+    },
+
+    fixE : function(e) {
+        if (typeof e == 'undefined')        e = window.event;
+        if (typeof e.layerX == 'undefined') e.layerX = e.offsetX;
+        if (typeof e.layerY == 'undefined') e.layerY = e.offsetY;
+        return e;
+    }
+};
\ No newline at end of file
diff --git a/templates/default/imgedit.js b/templates/default/imgedit.js
new file mode 100644 (file)
index 0000000..8007fcb
--- /dev/null
@@ -0,0 +1,302 @@
+/**************************************************\r
+ * imgedit.js\r
+ * 2003-10-17\r
+ * www.sonnd.com / www.supergarv.de\r
+ *\r
+ * COPYRIGHT (C) BY sonnd / Garvin Hicking\r
+ * Published as GPL. Copyright notice has to stay in effect.\r
+ **************************************************/\r
+\r
+// Gets a position of an element on a certain axis\r
+function imgedit_position(element, axis) {\r
+    if (axis == 'x') {\r
+        return (element.x) ? element.x : imgedit_subposition(element, 'Left');\r
+    } else {\r
+        return (element.y) ? element.y : imgedit_subposition(element, 'Top');\r
+    }\r
+}\r
+\r
+// Calculate possible referenced subpositions to really get the absolute position.\r
+function imgedit_subposition(element, axis) {\r
+    currentPos = 0;\r
+    while (element != null) {\r
+        currentPos += element['offset' + axis];\r
+        element     = element.offsetParent;\r
+    }\r
+\r
+    return currentPos;\r
+}\r
+\r
+// Places the cropping area to a certain X/Y coordinate. Then clips the overlay picture correspondingly\r
+function imgedit_placeArea(new_x, new_y) {\r
+    o_area.style.left    = new_x + 'px';\r
+    o_area.style.top     = new_y + 'px';\r
+    o_overlay.style.clip = "rect(" + (new_y+area_border) + " " + (new_x+inner_area_x) + " " + (new_y+inner_area_y) + " " + (new_x+area_border) + ")";\r
+}\r
+\r
+// Set correct restraints of the cropping area inside which it can move\r
+function imgedit_setMax(new_width, new_height) {\r
+    o_area.maxX = imgedit_getMax('x', new_width, area_orientation);\r
+    o_area.maxY = imgedit_getMax('y', new_height, area_orientation);\r
+}\r
+\r
+// Toggle the current area orientation to the opposite one\r
+function imgedit_areaOrientation() {\r
+    if (area_orientation == 'h') {\r
+        imgedit_toggleAreaOrientation('v');\r
+    } else {\r
+        imgedit_toggleAreaOrientation('h');\r
+    }\r
+\r
+    return false;\r
+}\r
+\r
+// Toggle the current area orientation\r
+function imgedit_toggleAreaOrientation(new_orientation) {\r
+    if (new_orientation == area_orientation) {\r
+        return;\r
+    }\r
+\r
+    // Display the corresponding cropping area and hide the other one.\r
+    if (new_orientation == 'h') {\r
+        area_orientation         = 'h';\r
+        o_area                   = o_harea;\r
+        area_x                   = harea_x;\r
+        area_y                   = harea_y;\r
+        inner_area_x             = inner_harea_x;\r
+        inner_area_y             = inner_harea_y;\r
+\r
+        o_varea.style.visibility = 'hidden';\r
+        o_area.style.left        = o_varea.style.left;\r
+        o_area.style.top         = o_varea.style.top;\r
+        o_area.style.visibility  = 'visible';\r
+    } else {\r
+        area_orientation         = 'v';\r
+        o_area                   = o_varea;\r
+        area_x                   = varea_x;\r
+        area_y                   = varea_y;\r
+        inner_area_x             = inner_varea_x;\r
+        inner_area_y             = inner_varea_y;\r
+\r
+        o_harea.style.visibility = 'hidden';\r
+        o_area.style.left        = o_harea.style.left;\r
+        o_area.style.top         = o_harea.style.top;\r
+        o_area.style.visibility  = 'visible';\r
+    }\r
+\r
+    // Set the new clipping inside the cropping area\r
+    imgedit_setMax(o_backdrop.width, o_backdrop.height);\r
+    o_overlay.style.clip = "rect(" + (parseInt(o_area.style.top)+area_border) + " " + (parseInt(o_area.style.left)+inner_area_x) + " " + (parseInt(o_area.style.top)+inner_area_y) + " " + (parseInt(o_area.style.left)+area_border) + ")";\r
+}\r
+\r
+// Zoom the image. Takes a given stepping (can be negative)\r
+function imgedit_zoom(step) {\r
+    pos = parseInt(o_zoombutton.style.top);\r
+    if (pos+step > slider_top && pos+step < slider_bottom) {\r
+        imgedit_zoomTo(position(o_zoombutton, 'y') + step);\r
+        o_zoombutton.style.top = pos + step + 'px';\r
+    }\r
+\r
+    return false;\r
+}\r
+\r
+// Automatically resize the window to fit cropping area\r
+function imgedit_autoSize(flip) {\r
+\r
+    // First find the largest side\r
+    if (real_x > real_y) {\r
+        // The image is a horizontal one. Resize height to fit.\r
+        fitmode = 'height';\r
+    } else {\r
+        // The image is a vertical one. Resize width to fit.\r
+        fitmode = 'width';\r
+    }\r
+\r
+    // Check if the size should be flipped. If it is 'true' the image will completely fit inside the cropping area.\r
+    // If it is 'false', it will only fit one side inside the cropping area\r
+    if (flip == 'true') {\r
+        if (fitmode == 'width') {\r
+            fitmode = 'height';\r
+        } else {\r
+            fitmode = 'width';\r
+        }\r
+    }\r
+\r
+    // Get new width/height of the image\r
+    if (fitmode == 'width') {\r
+        new_width  = inner_area_x - area_border;\r
+        ratio      = new_width / real_x;\r
+        new_height = real_y * ratio;\r
+    } else {\r
+        new_height = inner_area_y - area_border;\r
+        ratio      = new_height / real_y;\r
+        new_width  = real_x * ratio;\r
+    }\r
+\r
+    // Place cropping area to (0|0), because the image has been resized?\r
+    imgedit_scaleIMG(new_width, new_height);\r
+    imgedit_placeArea(-area_border, -area_border);\r
+\r
+    // Place the slider to corresponding ratio.\r
+    o_zoombutton.style.top = slider_bottom - parseInt(ratio/2 * (slider_middle/3)) + 'px';\r
+\r
+    // Adjust some settings inside the HTML form.\r
+    document.getElementById('scaletext').style.visibility = 'visible';\r
+    document.getElementById('autoguess_clicked').value = 'true';\r
+    new_ratio = ratio;\r
+\r
+    return false;\r
+}\r
+\r
+// Get the maximum width/height for a certain axis the cropping area is allowed to move to\r
+function imgedit_getMax(axis, pixels, area_orientation) {\r
+\r
+    // Which is the size we should get?\r
+    if (area_orientation == 'h') {\r
+        maxarea_x = harea_x;\r
+        maxarea_y = harea_y\r
+    } else if (area_orientation == 'v') {\r
+        maxarea_x = varea_x;\r
+        maxarea_y = varea_y\r
+    } else {\r
+        maxarea_x = area_x;\r
+        maxarea_y = area_y\r
+    }\r
+\r
+    if (axis == 'x') {\r
+        value = pixels - maxarea_x + area_border;\r
+    } else {\r
+        value = pixels - maxarea_y + area_border;\r
+    }\r
+\r
+    if (value < -area_border) {\r
+        value = -area_border;\r
+    }\r
+\r
+    return value;\r
+}\r
+\r
+// Scales the background image to a certain size\r
+function imgedit_scaleIMG(new_width, new_height) {\r
+    o_backdrop.width = new_width;\r
+    o_backdrop.height = new_height;\r
+\r
+    o_overlay.width = new_width;\r
+    o_overlay.height = new_height;\r
+\r
+    imgedit_setMax(new_width, new_height);\r
+\r
+    return true;\r
+}\r
+\r
+// Zooms the image to a certain stepping\r
+function imgedit_zoomTo(y) {\r
+    current = slider_bottom - y;\r
+\r
+    temp_height = current - slider_middle;\r
+    temp_ratio = temp_height / (slider_middle*3);\r
+\r
+    if (current > slider_middle) {\r
+        // make smaller than 100%\r
+        new_ratio = 1 + (temp_ratio+temp_ratio);\r
+    } else {\r
+        // make larger than 100%\r
+        new_ratio = 1 + (temp_ratio+temp_ratio);\r
+    }\r
+\r
+    new_width = parseInt(real_x * new_ratio);\r
+    new_height = parseInt(real_y * new_ratio);\r
+\r
+    imgedit_scaleIMG(new_width, new_height);\r
+\r
+    return true;\r
+}\r
+\r
+// OnSubmit catch. Parses current JS values into the form\r
+function imgedit_getCoordinates() {\r
+    document.getElementById('zoombox_x').value        = parseInt(o_area.style.left);\r
+    document.getElementById('zoombox_y').value        = parseInt(o_area.style.top);\r
+    document.getElementById('zoombox_factor').value   = new_ratio;\r
+    document.getElementById('area_orientation').value = area_orientation;\r
+\r
+    return true;\r
+}\r
+\r
+// Initializes everything\r
+function imgedit_init(zoombox_width, init_area_border, pad_left, pad_top, init_area_orientation) {\r
+    // Store objects\r
+    o_backdrop   = document.getElementById("backdrop");\r
+    o_overlay    = document.getElementById("overlay");\r
+    o_harea      = document.getElementById("harea");\r
+    o_varea      = document.getElementById("varea");\r
+    o_zoombutton = document.getElementById("zoombutton");\r
+\r
+    // Object sizes\r
+    full_x = parseInt(o_backdrop.width);\r
+    full_y = parseInt(o_backdrop.height);\r
+\r
+    real_x = document.getElementById('real_img_width').value;\r
+    real_y = document.getElementById('real_img_height').value;\r
+\r
+    area_border  = init_area_border;\r
+\r
+    harea_x = parseInt(o_harea.width);\r
+    harea_y = parseInt(o_harea.height);\r
+\r
+    varea_x = parseInt(o_varea.width);\r
+    varea_y = parseInt(o_varea.height);\r
+\r
+    inner_harea_x = harea_x - area_border;\r
+    inner_harea_y = harea_y - area_border;\r
+\r
+    inner_varea_x = varea_x - area_border;\r
+    inner_varea_y = varea_y - area_border;\r
+\r
+    new_ratio = document.getElementById('zoombox_factor').value;\r
+\r
+    slider_width = 10;\r
+    slider_top = 0;\r
+    slider_bottom = 95;\r
+    slider_space = slider_bottom - slider_top;\r
+    slider_middle = slider_space / 2;\r
+    zoombox_left = -(o_zoombutton.width/2) + (slider_width/2);\r
+\r
+    // Make objects dragabble\r
+    DOMDrag.init(o_harea,      null, -area_border, imgedit_getMax('x', full_x, 'h'), -area_border, imgedit_getMax('y', full_y, 'h'));\r
+    DOMDrag.init(o_varea,      null, -area_border, imgedit_getMax('x', full_x, 'v'), -area_border, imgedit_getMax('y', full_y, 'v'));\r
+    DOMDrag.init(o_zoombutton, null, zoombox_left, zoombox_left, slider_top, slider_bottom);\r
+\r
+    o_harea.onDrag = function (x, y) {\r
+        o_overlay.style.clip = "rect(" + (y+area_border) + " " + (x+inner_harea_x) + " " + (y+inner_harea_y) + " " + (x+area_border) + ")";\r
+    }\r
+\r
+    o_varea.onDrag = function (x, y) {\r
+        o_overlay.style.clip = "rect(" + (y+area_border) + " " + (x+inner_varea_x) + " " + (y+inner_varea_y) + " " + (x+area_border) + ")";\r
+    }\r
+\r
+    o_zoombutton.onDrag = function (x, y) {\r
+        imgedit_zoomTo(y);\r
+    }\r
+\r
+    o_zoombutton.style.left       = zoombox_left + 'px';\r
+\r
+    zf = document.getElementById('zoombox_factor').value;\r
+    if (zf != 1) {\r
+        o_zoombutton.style.top        = slider_bottom - parseInt(zf/2 * (slider_middle/3)) + 'px';\r
+    } else {\r
+        o_zoombutton.style.top        = slider_top + slider_middle + 'px';\r
+    }\r
+\r
+    o_zoombutton.style.visibility = 'visible';\r
+\r
+    o_harea.style.cursor = 'move';\r
+    o_harea.style.left   = pad_left + 'px';\r
+    o_harea.style.top    = pad_top  + 'px';\r
+\r
+    o_varea.style.cursor = 'move';\r
+    o_varea.style.left   = pad_left + 'px';\r
+    o_varea.style.top    = pad_top  + 'px';\r
+\r
+    area_orientation     = '';\r
+    imgedit_toggleAreaOrientation(init_area_orientation);\r
+}
\ No newline at end of file