/*
    Getme Ltd reserve all intellectual property rights of this source code.
    
    EXCEPT AS OTHERWISE EXPRESSLY PROVIDED IN A WRITTEN AGREEMENT BETWEEN YOU 
    AND GETME LTD, THIS SOURCE CODE IS PROVIDED "AS IS" AND WITHOUT ANY 
    WARRANTIES OF ANY KIND, INCLUDING WARRANTIES OF MERCHANTABILITY OR FITNESS 
    FOR A PARTICULAR PURPOSE.

    IN NO EVENT SHALL GETME LTD BE LIABLE FOR ANY DIRECT, INDIRECT, 
    INCIDENTIAL, SPECIAL OR CONSEQUENTIAL DAMAGES OR DAMAGES FROM BUSINESS 
    INTERRUPTION OR LOSS OF PROFITS, REVENUE, DATA OR USE, INCURRED BY YOU OR 
    ANY THIRD PARTY, WHETHER IN CONTRACT OR TORT, ARISING FROM YOUR ACCESS TO, 
    OR USE OF, THIS SOURCE CODE EVEN IF GETME LTD HAS BEEN ADVISED OF THE 
    POSSIBILITY OF SUCH DAMAGES.
    
    Copyright (c)2009 Getme Ltd.
 */

/**
 * @fileoverview    Real-time web browser editing.
 * @version         0.0.3
 * @author          Anthony Blackshaw ant@getme.co.uk
 * @requires        IXMO, jQuery, jQuery.ui (draggable, droppable & resizable), 
 * SELECT
 * @alpha           EDIT, NODE
 */


// Dependency check(s)
if (window.IXMO===undefined) {
    throw Error('Inline XHTML Manipulation & Optimisation library (ixmo.js) is required.');
}

if (window.jQuery===undefined) {
    throw Error('JQuery JavaScript framework (http://jquery.com/) is required.');
}

if (window.jQuery.ui.draggable===undefined) {
    throw Error('JQuery JavaScript UI draggable plugin (http://jqueryui.com/) is required.');
}

if (window.jQuery.ui.droppable===undefined) {
    throw Error('JQuery JavaScript UI droppable plugin (http://jqueryui.com/) is required.');
}

if (window.jQuery.ui.resizable===undefined) {
    throw Error('JQuery JavaScript UI resizable plugin (http://jqueryui.com/) is required.');
}

if (window.SELECT===undefined) {
    throw Error('Cross-browser content selection library (select.js) is required.');
}

if(!Array.indexOf) {
    /**
     * Ensure indexOf method is supported (cheers for this IE)
     * @ignore
     */    
    Array.prototype.indexOf = function(obj) {
        for(var i=0; i<this.length; i++) {
            if(this[i]==obj) {
                return i;
            }
        }
        return -1;
    }
}


/**
 * The EDITABLE namespace.
 * The real-time web browser editing library uses EDITABLE to namespace it's
 * classes and functions.
 * @final
 * @type Namespace
 */  
var EDITABLE = {};

/**
 * A handle to the single(ton) instance of the Settings class.
 * @type EDITABLE.Settings
 * @private
 */
EDITABLE._settings = null;

/**
 * A puesdo ID counter used by the default 'request_id' method which 
 * typically would be overwritten to request an ID from the remote server.
 * Useful when using the editor stand alone or as a demo.
 * @type Number
 * @private
 */
EDITABLE._puesdo_id_counter = 0;

/**
 * Return a handle to the Single(ton) instance of the Settings class.
 * @return A handle to the Single(ton) instance of the Settings class.
 * @type EDITABLE.Settings
 */
EDITABLE.get_settings = function() {
    return new EDITABLE.Settings();
};

/**
 * Returns a unique ID that can be used to identify an editable Node within the
 * HTML DOM and typically on a remote server (dependant on the load/save 
 * framework implementation). This method may be overridden to provide an ID 
 * generated on a remote server (in such cases the recommended method is use 
 * an ID pool of X IDs that needs updating infrequesntly). The default 
 * implementation provides a simple automatically incrementing ID, which is 
 * useful for demonstration purposes and for snapshot load/save frameworks.
 * @return A unique ID for an editable Node
 * @type Number
 */
EDITABLE.request_id = function() {
    EDITABLE._puesdo_id_counter++;
    return String(EDITABLE._puesdo_id_counter);
}

/**
 * Create a settings manager for editing. (Imporant: This class behaves as a 
 * singleton).
 * @class A class to manage edit settings.
 * @constructor
 */  
EDITABLE.Settings = function() {
    // Define singleton behaviour
    if (EDITABLE._settings!=null) {
        return EDITABLE._settings;
    }
    EDITABLE._settings = this;
   
    /**
     * At any one time there can be multiple editable Collections in existance,
     * this holds a handle to the currently selected Collection.
     * @type EDITABLE.Collection
     * @private
     */
    this._selected_collection = null;

    // This is currently an experimental approach to providing key down/up 
    // information to Node types that do not support keyboard focus.
    
    /**
     * A flag that determines if we can safely steal key events.
     * @type Boolean
     * @private
     */    
    this._can_steal_key_events = true;

    // We don't want to steal key events when an input element is focused
    $(':input').bind('focus',this,function(ev) { ev.data._can_steal_key_events=false; });
    $(':input').bind('blur',this,function(ev) { ev.data._can_steal_key_events=true; });
    
    $(document).bind('keydown',this,function(ev) {
        if (ev.data._can_steal_key_events) {
            var selected_collection = ev.data.get_selected_collection();
            if (selected_collection) {
                var selected_node = selected_collection.get_selected_node();
                // If we have a selected node only force the event if it does
                // not support for keyboard focus.
                if (selected_node && !selected_node._can_focus) {
                    return selected_node._key_down(ev);
                }
            }
        }
    });
    $(document).bind('keyup',this,function(ev) {
        if (ev.data._can_steal_key_events) {
            var selected_collection = ev.data.get_selected_collection();
            if (selected_collection) {
                var selected_node = selected_collection.get_selected_node();
                // If we have a selected node only force the event if it does
                // not support for keyboard focus.
                if (selected_node && !selected_node._can_focus) {
                    return selected_node._key_up(ev);
                }
            }
        }
    });
}

/**
 * Set a Collection as currently selected.
 * @param {EDITABLE.Collection} collection The Collection to selected.
 * @private
 */
EDITABLE.Settings.prototype._set_selected_collection = function(collection) {
    if (this._selected_collection!=collection) {
        var prev_collection = this._selected_collection;
        if (prev_collection!=null) {
            prev_collection.trigger('unselect',{'collection':collection});
        }
        this._selected_collection = collection;
        if (collection!=null) {
            collection.trigger('select',{'collection':prev_collection});
        }
    }
}

/**
 * Get the currently selected Collection.
 * @return The currently selected Collection.
 * @type EDITABLE.Collection
 */
EDITABLE.Settings.prototype.get_selected_collection = function() {
    return this._selected_collection;
}

/**
 * Create a manager class for a Collection of editable Nodes. 
 * @class A class to manage a Collection of editable Nodes.
 * @param {Element} parent_node The parent level block node that will contain 
 * the list editable Nodes (must be a 'div' or 'body' element).
 * @constructor
 */  
EDITABLE.Collection = function(parent_node) {
    
    /**
     * A list of editable Nodes. 
     * @type Array
     * @private
     */
    this._node_list = new Array();
    
    /**
     * A count of the nodes, used to determine if the collection has been
     * modified by a deleted node.
     * @type Number
     * @private
     */
    this._node_count = 0;
    
    /**
     * The currently selected Node within the Collection. 
     * @type EDITABLE.Node
     * @private
     */
    this._selected_node = null;
    
    /**
     * A map of functions bound to events supported by the Collection that get
     * exectuted when an event is triggered.
     * @type Object
     * @private
     */
    this._event_map = new Object();
    
    // Ensure a valid parent node has been specified
    if (['body','div'].indexOf(parent_node.nodeName.toLowerCase())==-1) {
        throw Error('The parent node for a Collection must be a <body> or <div>.');
    }
    
    /**
     * The parent node within which all the Collections editable Nodes will be 
     * rendered.
     * @type Element
     */
    this.parent_node = parent_node;    
}

/**
 * Return a Collection pre-populated by the contents of the parent node.
 * @param {Element} parent_node The parent level block node that will contain 
 * the list editable Nodes (must be a 'div' or 'body' element).
 * @return The pre-populated collection
 * @type EDITABLE.Collection
 */
EDITABLE.Collection.from_node = function(node) {
    
    var collection = new EDITABLE.Collection(node);
    
    // Parse the existing content
    var child_list = $(node).children();
    $(node).html('');
    var node_list = new Array();
    for (var i=0; i<child_list.length; i++) {
        var child = child_list[i];
        if ($(child).hasClass('edit-text')) {
            node_list.push(EDITABLE.Text.from_node(collection, child));
        } else if ($(child).hasClass('edit-image')) {
            node_list.push(EDITABLE.Image.from_node(collection, child));
        } else if ($(child).hasClass('edit-movie')) {
            node_list.push(EDITABLE.Movie.from_node(collection, child));
        } 
        // ...
    }
    
    // Build the collection
    for (var j=0; j<node_list.length; j++) {
        collection.add_node(node_list[j]);
    }
    
    // Update the node count
    collection._node_count = node_list.length;
    
    return collection;
}

/**
 * Return true if any Node in the Collection is modified.
 * @return Whether any Node in the Collection is modified.
 * @type Boolean
 */
EDITABLE.Collection.prototype.is_modified = function() {
    if (this._node_list.length != this._node_count) {
        return true;
    }
    for (var i=0; i<this._node_list.length; i++) {
        if (this._node_list[i].is_modified()){
            return true;
        }
    }
    return false;
}

/**
 * Commit any changes to Nodes in this collection.
 */
EDITABLE.Collection.prototype.commit = function() {
    for (var i=0; i<this._node_list.length; i++) {
        if (this._node_list[i].is_modified()){
            this._node_list[i].commit() ;
        }
    }
}

/**
 * Return the Collection as HTML.
 * @return The Collection as a HTML String. 
 * @type String
 */
EDITABLE.Collection.prototype.to_html = function() { 
    var html = '';
    for (var i=0; i<this._node_list.length; i++) {
        html += this._node_list[i].to_html();
    }
    return html;
}

// Node selection

/**
 * Set the a Node as currently selected in this Collection.
 * @param {EDITABLE.Node} node The Node to select.
 * @private
 */
EDITABLE.Collection.prototype._set_selected_node = function(node) {
    if (this._selected_node!=node) {
        var prev_node = this._selected_node;
        if (prev_node!=null) {
            prev_node.unselect();
        }
        this.trigger('node_change',{'pre_node':prev_node,'node':node});
        this._selected_node = node;
    }
    if (node==null) {
        if (this==EDITABLE.get_settings().get_selected_collection()) {
            EDITABLE.get_settings()._set_selected_collection(null);
        }
    } else {
        if (this!=EDITABLE.get_settings().get_selected_collection()) {
            EDITABLE.get_settings()._set_selected_collection(this);
        }        
    }
}

/**
 * Get the currently selected Node in this Collection.
 * @return The currently selected Node.
 * @type EDITABLE.Node
 */
EDITABLE.Collection.prototype.get_selected_node = function() {
    return this._selected_node;
}

/**
 * Get all Nodes, or all Nodes of a type.
 * @param {String} Optional. The type of Node to return; only Nodes of this 
 * will be specifiedtype
 * @return The list of (matching) Nodes.
 * @type Array
 */
EDITABLE.Collection.prototype.get_nodes = function(node_type) {
    if (node_type) {
        var node_list = new Array();
        for (var i=0; i<this._node_list.length; i++) {
            var node = this._node_list[i];
            if (node.is_a(node_type)) {
                node_list.push(node);
            }
        }
        return node_list;
    } else {
        return this._node_list.slice(0);
    }
}

/**
 * Get a Node after another Node in this Collection.
 * @param {EDITABLE.Node} node The Node to select a Node after.
 * @param {String} Optional. The type of Node to return; if specified first 
 * node after the specified Node that matches the specified type will be 
 * returned. 
 * @return The Node (matching) after the specified Node.
 * @type EDITABLE.Node
 */
EDITABLE.Collection.prototype.get_node_after = function(node,node_type) {
    var pos = this._node_list.indexOf(node);
    if (pos==-1) {
        throw Error('The specified Node is not part of this Collection.');
    }
    // Find the (matching) node
    var after_node = null;
    while(pos<this._node_list.length-1) {
        node = this._node_list[pos+1];
        if (node_type) {
            // Matching
            if (node.is_a(node_type)) {
                after_node = node;
                break;
            }
        } else {
            after_node = node;
            break;
        }
        pos++;
    }
    return after_node;
}

/**
 * Get a Node before another Node in this Collection.
 * @param {EDITABLE.Node} node The Node to select a Node before.
 * @param {String} Optional. The type of Node to return; if specified first 
 * node before the specified Node that matches the specified type will be 
 * returned. 
 * @return The Node (matching) before the specified Node.
 * @type EDITABLE.Node
 */
EDITABLE.Collection.prototype.get_node_before = function(node,node_type) {
    var pos = this._node_list.indexOf(node);
    if (pos==-1) {
        throw Error('The specified Node is not part of this Collection.');
    }
    // Find the (matching) node
    var before_node = null;
    while(pos>0) {
        node = this._node_list[pos-1];
        if (node_type) {
            // Matching
            if (node.is_a(node_type)) {
                before_node = node;
                break;
            }
        } else {
            before_node = node;
            break;
        }
        pos--;
    }
    return before_node;
}

// Event management

/**
 * Bind a function to an event. Supported events are;
 * <ul>
 *     <li>select - triggered when the Collection is selected.</li>
 *     <li>unselect - triggered when the Collection is unselected.</li>
 *     <li>add_node - triggered when a node is added to the Collection.</li>
 *     <li>remove_node - triggered when a Node is removed from the Collection.
 * </li>
 *     <li>move_node - triggered when a Node is moved in the Collection.</li>
 *     <li>node_change - triggered when the a different Node is selected
 * within the Collection.</li>
 *     <li>node_click - triggered when the a Node within the collection is
 * clicked</li>
 *     <li>node_keyup - triggered when a keyup event occurs for the
 * selected Node within the the Collection.</li>
 *     <li>node_keypress - triggered when a keypress event occurs for the
 * selected Node within the the Collection.</li>
 *     <li>content_change - triggered when the contents of any Node in the 
 * Collection changes.</li>
 *     <li>format_change - triggered when the format of any Node in the 
 * Collection changes.</li>
 *     <li>resiz_node - triggered when the size of any image Node in the 
 * Collection changes.</li>
 * </ul>
 * @param {String} event The name of the event to bind to.
 * @param {Object} data Additional data passed to the event handler as 
 * 'event.data'.
 * @param {Function} func A function called whenever the event is triggered. 
 */
EDITABLE.Collection.prototype.bind = function(event, data, func) {
    if (!this._event_map[event]) {
        this._event_map[event] = new Array();
    }
    this._event_map[event].push([data,func]);
}

/**
 * Unbind a function or all functions from an event or all events.
 * @param {String} event The event to remove a function/all functions from.
 * @param {Function} func The function to remove from an event (if not 
 * specified all functions are removed from the event).
 */
EDITABLE.Collection.prototype.unbind = function(event, func) {
    if (event) {
        if (func) {
            if (this._event_map[event]) {
                var clean_event_list = new Array();
                for (var i=0; i<this._event_map[event].length; i++ ) {
                    if (this._event_map[event][i][1]!=func) {
                        clean_event_list.push(this._event_map[event][i]);
                    }
                }
                this._event_map[event] = clean_event_list;
            }
        } else {
            delete this._event_map[event];
        }
    } else {
        for (var event in this._event_map) {
            this.unbind(event,func);
        }
    }
}

/**
 * Trigger and event.
 * @param {String} event The event to trigger.
 * @param {Object} data An object containing data about the event.
 */
EDITABLE.Collection.prototype.trigger = function(event, data) {
    if (this._event_map[event]) {
        for (var i=0; i<this._event_map[event].length; i++) {
            data.data = this._event_map[event][i][0];
            this._event_map[event][i][1](data);
        }
    }
}

// Node manipulation

/**
 * Add an Node to the Collection.
 * @param {EDITABLE.Node} node The Node to add to the Collection.
 * @param {EDITABLE.Node} pre_node Optional. The Node to insert the new Node 
 * before or after. If not specified the Node will be added to the end of the
 * Collection.
 * @param {String} insert Optional. A string specifying the insert instruction.
 * A value of 'before' indicates the new Node should be insert before the 
 * pre-Node, a value of 'after' that it should be inserted after the pre-Node.
 * By default the new Node is inserted after the pre-Node.
 */
EDITABLE.Collection.prototype.add_node = function(node, pre_node, insert) {
    // Ensure the Node is not already part of the Collection
    if (this._node_list.indexOf(node)!=-1) {
        throw Error('The specified Node is already part of this Collection. Nodes should exist once in one Collection.');
    }
    // Add the node to the node list
    if (pre_node) {
        var insert_pos = this._node_list.indexOf(pre_node);
        if (insert_pos==-1) {
            throw Error('The specified pre-Node is not part of this Collection.');
        }   
        if (insert=='before') {
            if (insert_pos==0) {
                this._node_list.unshift(node);
            } else {
                this._node_list.splice(insert_pos,0,node);
            }
        } else {
            if (insert_pos==this._node_list.length) {
                this._node_list.push(node);
            } else {
                this._node_list.splice(insert_pos+1,0,node);
            }
        }
    } else {
        this._node_list.push(node);
    }
    // Render the node
    node.render();
    // Trigger the event
    this.trigger('add_node',{'node':node});
}

/**
 * Move a Node in the Collection to after/before another Node.
 * @param {EDITABLE.Node} node The Node to move in the Collection.
 * @param {EDITABLE.Node} pre_node The pre-Node to move the Node before or 
 * after. If not specified the Node will be moved to the end of the Collection.
 * @param {String} insert Optional. A string specifying the insert instruction.
 * A value of 'before' indicates the new Node should be insert before the 
 * pre-Node, a value of 'after' that it should be inserted after the pre-Node.
 * By default the new Node is inserted after the pre-Node.
 */
EDITABLE.Collection.prototype.move_node = function(node, pre_node, insert) {
    // Check the nodes exists
    if (this._node_list.indexOf(node)==-1) {
        throw Error('The specified Node is not part of this Collection.');
    }    
    if (this._node_list.indexOf(pre_node)==-1) {
        throw Error('The specified pre-Node is not part of this Collection.');
    }        
    // Cut the node out of the collection
    this._node_list.splice(this._node_list.indexOf(node),1);
    if (insert=='before') {
        this._node_list.splice(this._node_list.indexOf(pre_node),0,node);
        $(pre_node.node).before($(node.node));        
    } else {
        this._node_list.splice(this._node_list.indexOf(pre_node)+1,0,node);
        $(pre_node.node).after($(node.node));
    }
    this.trigger('move_node',{'node':node,'pre_node':node});
}

/**
 * Remove an Node from the Collection.
 * @param {EDITABLE.Node} node The node to remove from the collection.
 */
EDITABLE.Collection.prototype.remove_node = function(node) {
    // Check the node exists
    if (this._node_list.indexOf(node)==-1) {
        throw Error('The specified Node is not part of this Collection.');
    }
    // If this is the selected node first unselect it
    if (node==this.get_selected_node()) {
        this._set_selected_node(null);
    }
    // Remove the node from the collection
    this._node_list.splice(this._node_list.indexOf(node),1);
    // Clear the render from the HTML DOM
    node.unrender();
    this.trigger('remove_node',{'node':node});
}

/**
 * Add a Text Node to the collection.
 * @param {String} node_type The type of element to add (must be <h1-6> or 
 * <p>).
 * @param {String} content Optional. The Nodes initial content.
 * @param {Object} options The options for the Text Node.
 * @param {EDITABLE.Node} pre_node Optional. The Node to insert the new Node 
 * before or after. If not specified the Node will be added to the end of the
 * Collection.
 * @param {String} insert Optional. A string specifying the insert instruction.
 * A value of 'before' indicates the new Node should be insert before the 
 * pre-Node, a value of 'after' that it should be inserted after the pre-Node.
 * By default the new Node is inserted after the pre-Node.
 */
EDITABLE.Collection.prototype.add_text = function(element_type, content, options, pre_node, insert) {
    var text_node = new EDITABLE.Text(this,element_type,content,options);
    this.add_node(text_node,pre_node,insert);
    return text_node;
}

/**
 * Add an Image Node to the collection.
 * @param {String} src The location of the image.
 * @param {Array} size The size of the image (in pixels) as a two item Array 
 * [width,height].
 * @param {String} alt Optional. The alternative text for the image.
 * @param {Object} options The options for the Image Node.
 * @param {EDITABLE.Node} pre_node Optional. The Node to insert the new Node 
 * before or after. If not specified the Node will be added to the end of the
 * Collection.
 * @param {String} insert Optional. A string specifying the insert instruction.
 * A value of 'before' indicates the new Node should be insert before the 
 * pre-Node, a value of 'after' that it should be inserted after the pre-Node.
 * By default the new Node is inserted after the pre-Node.
 */
EDITABLE.Collection.prototype.add_image = function(src, size, alt, options, pre_node, insert) {
    var image_node = new EDITABLE.Image(this,src,size,alt,options);
    this.add_node(image_node,pre_node,insert);
    return image_node;
}

/**
 * Add a Movie Node to the collection.
 * @param {String} url A URL to the Movie.
 * @param {Array} size The size of the Movie (in pixels) as a two item Array 
 * (width, height).
 * @param {String} alt The alternative text for the Movie.
 * @param {Object} options The options for the Movie Node.
 * @param {EDITABLE.Node} pre_node Optional. The Node to insert the new Node 
 * before or after. If not specified the Node will be added to the end of the
 * Collection.
 * @param {String} insert Optional. A string specifying the insert instruction.
 * A value of 'before' indicates the new Node should be insert before the 
 * pre-Node, a value of 'after' that it should be inserted after the pre-Node.
 * By default the new Node is inserted after the pre-Node.
 */
EDITABLE.Collection.prototype.add_movie = function(url, size, alt, options, pre_node, insert) {
    var movie_node = new EDITABLE.Movie(this, url, size, alt, options);
    this.add_node(movie_node, pre_node, insert);
    return movie_node;
}

// Nodes

/**
 * Create a new editable Node.
 * @class A base editable Node class.
 * @param {EDITABLE.Collection} owner The Collection that owns this Node.
 * @param {Object} options Optional. A map of options to set intitially for the
 * Node.
 * @constructor
 */  
EDITABLE.Node = function(owner,options) {

    options = options || {};

    /**
     * The Nodes remote ID.
     * @type String
     * @private
     */
    this._id = EDITABLE.request_id();

    /**
     * The ID of the element representing the Node in the HTML DOM. 
     * @type String
     */
    this.element_id = 'editable-node-'+this._id;
    
    /**
     * A flag indicating if the Node has been modified. Useful for implementing
     * load/save frameworks. If the restored option is set to true then the 
     * Node is considered initially unmodified. Useful when restoring saved
     * data.
     * @type Boolean
     * @private
     */
    this._modified = true;
    if (options['restored']==true) {
        this._modifed = false;
    }

    /**
     * The Collection that this Node belongs to.
     * @type EDITABLE.Collection
     * @private
     */
    this._owner = owner;

    /**
     * A flag indicating if the node is selected.
     * @type Boolean
     * @private
     */
    this._selected = false;    
    
    /**
     * The alignment of the Node. Nodes or their content can be aligned 
     * relative to other Nodes in the collection.
     * @type String
     * @private
     */
    this._css_align = '';
    
    /**
     * A CSS class that applies to this Node. Nodes can have a single CSS class
     * applied to the them. The limitation of a single class is intentional, it
     * helps to keep formatting simple for users.
     * @type String
     * @private
     */
    this._css_style = '';
    
    // Important: Typically each type of Node will override the default 'css'
    // and 'align' methods to ensure only valid values can be set and to 
    // provide mapping of values such as 'left' to the correct but longer CSS
    // class name 'text-left'.
    
    /**
     * The physical node in the HTML DOM. 
     * @type Element
     */
    this.node = null;
}

/**
 * A list which contains the Nodes base type(s) and final type. Used to 
 * identify Nodes by type.
 * @type Array
 * @private
 */
EDITABLE.Node.prototype._is_a = ['base'];

/**
 * A flag which specifies whether the Node can receive focus.
 * @type Boolean
 * @private
 */
EDITABLE.Node.prototype._can_focus = false;

/**
 * Return true if the Node is of the specified type.
 * @param {String} node_type The type of Node to match against.
 * @return Whether the Node is of the specified type.
 * @type Boolean
 */
EDITABLE.Node.prototype.is_a = function(node_type) {
    return this._is_a.indexOf(node_type)!=-1;
} 

/**
 * Select the Node.
 */
EDITABLE.Node.prototype.select = function() {
    if (!this._selected) {
        this._selected=true;
        $(this.node).addClass('edit');
        $(this.node).removeClass('over');
        this._owner._set_selected_node(this);
    }
}

/**
 * Unselect the Node.
 */
EDITABLE.Node.prototype.unselect = function() {
    if (this._selected) {
        this._selected=false;
        $(this.node).removeClass('edit');
        this._owner._set_selected_node(null);
    }
}

/**
 * Return true if the Node is selected.
 * @return Whether the Node is selected.
 * @type Boolean
 */
EDITABLE.Node.prototype.is_selected = function() {
    return this._selected;
}

/**
 * Return the final Node type.
 * @return The final Node type.
 * @type String
 */
EDITABLE.Node.prototype.get_final_type = function() {
    return this._is_a[this._is_a.length-1];
}

/**
 * Return true if the Node is modified.
 * @return Whether the Node is modified or not.
 * @type Boolean
 */
EDITABLE.Node.prototype.is_modified = function() {
    return this._modified;
}

/**
 * Commit any changes to this Node.
 */
EDITABLE.Node.prototype.commit = function() {
    this._modified = false;
}

/**
 * Set/Get the css align class for this Node. If a class name is provided then
 * the CSS align class is set, else it is returned. To clear the CSS align 
 * class for the Node specify a class name of ''; 
 * @param {String} class_name Optional. The CSS align class to apply to the 
 * Node.
 */
EDITABLE.Node.prototype.align = function(class_name) {
    if (class_name===undefined) {
        return this._css_align;
    } else {
        if (this._css_align!=class_name) {
            $(this.node).removeClass(this._css_align);
            if (class_name) {
                $(this.node).addClass(class_name);
            }
            this._css_align = class_name;
            this._modified = true;
            this._owner.trigger('format_change',{'node':this,'attribute':'align'});
        }
    }
}

/**
 * Set/Get the CSS class for this Node. If a CSS class name is provided then 
 * the CSS class is set, else it is returned. To clear the CSS class for the 
 * Node specify a class name of ''; 
 * @param {String} class_name Optional. The CSS class to apply to the Node.
 */
EDITABLE.Node.prototype.style = function(class_name) {
    if (class_name===undefined) {
        return this._css_style;
    } else {
        if (this._css_style!=class_name) {
            if (class_name) {
                $(this.node).addClass(class_name);
            } else {
                $(this.node).removeClass(this._css_style);
            }
            this._css_style = class_name;
            this._modified = true;
            this._owner.trigger('format_change',{'node':this,'attribute':'style'});
        }
    }
}


/**
 * Return the Node as HTML. The base Node has no representation and so the
 * method returns an empty String.
 * @return The Node as a HTML String. 
 * @type String
 */
EDITABLE.Node.prototype.to_html = function() { 
    return '';
}

/**
 * Render the Node to the HTML DOM. The base Node has no representation and so
 * the method does nothing.
 */
EDITABLE.Node.prototype.render = function() {}

/**
 * Remove the rendered Node from the HTML DOM.
 */
EDITABLE.Node.prototype.unrender = function() {
    // Remove JQuery events
    $('#'+this.element_id).unbind();
    // Remove the node from the HTML DOM
    $('#'+this.element_id).remove();
    // Clear any existing selection
    SELECT.clear();
}

/**
 * Return a copy of the Node.
 * @return A copy of the Node
 * @type EDITABLE.Node
 */
EDITABLE.Node.prototype.copy = function(owner) {
    var copy_node = new EDITABLE.Node(owner);
    return copy_node;
} 

// Puesdo event handling for non-focusable Node types

/**
 * Key down event.
 */
EDITABLE.Node.prototype._key_down = function(ev) {}

/**
 * Key up event.
 */
EDITABLE.Node.prototype._key_up = function(ev) {}


/**
 * Create a new editable Text Node.
 * @class An editable Text Node. 
 * Supported align values are 'text-(left|center|right|justify)'.
 * @param {EDITABLE.Collection} owner The Collection that owns this Text Node.
 * @param {String} element_type The type of element.
 * @param {Content} The initial content of the node, if not specified the nodes
 * existing content will be used, if the Text Node has no content the value of
 * EDITABLE.Text.DEFAULT_TEXT will be used.
 * @param {Object} options Optional. A map of options to set intitially for the
 * Node. 
 * @constructor
 */
EDITABLE.Text = function(owner, element_type, content, options) {
    
    options = options || {};
    EDITABLE.Node.call(this,owner,options);

    // Ensure element type is supported
    if (EDITABLE.Text.SUPPORTED_ELEMENT_TYPE_LIST.indexOf(element_type.toLowerCase())==-1) {
        throw Error('The <'+node_type+'> is not supported for text editing.');
    }
    
    // If the element type is a <h1> ensure one doesn't already exist
    if (element_type.toLowerCase()=='h1' && $('h1').length>0) {
        throw Error('Only one <h1> tag can exist in the document.')
    }
    
    /**
     * The HTML element that the Text Node will render as.
     * @type String
     * @private
     */
    this._element_type = element_type.toLowerCase();
    
    /**
     * The HTML last extracted from the HTML DOM node representing this 
     * editable node on the page.
     * @type String
     * @private
     */
    this._inner_html_snapshot = null;  
    
    /**
     * The contents of the Text Node represented as an IXMO.Soup instance.
     * @type IXMO.Soup
     * @private
     */
    this._soup = EDITABLE.Text._parser.parse(content||EDITABLE.Text.DEFAULT_CONTENT);
    
    /** 
     * The current selection within the Text Node.
     * @type SELECT.Selection
     * @private
     */
    this._selection = new SELECT.Selection(0,0);
    
    // Set the alignment for the node
    if (options['align']) {
        this.align(options['align']);
    } else {
        this.align(EDITABLE.Text.DEFAULT_ALIGN);
    }
    
    // Set the style for the node
    if (options['style']) {
        this.style(options['style']);
    } else {
        this.style(EDITABLE.Text.DEFAULT_STYLE);
    }    
}

// Inheritance
EDITABLE.Text.prototype = new EDITABLE.Node;
EDITABLE.Text.prototype._is_a = ['base','text'];
EDITABLE.Text.prototype._can_focus = true;

/**
 * Generate a Text Node from a HTML DOM node
 * @param {EDITABLE.Collection} owner The Collection the Text Node will belong 
 * to.
 * @param {Element} node The HTML DOM node from which to generate the Text 
 * Node.
 */
EDITABLE.Text.from_node = function(owner,node) {
    var options = {'restored':true};
    var class_list = $(node).attr('class').split(' ');
    if (class_list.length==2) {
        if (['text-left', 'text-right', 'text-center', 'text-justify'].indexOf(class_list[1]) == -1) {
            options['style'] = class_list[1];
        } else {
            options['align'] = class_list[1];
        }
    } 
    if (class_list.length==3) {
        options['align'] = class_list[1];
        options['style'] = class_list[2];
    }
    
    var editable_node = new EDITABLE.Text(owner,node.nodeName,$(node).html(),options);
    return editable_node;
}

/** 
 * An inline XHTML parser for managing the I/O of content in Text Nodes. Only
 * one instance is required so we provide a shared reference.
 * @final
 * @type IXMO.Parser
 * @private
 */
EDITABLE.Text._parser = new IXMO.Parser();

/** 
 * The default content inserted into empty Text Nodes.
 * @final
 * @type String
 */
EDITABLE.Text.DEFAULT_CONTENT = 'Insert text...';

/** 
 * A list of supported element types for editable Text Nodes.
 * @final
 * @type Array
 */
EDITABLE.Text.SUPPORTED_ELEMENT_TYPE_LIST = 'h1,h2,h3,h4,h5,h6,p'.split(',');

/** 
 * The default alignment for Text Nodes.
 * @final
 * @type String
 */
EDITABLE.Text.DEFAULT_ALIGN = 'text-left';

/** 
 * The default style for Text Nodes.
 * @final
 * @type String
 */
EDITABLE.Text.DEFAULT_STYLE = '';

/**
 * Set/Get the element type for this Text Node. If an element type is provided
 * then element type set, else it is returned.
 * @param {String} element_type Optional. The element type to set for this Text
 * Node.
 */
EDITABLE.Text.prototype.element_type = function(element_type) {
    if (element_type===undefined) {
        return this._element_type;
    } else {
        if (element_type!=this._element_type) {
            // Ensure a supported element type has been specified
            if (EDITABLE.Text.SUPPORTED_ELEMENT_TYPE_LIST.indexOf(element_type.toLowerCase())==-1) {
                throw Error('<'+element_type.toLowerCase()+'> is not a supported element type for Text Nodes.');
            }
            this._element_type = element_type.toLowerCase();
            this._modified = true;
            this.render();
            this._owner.trigger('format_change',{'node':this,'attribute':'element_type'});
        }
    }
}

/**
 * Update the Soup with the contents of the Node.
 */
EDITABLE.Text.prototype._update_soup = function() {
    // Only update the Soup if the inner content has changed since the last
    // time we checked.
    if (this._inner_html_snapshot!=$(this.node).html()) {
        this._inner_html_snapshot = $(this.node).html();
        var content = this._inner_html_snapshot.replace(/^\s+/g,'').replace(/\s+$/g,'');
        this._soup = EDITABLE.Text._parser.parse(content);
        this._modified = true;
        this._owner.trigger('content_change',{'node':this});
    }
}

/**
 * Return true if the Text Node is modified.
 * @return Whether the Node is modified or not.
 * @type Boolean
 */
EDITABLE.Text.prototype.is_modified = function() {
    this._update_soup();
    return this._modified;
}

/**
 * Get the Soup that represenets the content in the Text Node. Even internally 
 * this is the recommended (and safe) way to access the Text Nodes Soup.
 * @return The Text Nodes Soup.
 * @type IXMO.Soup
 */
EDITABLE.Text.prototype.get_soup = function() {
    this._update_soup();
    return this._soup;
}

/**
 * Set the Soup for the Text Node.
 * @param {IXMO.Soup} soup The Soup to set for the Node.
 * @param {Boolean} render If true the method will render the Text Node.
 */
EDITABLE.Text.prototype.set_soup = function(soup, render) {
    this._soup = soup;
    if (render) {
        this.render();
    }
}

/**
 * Set the Selection for the Text Node.
 * @param {Number} start The start position of the Selection.
 * @param {Number} end The end position of the selection.
 * @param {Boolean} select If true the method will set the Selection.
 */
EDITABLE.Text.prototype.set_selection = function(start, end, select) {
    this._selection.start = start;
    this._selection.end = end | start;
    if (select) {
        SELECT.set(this.node,this._selection,true);
    }
}

/**
 * Get the Selection for the Text Node. Even internally this is the recommended
 * (and safe) way to access the Text Nodes Selection.
 * @return The selection within this Text Node.
 * @type SELECT.Selection
 */
EDITABLE.Text.prototype.get_selection = function() {
    // Only update the Selection if the Text Node is currently selected.
    var selection = SELECT.get(this.node)[0];
    if (selection) {
        this._selection = selection;
    }
    return this._selection;
}

/**
 * Return the Text Node as HTML.
 * @return The Text Node as a HTML String. 
 * @type String
 */
EDITABLE.Text.prototype.to_html = function() { 

    // Node open
    var node_html = '<'+this.element_type();
    
    // Node CSS
    node_html += ' class="edit-text';
    if (this._css_align) {
        node_html += ' ' + this._css_align;
    } else {
        node_html += ' ' + EDITABLE.Text.DEFAULT_ALIGN;
    }
    if (this._css_style) {
        node_html += ' '+this._css_style;
    }
    node_html += '"';
    
    // Node content
    node_html += '>'+this.get_soup().render(true);
    
    // Node close
    node_html += '</'+this.element_type()+'>';
    
    return node_html;
}

/**
 * Render the Text Node to the HTML DOM.
 */
EDITABLE.Text.prototype.render = function() {
    
    // Remove any existing HTML DOM node
    this.unrender();
    
    // Build the HTML required to add the HTML DOM node
    var node_html = '<'+this.element_type()+' id="'+this.element_id+'"></'+this.element_type()+'>';

    // If the node is not the first in the list render it after the previous 
    // node, else prepend it to the owner node.
    var prev_node = this._owner.get_node_before(this);
    
    if (prev_node) {
        $(prev_node.node).after(node_html);
    } else {
        $(this._owner.parent_node).prepend(node_html)
    }
    
    // Update the reference to the HTML DOM node
    this.node = $('#'+this.element_id)[0];
    
    // Render the inner content of the Text Node. Important! This is one of the
    // few occasions where we don't want to use the 'get_soup' method.
    $(this.node).html(this._soup.render(true));
    
    // Add applicable CSS classes to the node
    $(this.node).addClass('edit-text');
    if (this._css_align) {
        $(this.node).addClass(this._css_align);
    }
    if (this._css_style) {
        $(this.node).addClass(this._css_style);
    }
    
    // Events
    
    // Mouse events
    $(this.node).bind('mouseover',this,
        function(ev) {
            if (!ev.data._selected) {
                $(this).addClass('over');
            }
        }
    );
    $(this.node).bind('mouseout',this,
        function(ev) {
            $(this).removeClass('over');
        }
    );
    $(this.node).bind('mousedown',this,
        function(ev) {
            ev.data.select();
        }
    );
    $(this.node).bind('mouseup',this,
        function(ev) {
            ev.data.get_selection();
        }
    );
    $(this.node).bind('click',this,
        function(ev) {
            ev.data._owner.trigger('node_click',{'node':ev.data});
        }
    );
            
    // Keyboard events
    $(this.node).bind('keydown',this,
        function(ev) {
            var allow_event = true;
            var node = ev.data;
            var selection = node.get_selection();
            var soup = node.get_soup();
                    
            switch(ev.which) {
                
                // Merging
                case 8: // Backspace
                    if (selection.start==0 && selection.is_collapsed()) {
                        var prev_node = node._owner.get_node_before(node,'text');
                        if (prev_node) {
                            var insert_pos = prev_node.get_soup().get_length();
                            prev_node.get_soup().paste(soup,insert_pos);
                            prev_node.render();
                            prev_node.set_selection(insert_pos);
                            if(node.get_soup().get_length()>0) {
                                node._owner.remove_node(node);
                            }
                            prev_node.select();
                            allow_event = false;
                        
                        }
                    }
                    break;
                
                case 46: // Delete
                    if (selection.start==soup.get_length() && selection.is_collapsed()) {
                        var next_node = node._owner.get_node_after(node,'text');
                        if (next_node) {
                            var insert_pos = soup.get_length();
                            soup.paste(next_node.get_soup(),insert_pos);
                            node.set_selection(insert_pos,insert_pos);
                            node.render();
                            node._owner.remove_node(next_node);
                            allow_event = false;
                        }
                    }
                    break;
                
                // Splitting
                case 13: // Return
                    // Delete selected content 
                    if (!selection.is_collapsed()) {
                        soup.erase(selection.as_array());
                        node.set_selection(selection.start);
                    }
                    // Split the paragraph in two
                    var soup_list = soup.split(selection.start);
                    // Check for empty an existing empty Text Node
                    var first_empty = false;
                    var second_empty = false;
                    if (soup_list[0].get_length()==0) {
                        soup_list[0] = EDITABLE.Text._parser.parse(EDITABLE.Text.DEFAULT_CONTENT);
                        first_empty = true;
                    }
                    if (soup_list[1].get_length()==0) {
                        second_empty = true;
                    }
                    // Change the existing Text Node
                    node.set_soup(soup_list[0],true);
                    // Add the new Text Node
                    options = {};
                    if (node.element_type()=='p') {
                        options['align'] = node.align();
                        options['style'] = node.style();
                    }
                    var new_node = node._owner.add_text(
                        'p',
                        soup_list[1].render(true),
                        options,
                        node,
                        'after'
                        );
                    if (first_empty) {
                        node.select();
                        node.set_selection(0,node.get_soup().get_length(),true);
                    } else {
                        new_node.select();
                        if (second_empty) {
                            new_node.set_selection(0,new_node.get_soup().get_length(),true);
                        }
                    }
                    allow_event = false;
                    break;                
                
                // Navigation
                
                // Arrows
                case 37: // Left-arrow
                    if (selection.start==0 && selection.is_collapsed()) {
                        var prev_node = node._owner.get_node_before(node,'text');
                        if (prev_node) {
                            prev_node.set_selection(prev_node.get_soup().get_length());
                            prev_node.select();
                            allow_event = false;   
                        }
                    }
                    break;

                case 39: // Right-arrow
                    if (selection.start==soup.get_length() && selection.is_collapsed()) {
                        var next_node = node._owner.get_node_after(node,'text');
                        if (next_node) {
                            next_node.set_selection(0);
                            next_node.select();
                            allow_event = false;   
                        }
                    }
                    break;
                
                case 38: // Up-arrow
                    if (selection.start==0&&selection.is_collapsed()) {
                        var prev_node = node._owner.get_node_before(node,'text');
                        if (prev_node) {
                            prev_node.set_selection(prev_node.get_soup().get_length());
                            prev_node.select();
                            allow_event = false;   
                        }
                    }
                    break;                

                case 40: // Down-arrow
                    if (selection.start==soup.get_length() && selection.is_collapsed()) {
                        var next_node = node._owner.get_node_after(node,'text');
                        if (next_node) {
                            next_node.set_selection(0);
                            next_node.select();
                            allow_event = false;   
                        }
                    }
                    break;   
                
                // Tabbing
                case 9: // Tab-key
                    if (ev.shiftKey) { // Reverse
                        var prev_node = node._owner.get_node_before(node);
                        if (prev_node) {
                            prev_node.select();
                            allow_event = false;   
                        }
                    } else { // Forward
                        var next_node = node._owner.get_node_after(node);
                        if (next_node) {
                            next_node.select();
                            allow_event = false;   
                        }                
                    }
                    break;
                
                // Meta-keys
                case 33: // Page up
                    node.set_selection(0,null,true);
                    allow_event = false;
                    break;                  

                case 34: // Page down
                    node.set_selection(soup.get_length(),null,true);
                    allow_event = false;
                    break;    
                
            }
            node._update_soup();
            return allow_event;
        }
    );    
    $(this.node).bind('keyup',this,
        function(ev) {
            ev.data._update_soup();
            ev.data._owner.trigger('node_keyup',{'node':ev.data});
        }    
    ); 
    $(this.node).bind('keypress',this,
        function(ev) {
            ev.data._owner.trigger('node_keypress',{'node':ev.data});
        }
    );

    // Pasting
    $(this.node).bind('paste',this,
        function(ev) {
            var paste_text = '';
            if (window.clipboardData) {
                // IE
                paste_text = window.clipboardData.getData('Text');
            } else if (ev.originalEvent.clipboardData) {
                // Safari
                paste_text = ev.originalEvent.clipboardData.getData('text/plain');
            } else {     
                // Mozilla
                try {
                    // Ask the user for permission
                    //netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
                    var clip = Components.classes['@mozilla.org/widget/clipboard;1'].getService(Components.interfaces.nsIClipboard);   
                    if (!clip) {
                        return false;
                    }     
                    var trans = Components.classes['@mozilla.org/widget/transferable;1'].createInstance(Components.interfaces.nsITransferable);   
                    if (!trans) {
                        return false;
                    }
                    // Extract the clipboard data
                    trans.addDataFlavor('text/unicode');  
                    clip.getData(trans,clip.kGlobalClipboard);   
                    var str = new Object();   
                    var str_len = new Object();   
                    trans.getTransferData("text/unicode", str, str_len);
                    var paste_text = '';
                    if (str) {
                        str = str.value.QueryInterface(Components.interfaces.nsISupportsString);   
                        paste_text = str.data.substring(0,str_len.value/2);
                    }
                } catch (e) {
                    // Fallback copy paste system for Firefox
                    paste_text = prompt('Please enter the text you wish to copy...');
                }
            }
            if (paste_text) {
                node = ev.data;
                selection = node.get_selection();
                soup = node.get_soup();
                // Remove any currently selected text
                if (!selection.is_collapsed()) {
                    soup.erase(selection.as_array());
                    node.set_selection(selection.start);
                }
                // Get the text at the end of the split
                var soup_list = soup.split(selection.start);
                // Format the paste text to have no new lines
                paste_text = paste_text.replace(/\r\n/g,'\n');
                paste_text = paste_text.replace(/\r/g,'\n');
                paste_text = paste_text.replace(/\n/g,' ');
                // Paste the text
                node.get_soup().insert(paste_text,selection.start);
                node.render();
                node._update_soup();  
            }
            return false;
        }
    );     
        
    // Droppable events
    $(this.node).droppable({accept:'.draggable',tolerance:'pointer'});
    $(this.node).bind('dropover',this,
        function(ev,ui) {
            $(this).addClass('drop');
            // Show the direction of the drag/drop
            var dragging_node = ev.data._owner.get_selected_node();
            if (dragging_node) {
                $(this).addClass(dragging_node.align().replace(dragging_node.get_final_type(),'drop'));
            }
        }
    );
    $(this.node).bind('dropout',this,
        function(ev,ui) {
            var dragging_node = ev.data._owner.get_selected_node();
            $(this).removeClass('drop');
            // Hide the direction of the drag/drop
            if (dragging_node) {
                $(this).removeClass(dragging_node.align().replace(dragging_node.get_final_type(),'drop'));
            }
        }
    );            
    $(this.node).bind('drop',this,
        function(ev,ui) {
            var dragging_node = ev.data._owner.get_selected_node();
            $(this).removeClass('drop');
            if (dragging_node) {
                $(this).removeClass(dragging_node.align().replace(dragging_node.get_final_type(),'drop'));
                if (dragging_node) {
                    // Perform the drag/drop
                    if (dragging_node.align()==dragging_node.get_final_type()+'-center') {
                        dragging_node._owner.move_node(dragging_node,ev.data,'after');
                        dragging_node._modified = true;
                    } else {
                        dragging_node._owner.move_node(dragging_node,ev.data,'before');
                        dragging_node._modified = true;
                    }
                }
            }
        }
    );    
    
    // Re-select the Node if it was previously selected
    if (this._selected) {
        this._selected = false;
        this.select();
    }
}

/**
 * Remove the rendered Text Node from the HTML DOM.
 */
EDITABLE.Text.prototype.unrender = function() {
    $('#'+this.element_id).droppable('destroy');
    EDITABLE.Node.prototype.unrender.call(this);
}

/**
 * Select the Text Node.
 */
EDITABLE.Text.prototype.select = function() {
    if (!this._selected) {
        // Make the Text Node content editable
        $(this.node).attr('contentEditable','true'); // Capital 'E'ditable required by IE 7
        $(this.node).focus();
        // Select the Node
        EDITABLE.Node.prototype.select.call(this);
        // Set the current Selection. Important! This is one of the few 
        // occasions where we don't want to use the 'get_selection' method.
        SELECT.set(this.node,this._selection,true);
    }
}

/**
 * Unselect the Text Node.
 */
EDITABLE.Text.prototype.unselect = function() {
    if (this._selected) {
        // Ensure the Soup and Selection is up-to-date...
        this._update_soup();
        // ...unselect the Node...
        EDITABLE.Node.prototype.unselect.call(this);
        // Content editable Nodes must be re-rendered when the user has 
        // finished editing them because of IE render engine, which will cause 
        // elements with the 'contentEditable' attribute to render with an 
        // CSS overlay:auto; effect.
        this.render();
        // ...and that the Text Node is not empty
        if (this.get_soup().get_length()==0) {
            this._owner.remove_node(this);
        }
    }
}

/**
 * Return a copy of the Text Node.
 * @return A copy of the Text Node
 * @type EDITABLE.Text
 */
EDITABLE.Text.prototype.copy = function(owner) {
    var copy_node = new EDITABLE.Text(
        owner,
        this.element_type(),
        this.get_soup().render(true),
        {align:this.align(),style:this.style()}
        );
    return copy_node;
} 


/**
 * Create a new editable Image Node.
 * @class An editable Image Node. 
 * Supported align values are 'image-(left|center|right)'.
 * @param {EDITABLE.Collection} owner The Collection that owns this Text Node.
 * @param {String} src The location or content of the image.
 * @param {Array} size A two item array containing the images dimensions 
 * (width,height).
 * @param {String} alt The alternative text for the image.
 * @param {Object} options Optional. A map of options to set intitially for the
 * Node.
 * @constructor
 */
EDITABLE.Image = function(owner, src, size, alt, options) {
    
    options = options || {};
    EDITABLE.Node.call(this,owner,options);
    
    /**
     * The location or content of the image.
     * @type String
     * @private
     */
    this._src = src;
    
    /**
     * A two item array containing the images dimensions (width,height).
     * @type Array
     * @private
     */
    this._size = size;
    
    // Prevent '0' dimension images 
    this._size[0] = Math.max(1,this._size[0]);
    this._size[1] = Math.max(1,this._size[1]);
    
    /**
     * The aspect ratio of the image.
     * @type Number
     */
     this.aspect_ratio = this.size()[0]/this.size()[1];
    
    /**
     * The alternative text for the image.
     * @type String
     * @private
     */
    this._alt = alt;
    
    /**
     * The long description for the image (can be text or a URL).
     * @type String
     * @private
     */
    this._long_desc = options['long_desc']||'';
    
    /**
     * The link (URL) for the image.
     * @type String
     */
    this._href = options['href']||'';
    
    /**
     * A title (shown as a tooltip) for the image.
     * @type String
     * @private
     */
    this._title = options['title']||'';
    
    /**
     * A two item array containing the minimum size an image can be reduced to
     * (width,height).
     * @type Array
     * @private
     */
    var min_size = options['min_width']||EDITABLE.Image.DEFAULT_MIN_SIZE;
    this._min_size = [min_size,min_size/this.aspect_ratio];    
    
    /**
     * A two item array containing the maximum size an image can be reduced to
     * (width,height).
     * @type Array
     * @private
     */
    var max_size = options['max_width']||EDITABLE.Image.DEFAULT_MAX_SIZE;
    this._max_size = [max_size,max_size/this.aspect_ratio];  
    
    /**
     * A flag indicating whether or not the image is draggable mode.
     * @type Boolean
     * @private
     */
    this._draggable = false;
    
    /**
     * A flag indicating whether or not the image is currently been dragged.
     * @type Boolean
     * @private
     */
    this._dragging = false;
    
    /**
     * A flag indicating whether or not the image is resizable mode.
     * @type Boolean
     * @private
     */
    this._resizable = false;   
    
    /**
     * A flag indicating whether or not the image is currently been resized.
     * @type Boolean
     * @private
     */
    this._resizing = false;
    
    // Set the alignment for the node
    this.align(options['align']||EDITABLE.Image.DEFAULT_ALIGN);

    // Set the style for the node
    this.style(options['style']||EDITABLE.Image.DEFAULT_STYLE); 
}

// Inheritance
EDITABLE.Image.prototype = new EDITABLE.Node;
EDITABLE.Image.prototype._is_a = ['node','image'];

/**
 * Generate an Image Node from a HTML DOM node
 * @param {EDITABLE.Collection} owner The Collection the Image Node will belong
 * to.
 * @param {Element} node The HTML DOM node from which to generate the Image 
 * Node.
 */
EDITABLE.Image.from_node = function(owner,node) {
    var options = {'restored':true};
    var class_list = $(node).attr('class').split(' ');
    if (class_list.length>1) {
        options['align'] = class_list[1];
    } 
    if (class_list.length>2) {
        options['style'] = class_list[2];
    }
    if (node.nodeName.toLowerCase()=='a') {
        options['href'] = $(node).attr('href');
        node = $(node).find('img')[0];
    }
    if ($(node).attr('longdesc')) {
        options['long_desc'] = $(node).attr('longdesc');
    }
    if ($(node).attr('title')) {
        options['title'] = $(node).attr('title');
    }
    if (!$(node).attr('style')) {
        $(node).css({'width': $(node).attr('width'),
            'height': $(node).attr('height'),
            'min-width': $(node).attr('width'),
            'max-width': $(node).attr('width')});
    }
    var style = $(node).attr('style');
    var width = parseInt(style.match(/(^|\s+)width:\s*(\d+)\s*px(;|$)/i)[2]);
    var height = parseInt(style.match(/(^|\s+)height:\s*(\d+)\s*px(;|$)/i)[2]);
    if (style.match(/(^|\s+)min-width:\s*(\d+)\s*px(;|$)/i)) {
        options['min_width'] = parseInt(style.match(/(^|\s+)min-width:\s*(\d+)\s*px(;|$)/i)[2]);
    }
    if (style.match(/(^|\s+)max-width:\s*(\d+)\s*px(;|$)/i)) {
        options['max_width'] = parseInt(style.match(/(^|\s+)max-width:\s*(\d+)\s*px(;|$)/i)[2]);
    }
    var i_n = new EDITABLE.Image(
        owner,
        $(node).attr('src'),
        [width,height],
        $(node).attr('alt'),
        options
        );
    return i_n;
}

/** 
 * The default minimum size for Image Nodes.
 * @final
 * @type Array
 */
EDITABLE.Image.DEFAULT_MIN_SIZE = 100;

/** 
 * The default maximum size for Image Nodes.
 * @final
 * @type Array
 */
EDITABLE.Image.DEFAULT_MAX_SIZE = 500;

/** 
 * The default size of the resizable square in the bottom left of the image.
 * @final
 * @type Number
 */
EDITABLE.Image.DEFAULT_RESIZE_THRESHOLD = 15;

/** 
 * The default alignment for Image Nodes.
 * @final
 * @type String
 */
EDITABLE.Image.DEFAULT_ALIGN = 'image-center';

/** 
 * The default style for Image Nodes.
 * @final
 * @type String
 */
EDITABLE.Image.DEFAULT_STYLE = '';

/**
 * Update the size of the Image Node.
 */
EDITABLE.Image.prototype._update_size = function() {
    this.size([$(this.node).innerWidth(),$(this.node).innerHeight()]);
    this._owner.trigger('resize_node',{'node':this});
}

/**
 * Get (read-only) the element type for this Image Node. 
 * @return The nodes elemeny type (always 'img').
 */
EDITABLE.Image.prototype.element_type = function() {
    return 'img';
}

/**
 * Change the image.
 * @param {String} src The src to set for this Image Node.
 */
EDITABLE.Image.prototype.change = function(src, size, alt, options) {
    var changed = false;
    if (src!=this._src) {
        this._src = src;
        changed = true;
    }
    if (size!=this._size) {
        this._size = size;
        this.aspect_ratio = this.size()[0]/this.size()[1];
        changed = true;  
    }
    if (alt && alt!=this._alt) {
        this._alt = alt;
        changed = true;  
    }
    if (options['long_desc'] && options['long_desc']!=this._long_desc) {
        this._long_desc = options['long_desc'];
        changed = true;  
    }
    if (options['href'] && options['href']!=this._href) {
        this._href = options['href'];
        changed = true;  
    }
    if (options['title'] && options['title']!=this._title) {
        this._title = options['title'];
        changed = true;  
    }
    if (options['min_width'] && options['min_width']!=this._min_size) {
        this._min_size = [options['min_width'],options['min_width']/this.aspect_ratio];
    }
    if (options['max_width'] && options['max_width']!=this._max_size) {
        this._max_size = [options['max_width'],options['max_width']/this.aspect_ratio];          
    }
    this.render();
    if (changed) {
        this._modified = true;
        this._owner.trigger('content_change',{'node':this});
    }
}

/**
 * Get the location or content of the image.
 * @return The location or content of the image.
 * @type String
 */
EDITABLE.Image.prototype.src = function(src) {
    if (src===undefined) {
        return this._src;
    } else {
        if (src!=this._src) {
            this._src = src;
            $(this.node).attr('src', this._src);
            this._modified = true;
            this._owner.trigger('content_change',{'node':this});
        }
    }
}

/**
 * Set/Get the size of this Image Node. If size is provided then the size is 
 * set, else it is returned.
 * @param {Array} size Optional. The size of the Image Node as a two item Array
 * [width,height].
 */
EDITABLE.Image.prototype.size = function(size) {
    if (size===undefined) {
        return this._size;
    } else {
        if (size!=this._size) {
            this._size = size;
            $(this.node).css({'height':size[1],'width':size[0]});
            this._modified = true;
            this._owner.trigger('content_change',{'node':this});
        }
    }
}

/**
 * Set/Get the alternative text for this Image Node. If alt is provided
 * then the alternative text is set, else it is returned.
 * @param {String} alt Optional. The alternative text to set for this Image
 * Node.
 */
EDITABLE.Image.prototype.alt = function(alt) {
    if (alt===undefined) {
        return this._alt;
    } else {
        if (alt!=this._alt) {
            this._alt = alt;
            $(this.node).find('img').attr('alt',alt);
            this._modified = true;
            this._owner.trigger('content_change',{'node':this});
        }
    }
}

/**
 * Set/Get the long description for this Image Node. If long_desc is provided
 * then the long description is set, else it is returned.
 * @param {String} long_desc Optional. The long description to set for this 
 * Image Node.
 */
EDITABLE.Image.prototype.long_desc = function(long_desc) {
    if (long_desc===undefined) {
        return this._long_desc;
    } else {
        if (long_desc!=this._long_desc) {
            this._long_desc = long_desc;
            if (long_desc) {
                $(this.node).find('img').attr('longdesc',long_desc);
            } else {
                $(this.node).find('img').removeAttr('longdesc');
            }
            this._modified = true;
            this._owner.trigger('content_change',{'node':this});
        }
    }
}

/**
 * Set/Get the link (URL) for this Image Node. If href is provided then the
 * link is set, else it is returned.
 * @param {String} href Optional. The link to set for this Image Node.
 */
EDITABLE.Image.prototype.href = function(href) {
    if (href===undefined) {
        return this._href;
    } else {
        if (href!=this._href) {
            this._href = href;
            this._modified = true;
            this._owner.trigger('content_change',{'node':this});
        }
    }
}

/**
 * Set/Get the title (shown as a tooltip) for this Image Node. If title is 
 * provided then the title is set, else it is returned.
 * @param {String} title Optional. The title to set for this Image Node.
 */
EDITABLE.Image.prototype.title = function(title) {
    if (title===undefined) {
        return this._title;
    } else {
        if (title!=this._title) {
            this._title = title;
            if (title) {
                $(this.node).find('img').attr('title',title);
            } else {
                $(this.node).find('img').removeAttr('title');
            }
            this._modified = true;
            this._owner.trigger('content_change',{'node':this});
        }
    }
}

/**
 * Render the Image Node to the HTML DOM.
 */
EDITABLE.Image.prototype.render = function() {
    
    // Remove any existing HTML DOM node
    this.unrender();
    
    // Build the HTML required to add the HTML DOM node
    var node_html = '<a id="'+this.element_id+'"><img src="'+this._src+'" alt="'+this._alt+'" /></a>';

    // If the node is not the first in the list render it after the previous 
    // node, else prepend it to the owner node.
    var prev_node = this._owner.get_node_before(this);
    
    if (prev_node) {
        $(prev_node.node).after(node_html);
    } else {
        $(this._owner.parent_node).prepend(node_html)
    }
    
    // Update the reference to the HTML DOM node
    this.node = $('#'+this.element_id)[0];

    // Set the size
    $(this.node).css({'height':this.size()[1],'width':this.size()[0]});
    
    // Add optional attributes
    if (this._long_desc) {
        $(this.node).find('img').attr('longdesc',this._long_desc);
    }
    if (this._title) {
        $(this.node).find('img').attr('title',this._title);
    }
    
    // Add applicable CSS classes to the node
    $(this.node).addClass('edit-image');
    if (this._css_align) {
        $(this.node).addClass(this._css_align);
    }
    if (this._css_style) {
        $(this.node).addClass(this._css_style);
    }
    
    // Events
    
    // Mouse
    $(this.node).bind('mouseover',this,
        function(ev) {
            if (!ev.data.is_selected()) {
                $(this).addClass('over');
            }
        }
    );
    $(this.node).bind('mousemove',this,
        function(ev) {
            if (ev.data.is_selected() && !(ev.data._dragging || ev.data._resizing)) {
                var x = ev.pageX-this.offsetLeft;
                var y = ev.pageY-this.offsetTop;
                var w = $(this).width()-EDITABLE.Image.DEFAULT_RESIZE_THRESHOLD;
                var h = $(this).height()-EDITABLE.Image.DEFAULT_RESIZE_THRESHOLD;
                if (x>w && y>h) {
                    ev.data.resizable();
                } else {
                    ev.data.draggable();
                }
            }
        }
    );    
    $(this.node).bind('mouseout',this,
        function(ev) {
            $(this).removeClass('over');
        }
    );    
    $(this.node).bind('mousedown',this,
        function(ev) {
            ev.data.select();
        }
    );
    $(this.node).bind('click',this,
        function(ev) {
            ev.data._owner.trigger('node_click',{'node':ev.data});
        }
    );
    
    // UI Events
    $(this.node).bind('dragstart',this,
        function(ev) {
            ev.data._dragging = true;
        }
    );    
    $(this.node).bind('dragstop',this,
        function(ev) {
            ev.data._dragging = false;
        }
    );     
    $(this.node).bind('resizestart',this,
        function(ev) {
            ev.data._resizing = true;
        }
    );    
    $(this.node).bind('resizestop',this,
        function(ev) {
            ev.data._update_size();
            ev.data._resizing = false;
        }
    );     
    
    // Re-select the Node if it was previously selected
    if (this._selected) {
        this._selected = false;
        this.select();
    }
}

/**
 * Key up event.
 */
EDITABLE.Image.prototype._key_down = function(ev) {
    var allow_event = true;
    if (this.is_selected() && !(this._dragging || this._resizing)) {
        switch (ev.which) {
            case 46: // Delete
                this._owner.remove_node(this);
                allow_event = false;  
                break;
                        
            // Navigation
    
            // Tabbing
            case 9: // Tab-key
                if (ev.shiftKey) { // Reverse
                    var prev_node = this._owner.get_node_before(this);
                    if (prev_node) {
                        prev_node.select();
                        allow_event = false;   
                    }
                } else { // Forward
                    var next_node = this._owner.get_node_after(this);
                    if (next_node) {
                        next_node.select();
                        allow_event = false;   
                    }                
                }
                break;
        }
    }
    return allow_event;
}

/**
 * Remove the rendered Image Node from the HTML DOM.
 */
EDITABLE.Image.prototype.unrender = function() {
    $('#'+this.element_id).draggable('destroy');
    $('#'+this.element_id).resizable('destroy');
    EDITABLE.Node.prototype.unrender.call(this);
}

/**
 * Return the Image Node as HTML.
 * @return The Image Node as a HTML String. 
 * @type String
 */
EDITABLE.Image.prototype.to_html = function() { 
    // Image
    var node_html = '<img src="'+this._src+'" alt="'+this._alt+'" style="width:'+this.size()[0]+'px; height:'+this.size()[1]+'px; min-width:'+this._min_size[0]+'px; max-width:'+this._max_size[0]+'px;"';
    // Add optional attributes
    if (this._long_desc) {
        node_html += ' longdesc="'+this._long_desc+'"';
    }
    if (this._title) {
        node_html += ' title="'+this._title+'"';
    }
    // CSS
    var css = 'edit-image';
    if (this._css_align) {
        css += ' '+this._css_align;
    }
    if (this._css_style) {
        css += ' '+this._css_style;
    }
    // Link
    if (this._href) {
        node_html = '<a href="'+this._href+'" class="'+css+'" style="width:'+this.size()[0]+'px; height:'+this.size()[1]+'px;">'+node_html+' /></a>'; 
    } else {
        node_html += ' class="'+css+'"'+' />';
    }
    return node_html;
}

/**
 * Unselect the Image Node.
 */
EDITABLE.Image.prototype.unselect = function() {
    if (this._selected) {
        EDITABLE.Node.prototype.unselect.call(this);
        this.draggable(false);
        this.resizable(false);
    }
}

/**
 * Activate or deactivate dragging of the Image Node.
 * @param {Boolean} allow If true (or not specified) dragging is activated for
 * the Image Node, else it is disabled.
 */
EDITABLE.Image.prototype.draggable = function(allow) {
    if (allow===undefined || allow==true) {
        if (!this.is_draggable()) {
            this.resizable(false);
            $(this.node).find('img').draggable('destroy');
            // Enable dragging
            $(this.node).find('img').draggable({
                revert:true,
                revertDuration:0,
                helper:function() { 
                    var clone_node = $(this).clone();
                    clone_node.addClass('drag');
                    return clone_node; 
                    },
                opacity:0.35,
                cursor:'move',
                cursorAt:{left:this.size()[0]/2,top:this.size()[1]/2}
                });
            $(this.node).find('img').addClass('draggable');
            this._draggable = true;
        }
    } else {
        if (this._draggable) {
            $(this.node).find('img').removeClass('draggable');
            $(this.node).find('img').draggable('destroy');
        }
        this._draggable = false;
    }
}

/**
 * Return true if the Image Node is draggable.
 * @return Whether the Image Node is draggable.
 * @type Boolean
 */
EDITABLE.Image.prototype.is_draggable = function() {
    return this._draggable;
}


/**
 * Return true if the Image Node is resizable.
 * @return Whether the Image Node is resizable.
 * @type Boolean
 */
EDITABLE.Image.prototype.is_resizable = function() {
    return this._resizable;
}

/**
 * Activate or deactivate resizing of the Image Node.
 * @param {Boolean} allow If true (or not specified) resizing is activated for
 * the Image Node, else it is disabled.
 */
EDITABLE.Image.prototype.resizable = function(allow) {
    if (allow===undefined || allow==true) {
        if (!this.is_resizable()) {
            this.draggable(false);
            // Enable resizing
            $(this.node).resizable({
                aspectRatio:this.aspect_ratio,
                handles:'se',
                minHeight:this._min_size[1],
                minWidth:this._min_size[0],
                maxHeight:this._max_size[1],
                maxWidth:this._max_size[0]
                });
            $(this.node).addClass('resizable');
            this._resizable = true;
        }
    } else {
        if (this._resizable) {
            $(this.node).removeClass('resizable');
            $(this.node).resizable('destroy');
        }
        this._resizable = false;
    }
}

/**
 * Create a new editable YouTube Movie Node.
 *
 * IMPORTANT! For movies to be displayed as more than links the external 
 * editor must provide a mechanism for parsing the page for the links created
 * by editable and converting them in the SWF inserted objects/embeds.
 *
 * @class An editable Movie Node.
 * Supported align values are 'movie-(left|center|right)'.
 * @param {EDITABLE.Collection} owner The Collection that owns this Text Node.
 * @param {String} url The location of the movie.
 * @param {Array} size A two item array containing the movies dimensions 
 * (width,height).
 * @param {String} alt The alternative text for the movie.
 * @param {Object} options Optional. A map of options to set intitially for the
 * Node.
 * @constructor
 */
EDITABLE.Movie = function(owner, url, size, alt, options) {
    options = options || {};
    EDITABLE.Node.call(this,owner,options);
    
    /**
     * A URL for the movie.
     *
     * @type String
     * @private
     */
    this._url = url;
    
    /**
     * A array containing the movies dimensions (width, height).
     *
     * @type Array
     * @private
     */
    this._size = size;
    // Prevent '0' dimension movies 
    this._size[0] = Math.max(1, this._size[0]);
    this._size[1] = Math.max(1, this._size[1]);
    
    /**
     * The aspect ratio of the movie. Calculated from the initially specified
     * width and height.
     *
     * @type Number
     */
    this.aspect_ratio = this.size()[0] / this.size()[1];
    
    /**
     * The alternative text for the movie. The text will appear in a link to
     * the movie if the visitor does not have the require JavaScript or SWF 
     * player installed/enabled on their browser.
     *
     * @type String
     * @private
     */
    this._alt = alt;
    
    /**
     * A two item array containing the minimum size a movie can be reduced to
     * (width, height).
     *
     * @type Array
     * @private
     */
    var min_size = options['min_width'] || EDITABLE.Movie.DEFAULT_MIN_SIZE;
    this._min_size = [min_size, (min_size / this.aspect_ratio)];  
    
    
    /**
     * A two item array containing the maximum size a movie can be increased 
     * to (width, height).
     *
     * @type Array
     * @private
     */
    var max_size = options['max_width'] || EDITABLE.Movie.DEFAULT_MAX_SIZE;
    this._max_size = [max_size, max_size / this.aspect_ratio];  
    
    /**
     * A flag indicating whether or not the movie is in a draggable state.
     *
     * @type Boolean
     * @private
     */
    this._draggable = false;

    /**
     * A flag indicating whether or not the movie is currently being dragged.
     *
     * @type Boolean
     * @private
     */
    this._dragging = false;
    
    /**
     * A flag indicating whether or not the movie is in a resizable state.
     *
     * @type Boolean
     * @private
     */
    this._resizable = false;   
    
    /**
     * A flag indicating whether or not the movie is currently being resized.
     * @type Boolean
     * @private
     */
    this._resizing = false;
    
    // Set the alignment for the node
    this.align(options['align'] || EDITABLE.Movie.DEFAULT_ALIGN);
    
    // Set the style for the node
    this.style(options['style'] || EDITABLE.Movie.DEFAULT_STYLE); 
}

// Inheritance
EDITABLE.Movie.prototype = new EDITABLE.Node;
EDITABLE.Movie.prototype._is_a = ['node', 'movie'];

/** 
 * The default minimum size for Movie Nodes.
 *
 * @final
 * @type Number (uint)
 */
EDITABLE.Movie.DEFAULT_MIN_SIZE = 200;

/** 
 * The default maximum size for Movie Nodes.
 *
 * @final
 * @type Number (uint)
 */
EDITABLE.Movie.DEFAULT_MAX_SIZE = 500;

/** 
 * The default size of the resizable square in the bottom left of the Movie.
 *
 * @final
 * @type Number (uint)
 */
EDITABLE.Movie.DEFAULT_RESIZE_THRESHOLD = EDITABLE.Image.DEFAULT_RESIZE_THRESHOLD;

/** 
 * The default alignment for Movie Nodes.
 *
 * @final
 * @type String
 */
EDITABLE.Movie.DEFAULT_ALIGN = 'movie-center';

/** 
 * The default style for Movie Nodes.
 *
 * @final
 * @type String
 */
EDITABLE.Movie.DEFAULT_STYLE = '';

/**
 * Generate a Movie Node from a HTML DOM node.
 *
 * @param {EDITABLE.Collection} owner The Collection the Movie Node will belong
 * to.
 * @param {Element} node The HTML DOM node from which to generate the Movie 
 * Node.
 */
EDITABLE.Movie.from_node = function(owner, node) {
    var options = {'restored': true};
    var class_list = $(node).attr('class').split(' ');
    if (class_list.length > 1) {
        options['align'] = class_list[1];
    } 
    if (class_list.length > 2) {
        options['style'] = class_list[2];
    }
    if (!$(node).attr('style')) {
        $(node).css({'width': $(node).attr('width'),
            'height': $(node).attr('height'),
            'min-width': $(node).attr('width'),
            'max-width': $(node).attr('width')});
    }
    var style = $(node).attr('style');
    var width = parseInt(style.match(/(^|\s+)width:\s*(\d+)\s*px(;|$)/i)[2]);
    var height = parseInt(style.match(/(^|\s+)height:\s*(\d+)\s*px(;|$)/i)[2]);
    if (style.match(/(^|\s+)min-width:\s*(\d+)\s*px(;|$)/i)) {
        options['min_width'] = parseInt(style.match(/(^|\s+)min-width:\s*(\d+)\s*px(;|$)/i)[2]);
    }
    if (style.match(/(^|\s+)max-width:\s*(\d+)\s*px(;|$)/i)) {
        options['max_width'] = parseInt(style.match(/(^|\s+)max-width:\s*(\d+)\s*px(;|$)/i)[2]);
    }
    var m_n = new EDITABLE.Movie(owner,
        $(node).attr('href'),
        [width, height],
        $(node).html(),
        options);
    return m_n;
}

/**
 * Get (read-only) the element type for this Movie Node. 
 * @return The nodes elemeny type (always 'object').
 */
EDITABLE.Movie.prototype.element_type = function() {
    return 'object';
}

/**
 * Update the size of the Movie Node.
 */
EDITABLE.Movie.prototype._update_size = function() {
    this.size([$(this.node).innerWidth(), $(this.node).innerHeight()]);
    this._owner.trigger('resize_node', {'node':this});
}

/**
 * Key down event.
 */
EDITABLE.Movie.prototype._key_down = function(ev) {
    var allow_event = true;
    if (this.is_selected() && !(this._dragging || this._resizing)) {
        switch (ev.which) {
            case 46: // Delete
                this._owner.remove_node(this);
                allow_event = false;  
                break;
            // Navigation (Tabbing)
            case 9: // Tab-key
                if (ev.shiftKey) { // Reverse
                    var prev_node = this._owner.get_node_before(this);
                    if (prev_node) {
                        prev_node.select();
                        allow_event = false;   
                    }
                } else { // Forward
                    var next_node = this._owner.get_node_after(this);
                    if (next_node) {
                        next_node.select();
                        allow_event = false;   
                    }                
                }
                break;
        }
    }
    return allow_event;
}

/**
 * Get/Set the URL for the Movie.
 * @param {String} url Optional. The location of the movie.
 * @return The URL for the Movie.
 * @type String
 */
EDITABLE.Movie.prototype.url = function(url) {
    if (url === undefined) {
        return this._url;
    } else {
        if (url != this._url) {
            this._url = url;
            this._modified = true;
            this._owner.trigger('content_change', {'node': this});
            this.render();
        }
    }
}

/**
 * Set/Get the size of this Movie Node.
 * 
 * @param {Array} size Optional. The size of the Movie Node as an Array 
 *(width, height).
 * @return The size for the Movie.
 * @type Array
 */
EDITABLE.Movie.prototype.size = function(size) {
    if (size === undefined) {
        return this._size;
    } else {
        if (size != this._size) {
            this._size = size;
            $(this.node).css({'height': size[1], 'width': size[0]});
            this._modified = true;
            this._owner.trigger('content_change', {'node': this});
        }
    }
}

/**
 * Set/Get the alternative text for this Movie Node.
 *
 * @param {String} alt Optional. The alternative text for the Movie.
 * @return The alternative text for the Movie.
 * @type String 
 */
EDITABLE.Movie.prototype.alt = function(alt) {
    if (alt===undefined) {
        return this._alt;
    } else {
        if (alt!=this._alt) {
            this._alt = alt;
            this._modified = true;
            this._owner.trigger('content_change',{'node':this});
            this.render();
        }
    }
}

/**
 * Change the Movie.
 *
 * @param {String} url The url for the Movie Node.
 * @param {String} size The size for the Movie Node.
 * @param {String} alt The alternative text for the Movie Node.
 * @param {String} options An dictionary of options for the Movie node.
 */
EDITABLE.Movie.prototype.change = function(url, size, alt, options) {
    var changed = false;
    if (url != this._url) {
        this._url = url;
        changed = true;
    }
    if (size != this._size) {
        this._size = size;
        this.aspect_ratio = this.size()[0] / this.size()[1];
        changed = true;  
    }
    if (alt && alt != this._alt) {
        this._alt = alt;
        changed = true;  
    }
    if (options['min_width'] && options['min_width'] != this._min_size) {
        this._min_size = [options['min_width'], options['min_width'] / this.aspect_ratio];
    }
    if (options['max_width'] && options['max_width'] != this._max_size) {
        this._max_size = [options['max_width'], options['max_width'] / this.aspect_ratio];          
    }
    this.render();
    if (changed) {
        this._modified = true;
        this._owner.trigger('content_change', {'node': this});
    }
}

/**
 * Render the Movie Node to the HTML DOM.
 */
EDITABLE.Movie.prototype.render = function() {
    // Remove any existing HTML DOM node
    this.unrender();
    // Build the HTML required to add the HTML DOM node
    var node_html = '<a id="' + this.element_id + '">' + this._alt + '</a>';
    // If the node is not the first in the list render it after the previous 
    // node, else prepend it to the owner node.
    var prev_node = this._owner.get_node_before(this);
    if (prev_node) {
        $(prev_node.node).after(node_html);
    } else {
        $(this._owner.parent_node).prepend(node_html)
    }
    // Update the reference to the HTML DOM node
    this.node = $('#' + this.element_id)[0];
    // Set the size
    $(this.node).css({'height': this.size()[1], 'width': this.size()[0]});
    // Add applicable CSS classes to the node
    $(this.node).addClass('edit-movie');
    if (this._css_align) {
        $(this.node).addClass(this._css_align);
    }
    if (this._css_style) {
        $(this.node).addClass(this._css_style);
    }
    // Events (Mouse)
    $(this.node).bind('mouseover', this, function(ev) {
        if (!ev.data.is_selected()) {
            $(this).addClass('over');
        }
    });
    $(this.node).bind('mousemove', this, function(ev) {
        if (ev.data.is_selected() && !(ev.data._dragging || ev.data._resizing)) {
            var x = ev.pageX - this.offsetLeft;
            var y = ev.pageY - this.offsetTop;
            var w = $(this).width() - EDITABLE.Movie.DEFAULT_RESIZE_THRESHOLD;
            var h = $(this).height() - EDITABLE.Movie.DEFAULT_RESIZE_THRESHOLD;
            if (x > w && y > h) {
                ev.data.resizable();
            } else {
                ev.data.draggable();
            }
        }
    });
    $(this.node).bind('mouseout', this, function(ev) {
        $(this).removeClass('over');
    });
    $(this.node).bind('mousedown', this, function(ev) {
        ev.data.select();
    });
    $(this.node).bind('click', this, function(ev) {
        ev.data._owner.trigger('node_click', {'node': ev.data});
    });
    // Events (UI)
    $(this.node).bind('dragstart', this, function(ev) {
        ev.data._dragging = true;
    });
    $(this.node).bind('dragstop', this, function(ev) {
        ev.data._dragging = false;
    });
    $(this.node).bind('resizestart', this, function(ev) {
        ev.data._resizing = true;
    });
    $(this.node).bind('resizestop', this, function(ev) {
        ev.data._update_size();
        ev.data._resizing = false;
    });
    // Re-select the Node if it was previously selected
    if (this._selected) {
        this._selected = false;
        this.select();
    }
}

/**
 * Remove the rendered Movie Node from the HTML DOM.
 */
EDITABLE.Movie.prototype.unrender = function() {
    $('#' + this.element_id).draggable('destroy');
    $('#' + this.element_id).resizable('destroy');
    EDITABLE.Node.prototype.unrender.call(this);
}

/**
 * Return the Movie Node as HTML.
 * @return The Movie Node as a HTML String. 
 * @type String
 */
EDITABLE.Movie.prototype.to_html = function() { 
    // Movie (a)
    var node_html = '<a href="' + this._url + '" style="width:' + this.size()[0] + 'px; height:' + this.size()[1] + 'px; min-width:' + this._min_size[0] + 'px; max-width:' + this._max_size[0] + 'px;"';
    // CSS
    var css = 'edit-movie';
    if (this._css_align) {
        css += ' ' + this._css_align;
    }
    if (this._css_style) {
        css += ' ' + this._css_style;
    }
    node_html += ' class="' + css + '"' + '>';
    // Contents (alt)
    node_html += this._alt + '</a>';
    return node_html;
}

/**
 * Unselect the Movie Node.
 */
EDITABLE.Movie.prototype.unselect = function() {
    if (this._selected) {
        EDITABLE.Node.prototype.unselect.call(this);
        this.draggable(false);
        this.resizable(false);
    }
}

/**
 * Activate/Deactivate dragging of the Movie Node.
 *
 * @param {Boolean} allow If true (or not specified) dragging is activated for
 * the Movie Node, else it is deactivated.
 */
EDITABLE.Movie.prototype.draggable = function(allow) {
    if (allow === undefined || allow == true) {
        if (!this.is_draggable()) {
            this.resizable(false);
            $(this.node).draggable('destroy');
            // Enable dragging
            $(this.node).draggable({revert: true,
                revertDuration: 0,
                helper:function() { 
                    var clone_node = $(this).clone();
                    clone_node.addClass('drag');
                    return clone_node; 
                    },
                opacity: 0.35,
                cursor: 'move',
                cursorAt: {left: this.size()[0] / 2, top: this.size()[1] / 2}});
            $(this.node).addClass('draggable');
            this._draggable = true;
        }
    } else {
        if (this._draggable) {
            $(this.node).removeClass('draggable');
            $(this.node).draggable('destroy');
        }
        this._draggable = false;
    }
}

/**
 * Return true if the Movie Node is draggable.
 *
 * @return Whether the Movie Node is draggable.
 * @type Boolean
 */
EDITABLE.Movie.prototype.is_draggable = function() {return this._draggable;}

/**
 * Activate/Deactivate resizing of the Movie Node.
 *
 * @param {Boolean} allow If true (or not specified) resizing is activated for
 * the Movie Node, else it is disabled.
 */
EDITABLE.Movie.prototype.resizable = function(allow) {
    if (allow === undefined || allow == true) {
        if (!this.is_resizable()) {
            this.draggable(false);
            // Enable dragging
            $(this.node).resizable({aspectRatio: this.aspect_ratio,
                handles: 'se',
                minHeight: this._min_size[1],
                minWidth: this._min_size[0],
                maxHeight: this._max_size[1],
                maxWidth: this._max_size[0]});
            $(this.node).addClass('resizable');
            this._resizable = true;
        }
    } else {
        if (this._resizable) {
            $(this.node).removeClass('resizable');
            $(this.node).resizable('destroy');
        }
        this._resizable = false;
    }
}

/**
 * Return true if the Movie Node is resizable.
 *
 * @return Whether the Movie Node is resizable.
 * @type Boolean
 */
EDITABLE.Movie.prototype.is_resizable = function() {return this._resizable;}


// - Final class collection will be
//     - EDITABLE.Settings
//         - EDITABLE.Collection
//             - EDITABLE.Node - DONE
//             - EDITABLE.Text - DONE
//             - EDITABLE.Image - DONE
//             - EDITABLE.Static ???
//             - EDITABLE.List
//             - EDITABLE.Movie - DONE
//             - EDITABLE.Navigation???
//             - EDITABLE.Table
//             - EDITABLE.Form
//     - EDITABLE.Template (required and optional fields)

// - * Settings to provide more query facilities
// - * unselect one select should cover multiple collections too, kinda does I think
// - * Consider caching render output for unmodified Nodes

// Plan undo/redo architecture
//    - Local undo/redo supported in editable text
//    - Node changes stored in an undo stack (managed by save framework) - complete snapshot stored with selection information
//        - These can be loaded from the server also - the system will check for changes every 30 seconds, it wont save a change if
//        the change being made is in the currently selected node. Before the browser window is closed a final save check will be made.
//    - Upto X undo's will be supported
//    - Mile stones to previously published pages will be supported

// IE and FF have problems allowing users to drag segments of text from one part to another
// Set/Get methods should have return type documentation to
// Add any corner resize for images
