/* $Id$ */

/*
 * This is the master object for all cookie information. The object is
 * populatede onload and saved back to the cookie onunload, so there is no
 * need to save cookie information during a session.
 */
var novus = {};

/// user defined
var drop_hover_class  = 'droppable-hover';

var drop_dup_accept_class = ['popupdrag'];
var drag_dup_id_prefix    = 'feed_dup_';

var popup_layer_id     = 'popup_layer';
var bookmark_popup_id  = 'popup_bookmark';
var share_popup_id     = 'popup_share';
var feedtools_popup_id = 'popup_tools';
var sources_popup_id   = 'popup_sources';

var DEBUG = 1;

/*
 * Authors: Kjell-Magne Øierud  <kjellm@linpro.no>
 *          Jan Henning Thorsen <jhthorsen-at-linpro.no>
 */


/*############################################################################
 *                          String functions
 */

/* Creates a new String object with all leading whitespace removed */
String.prototype.ltrim = function() {
    if(DEBUG > 3) logDebug("ltrim: pre = '" + this + "'");
    var i = 0;
    ws: for (;; i++) {
        switch (this.charAt(i)) {
        case ' ':
        case '\r':
        case '\t':
        case '\n':
        case '\v':
        case '\f':
            break;
        default:
            break ws;
        }
    }
    if(DEBUG > 3) logDebug("ltrim: i = " + i);
    var s = this.substring(i,this.length); 
    if(DEBUG > 3) logDebug("ltrim: post = '" + s + "'");
    return s;
};

if(!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(elt /*, from*/) {
        var len = this.length;
        var from = Number(arguments[1]) || 0;

        from = (from < 0)
            ? Math.ceil(from)
            : Math.floor(from);
        if (from < 0)
            from += len;

        for (; from < len; from++) {
            if (from in this &&
                this[from] === elt)
                return from;
        }
        return -1;
    };
}


/*############################################################################
 *                                 Misc
 */


/*
 * Changes the class of a label, when checkbox/radio is clicked
 */

var toggleLabelClass = function(event) {

    /// init
    var input   = event.src ? event.src() : event;
    var label   = input.nextSibling;
    var wrapper = getFirstParentByTagAndClassName(label, 'UL', null);

    /// remove class from radio label elements
    if(input.type == 'radio') {
       var inputs = getElementsByTagAndClassName(
                        'INPUT', null, input.form
                    );
       forEach(inputs, function(i) {
           if(i.name != input.name) { return }
           if(i.checked)            { return }
           removeElementClass(i.nextSibling, 'checked');
       });
    }

    /// scroll to right position
    wrapper.scrollTop = label.nvsScrollTop;

    /// add or remove class from label
    if(input.checked) {
        addElementClass(label, 'checked');
    }
    else {
        removeElementClass(label, 'checked');
    }
};


/*
 * Changes the firstChild.nodeValue of the anchor, based on the checkboxes
 * that is selected in list-element.
 */
changeSelectListLegend = function(legend) {

    /// init
    var list     = legend.nvsList;
    var selected = [];
    var checked  = 0;
    var total    = 0;
    var txt      = "";
    var title;

    forEach(
        getElementsByTagAndClassName('INPUT', null, list), function(b) {
            if(b.checked) {
                selected.push(b.nextSibling.firstChild.nodeValue);
            }
            else {
                checked++;
            }
            total++;
        }
    );

    /// all is selected
    if(checked == total) {
        legend.title                = "";
        legend.firstChild.nodeValue = "Alle " + legend.name;
    }

    /// just some are selected
    else {
        var more = ",..(" + selected.length + ")";
        title    = selected.join(", ");
        txt      = selected.splice(0,1);
    
        while(txt.length < 26 && selected.length) {
            txt += ", " + selected.splice(0, 1);
        }
    
        legend.title                = title;
        legend.firstChild.nodeValue = (selected.length) ? txt + more : txt;
    }
}

/*
 * This function takes two nodes and swaps the position in the DOM
 */
swapParentNodes = function(n1, n2) {

    /// init
    var p1 = n1.parentNode;
    var p2 = n2.parentNode;

    if(! (p1 && p2)) { return; }

    /// swap
    var r1 = p1.removeChild(n1);
    var r2 = p2.replaceChild(r1, n2);
    p1.appendChild(r2);
}

/*
 * This function looks for every label inside a block and appends handlers
 * to make it function as an advanced select-list.
 */
setupWannabeSelectList = function(block) {

    /// find labels
    var labels = getElementsByTagAndClassName('LABEL', null, block);

    var mo = function(event) { addElementClass(event.src(), 'hover');    };
    var ml = function(event) { removeElementClass(event.src(), 'hover'); };

    /// connect handlers
    forEach(labels, function(label) {
        connect(label, 'onmouseover', mo);
        connect(label, 'onmouseleave', ml);
        connect(label, 'onmousedown', function(event) {
            var wrapper = getFirstParentByTagAndClassName(label, 'UL', null);
            event.src().nvsScrollTop = wrapper.scrollTop;
        });
        connect(label.previousSibling, 'onclick', toggleLabelClass);
        toggleLabelClass(label.previousSibling);
    });
}

/*
 * This function display can set obj.style.display in three states:
 * display: none,              ("none")
 * display: state, but hidden  ("-state")
 * display: state, and visible ("state");
 *
 * element targeted is either event or event.src()
 *
 * This function is written to get physical properties from an object,
 * without showing it at once.
 */
threeStateShowElement = function(event, state) {

    /// init
    var obj = event.src ? event.src() : event;

    /// check state
    if(!state) {
        state = '+block';
    }

    /// hide completely
    if(state == 'none') {
        setStyle(obj, { 'display': 'none', 'visibility': 'hidden' });
    }

    /// display, but make it invisible
    else if(state.indexOf('-') == 0) {
        state = state.replace(/^\-/, '');
        setStyle(obj, { 'display': state, 'visibility': 'hidden' });
    }

    /// display
    else {
        setStyle(obj, { 'display': state, 'visibility': 'visible' });
    }
}

/*
 * This function connects events to an input, to show/hide default text.
 */
setToggleInputValue = function(elem, txt) {

    /// init
    var toggle_on = txt || elem.defaultValue;

    /// clear input
    connect(elem, 'onfocus', function() {
        if(elem.value.indexOf(toggle_on) == 0) {
            elem.value = "";
        }
    });

    /// set input
    connect(elem, 'onblur', function() {
        if(elem.value == "") {
            elem.value = elem.defaultValue;
        }
    });
}

/*
 * This function show/hides the popuplayer. The popup layer is a
 * <div id="popup_layer"> that covers the whole document. It is created
 * if not allready exists.
 * It takes a elem and pos arg: elem is added to the layer, and positioned
 * at position pos.
 */
showPopupLayer = function(elem, pos) {

    /// init
    var layer = $(popup_layer_id);
    var dim   = getElementDimensions('global');

    /// create layer
    if(!layer) {
        layer = DIV({ 'id': 'popup_layer' });
        document.body.appendChild(layer);
        connect(layer, 'onclick', hidePopupLayer);
    }

    /// add elem to layer
    if(elem) {
        elem.nvsHomeElement = elem.parentNode;
        layer.appendChild(elem);
    }

    /// show box
    if(pos) {
        showElement(elem);
        setElementPosition(elem, pos);
    }

    /// show layer
    setStyle(layer, { 'height': dim.h + "px" });
    setOpacity('global', 0.7);
    showElement(layer);
}

/*
 * Hides the popup layer and the first child element. Restores opacity on
 * global as well.
 */
hidePopupLayer = function(event) {

    /// check
    if(event && event.target() != event.src()) {
        return;
    }

    var layer, box, home;

    /// hide
    if(layer = $(popup_layer_id)) {
        hideElement(layer);
        setOpacity('global', 1);
        if(box = layer.firstChild) {
            hideElement(box);
            if(home = box.nvsHomeElement) {
                home.appendChild(box);
            }
        }
    }
}

/*
 * Takes two arguments - sets "expanded" className to the first and removes
 * "expanded" className from the second. (in reverse order)
 */
swapVisibility = function(showElem, hideElem) {
    if(hideElem) { 
        removeElementClass(hideElem.parentNode, 'expanded');
    }
    if(showElem) {
        setElementClass(showElem.parentNode, 'expanded');
    }
}

/*
 * Hides an element after timeout milliseconds (default: 300ms).
 * Element is either event or event.src().
 */
hideDelayed = function(event, timeout) {

    /// init
    var obj = event.src ? event.src() : event;

    if(!timeout) { timeout = 300; }

    if(timeout > 0) {
        obj._hideTimer = setTimeout(
                             function() { hideDelayed(obj, -1) }, timeout
                         );
    }
    else {
        hideElement(obj);
    }
}

/*
 * Stops the _hideTimer of the element, and therefor prevents it from being
 * hidden. Element is either event or event.src().
 */
cancelHideDelayed = function(event) {
    var obj = event.src ? event.src() : event;
    clearTimeout(obj._hideTimer);
}

/*
 * These three functions enable / disables an user from clicking, if
 * the clicked element calls followClick() onclick.
 */

var stopClicking = 0;

/* */
followClickDisable = function(event) {
    stopClicking = 1;
}

/* */
followClickEnable = function() {
    stopClicking = 0;
}

/* */
followClick = function(event) {
    if(stopClicking) { event.stop() }
}


/*############################################################################
 *                       Dragable element handlers
 */

/*
 * Controls the start of a drag event.
 */
dragEventStart = function(dragObj, event) {

    /// init
    var dragElem = dragObj.element;
    var dim      = getElementDimensions(dragElem);

    setStyle(dragElem.parentNode, { 'height': dim.h + 'px' });
    setStyle(dragElem, { 'position': 'absolute' });

    /// disable clicking for 100ms
    followClickDisable();
    setTimeout(followClickEnable, 100);
}

/*
 * Controls the stop of a drag event.
 */
dragEventStop = function(dragObj, event) {

    /// init
    var dragElem = dragObj.element;

    setStyle(dragElem, { 'position': 'relative', 'top': 0, 'left': 0 });
    setStyle(dragElem.parentNode, { 'height': 'auto' });
    removeElementClass(dragElem.parentNode, drop_hover_class);

    /// disable clicking for 100ms
    followClickDisable();
    setTimeout(followClickEnable, 100);
}

/*
 * connect draggables signal
 */
connect(Draggables, 'start', dragEventStart);
connect(Draggables, 'end', dragEventStop);

/*
 * The function swaps the dragged element, with the target element.
 */
dropHover = function(dragElem, dropTarget, event) {

    /// init
    var dragReplace = dropTarget.firstChild;
    var dropSource  = dragElem.nvsHomeElement;
    var rElem;

    addElementClass(dropSource, drop_hover_class);

    /// still over it's own droppable
    if(dropTarget == dropSource) {
        return; 
    }

    /// swap with another dragElem
    if(dragReplace) {

        if(DEBUG > 1) logDebug(dragElem.id + " replace " + dragReplace.id);

        /// init and remove elements
        var dimT = getElementDimensions(dragReplace);
        var dimS = getElementDimensions(dragElem);
        var dim  = (dimT.h > dimS.h) ? dimT : dimS;
        var rep  = dropTarget.removeChild(dragReplace);
        var drag = dropSource.removeChild(dragElem);

        /// setup
        removeElementClass(dropSource, drop_hover_class);
        setStyle(dropTarget, { 'height': dim.h + "px" });
        setStyle(dropSource, { 'height': dim.h + "px" });
        dropSource.appendChild(rep);
        dropTarget.appendChild(drag);
        rep.nvsHomeElement  = dropSource;
        drag.nvsHomeElement = dropTarget;

        /// remember order
        if(dropSource.id && dropTarget.id) {
            var i1 = parseInt( /_(\d+)$/.exec(dropSource.id)  [1] );
            var i2 = parseInt( /_(\d+)$/.exec(dropTarget.id)  [1] );
            var n1 = parseInt( /_(\d+)$/.exec(dragElem.id)    [1] );
            var n2 = parseInt( /_(\d+)$/.exec(dragReplace.id) [1] );
    
            novus[dragElem.groupName].feedOrder[i1] = n2;
            novus[dragElem.groupName].feedOrder[i2] = n1;
        }
    }
};

/*
 * Finds drag-elements and sets them up with drop-elements.
 * Arguments:
 *  groupName:       the name of this group of d&d
 *  dropAcceptClass: the className that the drop accepts.
 *  addDragExtra:    function that is called each time a dragElem is added.
 */
setupDragDropRelations = function(args) {

    /// init
    var drags = [];
    var drops = [];
    var order = [];
    var name  = args.groupName;
    var i;

    /// init novus[name] obj
    if(!novus[name])           { novus[name]           = {} }
    if(!novus[name].feedOrder) { novus[name].feedOrder = [] }
    if(!novus[name].feedMore)  { novus[name].feedMore  = {} }

    /// find and replace drags with drops
    i = 0;
    while(dragElem = $(name + "_" + i)) {

        var dropElem = document.createElement('DIV');
        var dropObj  = new Droppable(dropElem, {
                           'accept':  args.dropAcceptClass,
                           'onhover': dropHover
                       });
        var pElem    = dragElem.parentNode;
        var rElem    = pElem.replaceChild(dropElem, dragElem)
    
        dropElem.className = 'droppable';
        dropElem.id        = name + "_drop_" + i;

        drags.push(rElem);
        drops.push(dropElem);
        order.push(i++);
    }

    /// get predefined order
    if(novus[name] && novus[name].feedOrder.length == order.length) {
        order = novus[name].feedOrder;
    }
    else if(order.length) {
        novus[name].feedOrder = order;
    }

    /// add drags
    forEach(order, function(i) {

        /// init
        var dragElem = drags[i];
        var dropElem = drops.shift();
        var dragObj  = new Draggable(dragElem, {
                           'scroll': window,
                           'handle': 'drag-handle'
                       });

        /// call custom function
        if(args.addDragExtra) {
            args.addDragExtra(dragElem, dropElem);
        }

        /// setup drag
        dragElem.nvsHomeElement = dropElem;
        dragElem.dragObj        = dragObj;
        dragElem.groupName      = name;
        dropElem.appendChild(dragElem);
    });

    /// connect handler to feedbox-dup-activator
    if(args.setupDup && $('mainmenu') && order.length) {
        var activator = A({ 'href': 'overblikk://tilpass' }, "Tilpass");
        $('mainmenu').appendChild( LI({ 'class': 'meta' }, activator) );
        connect(activator, 'onclick', setupFeedboxDups);
    }
}

/*
 * Takes an array as argument, that holds the feed order. Removes all the
 * drags first, and the re-inserts them in the correct order.
 */
reorderDrags = function(name) {

    /// init
    var order = novus[name].feedOrder;
    var drops = [];
    var drags = [];
    var i     = 0;

    /// reorder feeds in wrong positions
    while(dropElem = $(name + "_drop_" + i)) {
        var dragElem = dropElem.firstChild;
        var drag_n   = /(\d+)$/.exec(dragElem.id)[1];

        /// swap blocks
        if(order[i] != drag_n) {
            var node2 = $(name + "_" + order[i]);
            swapParentNodes(node2, dragElem);
        }

        /// loop end
        i++;
    }
}

/*
 * Takes a block and moves it in a direction or to a location.
 * Possible directions/positions: left, right, bottom, top
 *
 * TODO: Maybe add up and down direction-support?
 *       This function only supports two columns. maybe it should be more
 *       general?
 */
pushDragAround = function(block, direction) {

    /// init
    var id    = /(\d+)$/.exec(block.id)[1];
    var order = novus[block.groupName].feedOrder;
    var right_column;
    var index;
    var i, c;

    /// find feed index
    for(i = 0; i < order.length; i++) {
        if(order[i] == id) {
            index        = i;
            right_column = i % 2;
            break;
        }
    }

    /// couldn't find feed index
    if(typeof index == "undefined") { return; }

    /// set new order
    switch(direction) {
        case 'bottom':
            var current = index;
            var last    = right_column ? order.length - 1 : order.length - 2;
            var swap_me = order[index];

            while(current < order.length) {
                order[current] = order[current + 2];
                current += 2;
            }
            order[last] = swap_me;
            break;
        case 'top':
            var current = index;
            var first   = right_column ? 1 : 0;
            var swap_me = order[index];
            while(current > 0) {
                order[current] = order[current - 2];
                current -= 2;
            }
            order[first] = swap_me;
            break;
        case 'left':
            if(right_column) {
                var tmp          = order[index];
                order[index]     = order[index - 1];
                order[index - 1] = tmp;
            }
            break;
        case 'right':
            if(!right_column) {
                var tmp          = order[index];
                order[index]     = order[index + 1];
                order[index + 1] = tmp;
            }
            break;
        default:
            return; // unknown direction
    }

    /// the end
    if(DEBUG > 1) logDebug("Moving feed (" + index + ") to " + direction);
    reorderDrags(block.groupName);
}


/*############################################################################
 *                           Feedbox actions
 */

/*
 * Setup feed order, drag+drop handlers and shows maincol at end.
 */
onloadFeedboxSetup = function() {

    /// init block object
    if(!novus[BLOCK_NAME].feedMore) {
        novus[BLOCK_NAME].feedMore = {};
    }

    /// setup d&d
    setupDragDropRelations({
        'setupDup': true,
        'groupName': BLOCK_NAME,
        'dropAcceptClass': ['draggable'],
        'addDragExtra': function(dragElem, dropElem) {
            var feeditem_list = getFirstElementByTagAndClassName(
                                    'UL', 'feeditems', dragElem
                                );
            var tools         = feedToolboxDoc(dragElem);

            dragElem.insertBefore(tools, feeditem_list);
            setupRelatedActivators(getElementsByTagAndClassName(
                'LI', 'sourcelist-container', dragElem
            ));

            if(novus[BLOCK_NAME].feedMore[dragElem.id] == 1) {
                removeElementClass(dragElem, 'many-feeditems');
            }
            if(novus[BLOCK_NAME].feedMore[dragElem.id] == 0) {
                addElementClass(dragElem, 'many-feeditems');
            }
        }
    });
}

/*
 * Makes feed toolbox popup at the right place, with the correct attributes.
 * The attributes are gathered from popup._feed, when an action is taken.
 */
setupFeedToolboxPopup = function(event) {
    event.stop();

    /// init
    var activator = event.src();
    var popup     = feedToolboxPopupDoc();
    var pos       = getElementPosition(activator.parentNode);
    var feed      = getFirstParentByTagAndClassName(activator, 'DIV', 'feed');
    var many_few  = $('toolbox_item_more');

    /// who's your daddy
    popup._feed = feed;

    /// toggle many_few feed link text/url/classname
    if(feed.className.indexOf('many-feeditems') >= 0) {
        many_few.href      = many_few.href.replace(/3items/, '10items');
        many_few.className = 'few';
        many_few.innerHTML = "Tre nyheter";
    }
    else {
        many_few.href      = many_few.href.replace(/10items/, '10items');
        many_few.className = 'many';
        many_few.innerHTML = "Ti nyheter";
    }

    /// the end
    setElementPosition(popup, pos);
    showElement(popup);
}

/*
 * Creates the feed toolbox popup, instead of having it inline in the html-
 * document.
 */
feedToolboxPopupDoc = function() {

    if($(feedtools_popup_id)) { return $(feedtools_popup_id); }

    /// init
    var doc   = UL({ 'class': 'options', 'id': feedtools_popup_id });
    var tools = [
                    'explore',         'Utforsk',
                    'more',            'Ti nyheter',
                    'move-box-top',    'Flytt helt til toppen',
                    'move-box-right',  'Flytt til høyre',
                    'move-box-left',   'Flytt til Venstre',
                    'move-box-bottom', 'Flytt helt til bunnen'
                ];

    /// add list elements
    for(i = 0; i < tools.length; i += 2) {
        var _li = LI({});
        var _a  = A({ 'class': tools[i] }, tools[i+1]);

        if(tools[i].indexOf('move') == 0) {
            connect(_a, 'onclick', function(event) {
                event.preventDefault();
                var direction = /\-(\w+)$/.exec(event.src().href)[1];
                pushDragAround(doc._feed, direction);
            });
        }
        else if(tools[i] == 'more') {
            connect(_a, 'onclick', detailedFeedItems);
        }
        else if(tools[i] == 'explore') {
            connect(_a, 'onclick', function(event) {
                event.preventDefault();
                var heading            = getFirstElementByTagAndClassName(
                                             'H3', null, doc._feed
                                         );
                var url                = heading.firstChild.href;
                document.location.href = url;
            });
        }

        _a.setAttribute('href', 'overblikk://' + tools[i]);
        _a.setAttribute('id', 'toolbox_item_' + tools[i]);
        _li.appendChild(_a);
        doc.appendChild(_li);
    }

    /// connect handles
    connect(doc, 'onmouseleave', hideDelayed);
    connect(doc, 'onmouseover', cancelHideDelayed);
    connect(doc, 'onclick', function() { hideElement(doc); });

    /// the end
    document.body.appendChild(doc);
    return doc;
}

/*
 * Creates the feed toolbox elements to place in the feed-header, instead of
 * having it inline in the html-document.
 */
feedToolboxDoc = function(_feed) {

    /// init
    var feed         = _feed;
    var doc          = UL({ 'class': 'tools' });
    var feed_heading = getFirstElementByTagAndClassName('H3', null, feed);
    var settings_a   = A({ 'href': 'overblikk://valg' }, 'Valg');
    var many_few     = A({
                           'href': 'overblikk://many_few',
                           'title': 'Bytt mellom tre og ti saker'
                       }, ' ');

    /// connect handles
    connect(feed_heading, 'onclick', followClick);
    connect(settings_a, 'onclick', setupFeedToolboxPopup);
    connect(many_few, 'onclick', detailedFeedItems);

    /// append to doc
    appendChildNodes(doc,
        LI({ 'class': 'many-few' }, many_few),
        LI({ 'class': 'box-settings' }, settings_a)
    );

    /// the end
    return doc;
}

/*
 * Toggles between detailed/few to overview/many feed-items
 */
detailedFeedItems = function(event) {
    event.preventDefault();

    /// init
    var obj      = event.src ? event.src() : event;
    var dragElem = obj.parentNode.parentNode._feed
                || getFirstParentByTagAndClassName(obj, 'DIV', 'feed');

    /// overview+many
    if(dragElem.className.indexOf('many-feeditems') >= 0) {
        removeElementClass(dragElem, 'many-feeditems');
        novus[BLOCK_NAME].feedMore[dragElem.id] = 1;
    }

    /// detailed+few
    else {
        addElementClass(dragElem, 'many-feeditems');
        novus[BLOCK_NAME].feedMore[dragElem.id] = 0;
    }
}

/*
 * Loops the feed elements, and creates duplicates to be shown in a popup.
 *
 * TODO: Before the forEach loop is completed, some duplicates *may* have
 * the same id, which is not valid acording to spec.
 */
setupFeedboxDups = function(event) {

    /// init
    event.stop();
    var activator = event.src();
    var doc       = feedboxDupsDoc();
    var pos       = getElementPosition(activator)
    var drops     = getElementsByTagAndClassName('DIV', 'popupdrop', doc);
    var order     = novus[BLOCK_NAME].feedOrder;
    var i;

    /// setup id on drags
    i = 0;
    forEach(drops, function(dropDup) {
        var f       = order[i];
        var dragDup = dropDup.firstChild;
        var feed    = $(BLOCK_NAME + "_" + f);
        var heading = getFirstElementByTagAndClassName('A', null, feed);
        var color   = getStyle(heading, 'color');

        setStyle(dragDup, { 'background-color': color });
        dragDup.id        = drag_dup_id_prefix + f;
        dragDup.innerHTML = heading.innerHTML;
        i++;
    });

    /// fix position
    pos.x -= 200;

    /// the end
    showPopupLayer(doc, pos);
}

/*
 * Creates the feedbox duplicate popup, instead of having it inline in the
 * html-document.
 */
feedboxDupsDoc = function() {

    var setup_feeds_id = 'setup_feedboxes';

    /// check for existing document
    if($(setup_feeds_id)) { return $(setup_feeds_id); }

    /// init
    var doc           = DIV({ 'id': setup_feeds_id });
    var cancel_button = INPUT({ 'value': 'Avbryt',    'type': 'button' });
    var submit_button = INPUT({ 'value': 'Lagre',     'type': 'submit' })
    var reset_button  = INPUT({ 'value': 'Nullstill', 'type': 'submit' })
    var i;

    /// add to document
    appendChildNodes(doc,
        H2('Tilpass'),
        P('Dra boksene dit du vil ha de og trykk lagre')
    );

    /// setup drop and draggables
    i = 0;
    while(1) {
        if(! $(BLOCK_NAME + "_" + i) ) { break; }
        var drag     = DIV({ 'class': 'popupdrag' });
        var drop     = DIV({ 'class': 'popupdrop' });
        var dragObj  = new Draggable(drag);
        var dropObj  = new Droppable(drop, {
                           'accept':  drop_dup_accept_class,
                           'onhover': dropHover
                       });

        drag.nvsHomeElement = drop;
        drag.dragObj        = dragObj;

        drop.appendChild(drag);
        doc.appendChild(drop);
        i++;
    }

    /// add to document
    appendChildNodes(doc,
        DIV( { 'class': 'clear' }),
        submit_button,
        cancel_button,
        reset_button
    );

    /// setup signals
    connect(submit_button, 'onclick', function() {
        var drags = getElementsByTagAndClassName('DIV', 'popupdrag', doc);
        var order = [];

        forEach(drags, function(elem) {
            var id = /(\d+)$/.exec(elem.id)[1];
            order.push(id);
        })

        novus[BLOCK_NAME].feedOrder = order;
        reorderDrags(BLOCK_NAME)
        hidePopupLayer();
    });
    connect(cancel_button, 'onclick', hidePopupLayer);
    connect(reset_button, 'onclick', function() {
        var drags = getElementsByTagAndClassName('DIV', 'popupdrag', doc);
        var i     = 0;
        var feed  = $(BLOCK_NAME + "_" + i);

        while(feed) {
            var heading = getFirstElementByTagAndClassName('A', null, feed);
            var color   = getStyle(heading, 'color');

            drags[i].id        = drag_dup_id_prefix + i;
            drags[i].innerHTML = heading.innerHTML;
            setStyle(drags[i], { 'background-color': color });

            feed = $(BLOCK_NAME + "_" + (++i));
        }
    });

    /// the end
    document.body.appendChild(doc);
    return doc;
} 


/*############################################################################
 *                    Main search and advanced search
 */

/*
 * setup main and advanced search
 */

onloadAdvancedSearch = function(block) {

    /// init
    var fieldsets = getElementsByTagAndClassName(
                        'FIELDSET', 'select-list-wrapper', block
                    );
    var activator = $('advancedsearch_link');
    var c         = 0;

    /// setup wannabe selectlists
    forEach(fieldsets, function(fieldset) {
        c++;

        if(BLOCK_NAME != 'advancedsearch') {
            var p_node = fieldset.parentNode;
            var legend = getFirstElementByTagAndClassName(
                                'LEGEND', null, fieldset
                            );
            var txt    = legend.innerHTML;
            var anchor = A({ 'name': txt }, 'Alle ' + txt);

            fieldset.nvsLegend    = anchor;
            anchor.href           = 'overblikk://' + txt;
            anchor.nvsListWrapper = fieldset;
            anchor.nvsList        = getFirstElementByTagAndClassName(
                                        'UL', null, fieldset
                                    );

            p_node.appendChild(anchor);
            changeSelectListLegend(anchor);
            connect(anchor, 'onclick', showAdvancedSearchDoc);
            hideElement(fieldset);
        }

        advancedSearchButtons(fieldset);
        setupWannabeSelectList(fieldset);
    });

    /// add show/hide button
    return; // disable this for now...
    if(activator && BLOCK_NAME != 'advancedsearch') {
        activator.href = 'overblikk://advanced_search';
        connect(activator, 'onclick', function(event) {
            event.stop();
            toggle($('advancedsearch'), 'appear', {
                'duration': 0.7
            });
        });
    }
}

/*
 * display a list of options you can select from, when spesifying an
 * advanced search.
 */

showAdvancedSearchDoc = function(event) {

    /// init
    event.stop();
    var activator = event.src();
    var wrap      = activator.nvsListWrapper;
    var pos, dim;

    showPopupLayer(wrap);
    threeStateShowElement(wrap, "-block");

    /// get dim
    pos   = getElementPosition(activator);
    dim   = getElementDimensions(wrap);
    pos.y = pos.y - dim.h + 60;

    if(pos.y < 0) pos.y = 0;

    /// the end
    threeStateShowElement(wrap, "block");
    setElementPosition(wrap, pos);
}

/*
 * Adds select all/noen buttons to the fieldset
 */
advancedSearchButtons = function(doc) {

    /// init
    var inputs = doc.getElementsByTagName('input');
    var b      = {};
    var i;

    /// check all/none function
    var checkOptionBoxes = function(c) {
        forEach(getElementsByTagAndClassName('INPUT', null, doc),
            function(box) {
                box.checked = c;
                toggleLabelClass(box);
            }
        );
    }

    /// init
    for(i = inputs.length - 1; i > 0; i--) {
        var cn = inputs.item(i).className;
        switch(cn) {
            case 'all':
            case 'none':
            case 'close':
                b[cn] = inputs.item(i);
        }
    }

    /// connect
    if(b.all) {
        connect(b.all, 'onclick', function() { checkOptionBoxes(1) });
        setStyle(b.all, { 'display': 'inline' });
    }
    if(b.none) {
        connect(b.none,  'onclick', function() { checkOptionBoxes(0) });
        setStyle(b.none, { 'display': 'inline' });
    }
    if(b.close && $('utforsker')) {
        connect(b.close, 'onclick', function() {
            changeSelectListLegend(doc.nvsLegend);
            hidePopupLayer();
        });
        setStyle(b.close, { 'display': 'inline' });
    }
}

/*############################################################################
 *                             Sources list
 */

/*
 * Toggles the visibility of a or many category-items. The function takes
 * one argument: Either a single id or a list of ids, and appends/removes
 * the "expanded" className. It also saves the expanded category-items to a
 * cookie.
 */
toggleSourceblockVisibility = function(change_list) {

    /// init
    var active = novus.category.active;
    var blocks = getElementsByTagAndClassName(
                     'LI', 'category-item', $('category_list')
                 );

    /// make sure input is a list
    if(!isArrayLike(change_list)) {
        change_list = [change_list];
    }

    /// show or hide
    forEach(change_list, function(i) {
        if(!blocks[i]) { return; }

        var site_list = getFirstElementByTagAndClassName(
                            'UL', null, blocks[i]
                        );

        if(!site_list) { return; }

        /// hide
        if(hasElementClass(blocks[i], 'expanded')) {
            blindUp(site_list, {
                'duration': 0.3,
                'afterFinish': function() {
                                   removeElementClass(blocks[i], 'expanded');
                               }
            });
            delete active[i];
        }

        /// show
        else {
            addElementClass(blocks[i], 'expanded');

            /* blindDown hack from a top secret project ;) */
            setStyle(site_list, {
                'overflow': 'hidden',
                'display': 'block',
                'height': 0
            });
            blindDown(site_list, {
                'duration':  0.3,
                'restoreAfterFinish': false,
                'afterFinish': function() {
                                   setStyle(site_list, { 'height': 'auto' });
                               },
                'scaleMode': { 'originalHeight': site_list.scrollHeight }
            });
            active[i] = 1;
        }
    });
}

/*
 * Searches through the category-list and displays the one matching obj.value
 * in it's own tab. Also saves the query to a cookie.
 */
searchSourceItems = function(obj) {

    /// init
    var query       = obj.value.toLowerCase();
    var search_list = $('category_search');
    var various_ul  = UL({ 'class': 'links' });
    var empty_li    = categorySearchTabDoc('empty_li');

    novus.category.query  = query;
    search_list.innerHTML = "";

    /// init list
    if(!obj.sources) { setupSourcesList(obj); }

    /// find ok sources
    if(query) {
        forEach(obj.sources, function(s) {
            if(s.text.indexOf(query) != -1) {
                search_list.appendChild(empty_li);
                various_ul.appendChild( LI({}, s.node.cloneNode(true)) );
            }
        });
        forEach(obj.categories, function(c) {
            if(c.text.indexOf(query) != -1) {
                search_list.appendChild( c.node.parentNode.cloneNode(true) );
            }
        });
    }

    /// show search tab
    if(various_ul.firstChild || search_list.innerHTML) {
        if(various_ul.firstChild) {
            var empty_li_ul = getFirstElementByTagAndClassName(
                                  'UL', 'links', empty_li
                              );
            empty_li_ul.parentNode.replaceChild(various_ul, empty_li_ul);
        }
        swapVisibility($('category_search'), $('category_list'));
    }

    /// no results found
    else {
        search_list.appendChild(empty_li);
    }
}

/*
 * Creates a sorted category-list, where each elements is an object:
 * { 'node': li_element, 'text': li_element_text }
 */
setupSourcesList = function(obj) {

    /// init
    var links      = getElementsByTagAndClassName(
                         'A', null, $('category_list')
                     );
    var sources    = {};
    var categories = {};
    obj.sources    = [];
    obj.categories = [];

    /// fix sources list
    forEach(links, function(n) {
        if(n.className == 'source') {
            var text  = n.firstChild.nodeValue;
            sources[text] = { 'node': n, 'text': text.toLowerCase() };
        }
        else if(n.className == 'category-activator') {
            var text         = n.firstChild.nodeValue;
            categories[text] = { 'node': n, 'text': text.toLowerCase() };
        }
    });

    /// sort the list
    var _by_text = function(a, b) {
                       if(a.text < b.text) return -1;
                       if(a.text > b.text) return  1;
                                           return  0;
                   }
    obj.sources    = values(sources).sort(_by_text);
    obj.categories = values(categories).sort(_by_text);
}

/*
 * Creates a new tab in the category-block, for the category search-results.
 */
categorySearchTabDoc = function(part) {

    /// init
    var doc      = $('category_search');
    var empty_li =
        LI({ 'class': 'category-item' },
            SPAN({ 'class': 'category-activator' }, 'Søkeresultat'),
            UL({ 'class': 'links' },
                LI({}, "Tomt søkeresultat")
            )
        );

    /// create doc
    if(!doc) {
        doc = LI({},
            A({
                'href':  'overblikk://category_search_tab',
                'class': 'tab-activator tab-search'
            }, "Søk"),
            UL({ 'id': 'category_search', 'class': 'tab' }, empty_li)
        );
    }

    /// doc exists
    else {
        doc = doc.parentNode;
    }

    /// return parts of the doc
    if(part) {
        if(part == 'empty_li') {
            return empty_li;
        }
    }

    /// return whole doc
    else {
        return doc;
    }
}

/*
 * setup show+hide sources boxes, tabs and search
 */
onloadSourceList = function() {

    /// int
    var settings = novus['category'];

    if(!novus['category']) {
        novus['category'] = {
            'query':      '',
            'active':     {},
            'active_tab': 'list'
        };
        settings = novus.category;
    }
    else {
        toggleSourceblockVisibility( keys(novus.category.active) );
    }

    /* setup show+hide category */

    /// init
    var container      = getFirstParentByTagAndClassName(
                             $('category_list'), 'DIV', 'toolbox'
                         );
    var cat_index      = 0;
    var cat_activators = getElementsByTagAndClassName(
                             'A', 'category-activator', container
                         );

    /// connect handles
    forEach(cat_activators, function(_A) {
        var i = cat_index++;
        connect(_A, 'onclick', function(event) {
            event.stop();
            toggleSourceblockVisibility(i);
        });
    });

    /* setup list+search tab */

    /// init
    var list_tab_activator   = A({
                                   'href':  'overblikk://category_list_tab',
                                   'class': 'tab-activator tab-list'
                               }, "Liste");
    var search_tab_doc       = categorySearchTabDoc();
    var search_input         = INPUT({
                                   'autocomplete': 'off',
                                   'value': novus.category.query
                               });
    var search_tab_activator = search_tab_doc.firstChild;
    var search_timer;

    /// connect handlers
    connect(list_tab_activator, 'onclick', function(event) {
        event.stop();
        swapVisibility($('category_list'), $('category_search'));
        settings.active_tab = 'list';
    });
    connect(search_tab_activator, 'onclick', function(event) {
        event.stop();
        searchSourceItems(search_input);
        swapVisibility($('category_search'), $('category_list'));
        settings.active_tab = 'search';
    });
    connect(search_input, 'onkeyup', function() {
        search_timer = setTimeout(function() {
                           searchSourceItems(search_input)
                       }, 200);
    });
    connect(search_input, 'onkeydown', function() {
        clearTimeout(search_timer);
    });

    /// append document elements
    var pUL           = getFirstElementByTagAndClassName(
                            'UL', null, container
                        );
    var category_list = $('category_list');

    category_list.parentNode.insertBefore(list_tab_activator, category_list);
    pUL.parentNode.insertBefore(search_input, pUL);
    pUL.parentNode.insertBefore(
        P({}, "Begynn å skrive, så leter vi for deg."), pUL
    );
    pUL.appendChild(search_tab_doc);

    /// show correct tab
    if(settings.active_tab == 'list') {
        swapVisibility($('category_list'), $('category_search'));
    }
    else {
        swapVisibility($('category_search'), $('category_list'));
        searchSourceItems(search_input);
    }
}


/*############################################################################
 *                     Bookmark and share with friends
 */

/*
 * Sets up bookmark and share with friends elements, if there is a
 * div with id=utforsker or id=related.
 */
onloadBookmarkAndShare = function(parent_node) {

    /// init
    var share_txt    = 'Del med venner';
    var bookmark_txt = 'Bokmerk';
    var toolboxes    = getElementsByTagAndClassName(
                           'UL', 'explorermetatools', parent_node
                       );

    /// loop toolboxes
    forEach(toolboxes, function(box) {

        /// init
        var share_link       = getFirstElementByTagAndClassName(
                                   'LI', 'share', box
                               ).firstChild;
        var bookmark_link    = getFirstElementByTagAndClassName(
                                   'LI', 'bookmark', box
                               ).firstChild;
        var source_activator = getElementsByTagAndClassName(
                                   'LI', 'sourcelist-container', box 
                               );

        /// add handlers
        if(bookmark_link) {
            connect(bookmark_link, 'onmouseover', setupBookmarkPopup);
            connect(bookmark_link, 'onmouseleave', setupBookmarkPopup);
        }
        if(share_link) {
            connect(share_link, 'onclick', setupShareWithFriend)
        }
        if(source_activator) {
            setupRelatedActivators(source_activator);
        }
    });
}

/*
 * Puts the bookmark information-popup where it's supposed to be, and hides
 * it when the mouse leaves the popup.
 */
setupBookmarkPopup = function(event) {

    /// hide
    if(event.type() == 'mouseleave') {
        var doc = bookmarkPopupDoc();
        hideDelayed(doc, 250);
        return;
    }

    /// init
    var activator = event.src();
    var doc       = bookmarkPopupDoc();
    var doc_dim   = getElementDimensions(doc);
    var doc_pos   = getElementPosition(activator);

    /// fix position
    doc_pos.x -= doc_dim.w / 2 - 24;
    doc_pos.y -= doc_dim.h;
    if(doc_pos.x < 0) { doc_pos.x = 0; }

    /// the end
    setElementPosition(doc, doc_pos);
    showElement(doc);
}

/*
 * Creates the bookmark popup, instead of having it inline in the html-
 * document.
 */
bookmarkPopupDoc = function() {

    /// check for existing document
    if($(bookmark_popup_id)) { return $(bookmark_popup_id); }

    /// init
    var doc       = DIV({ 'id': bookmark_popup_id });
    var paragraph = P();

    /// create document
    appendChildNodes(paragraph,
        createDOM("SPAN", { 'class': 'heading' }, "Bokmerk lenke"),
        "For å bokmerke lenker, må du ",
        createDOM("STRONG", {}, "registrere"),
        " deg. Det er enkelt og helt ",
        createDOM("STRONG", {}, "gratis!")
    );

    /// the end
    doc.appendChild(paragraph);
    document.body.appendChild(doc);
    return doc;
}

/*
 * Sets up the share-with-friend form-elements with the correct values.
 * Appends the popup to popup-layer and positions the popup.
 */
setupShareWithFriend = function(event) {

    /// init
    event.stop();
    var activator = event.src();
    var popup     = shareWithFriendDoc();
    var pos       = getElementPosition(activator);
    var dim;

    showPopupLayer(popup);
    threeStateShowElement(popup, "-block");

    /// get dim
    dim    = getElementDimensions(popup);
    pos.x -= dim.w / 2;
    pos.y -= dim.h + 2;
    if(pos.x < 0) { pos.x = 0; }

    /// fill in form
    var pNode   = activator.parentNode.parentNode.parentNode;
    var heading = getFirstElementByTagAndClassName(
                     'H2', null, pNode
                  ) || getFirstElementByTagAndClassName(
                     'H3', null, pNode
                  );
    var anchor = getFirstElementByTagAndClassName('A', null, heading);
    var f      = getFirstElementByTagAndClassName('FORM', null, popup);

    f.elements['title'].value = anchor.firstChild.nodeValue;
    f.elements['url'].value   = anchor.href;

    /// the end
    threeStateShowElement(popup, "block");
    setElementPosition(popup, pos);
}

/*
 * Creates the share-with-friend popup, instead of having it inline in the
 * html-document.
 */
shareWithFriendDoc = function() {

    /// check for existing document
    if($(share_popup_id)) { return $(share_popup_id); }

    /// init
    var share_url = BASEURL + '/html/tellafriend/'
    var doc       = DIV({ 'id': share_popup_id });
    var form      = FORM({ 'method': 'post', 'action': share_url });

    var recepient = INPUT({
                        'type':  'text',
                        'name':  'recepient',
                        'value': 'Din venns epost',
                        'class': 'text-input'
                    });
    var sender    = INPUT({
                        'type':  'text',
                        'name':  'sender',
                        'value': 'Din epost',
                        'class': 'text-input'
                    });
    var close     = INPUT({ 'type': 'button', 'value': 'Lukk' })

    setToggleInputValue(recepient, 'Din venns');
    setToggleInputValue(sender, 'Din epost');
    connect(close, 'onclick', hidePopupLayer);

    /// create documen tellafriend
    appendChildNodes(form,
        INPUT({ 'type': 'hidden', 'name': 'title' }),
        INPUT({ 'type': 'hidden', 'name': 'url' }),
        createDOM("H6", { 'class': 'heading' }, "Del med venn"),
        LABEL({}, "Din venns epost"), recepient,
        LABEL({}, "Din epost"),       sender,
        LABEL({}, "Din melding"),     TEXTAREA({ 'name': 'message' }),
        DIV({ 'class': 'clear' }),
        INPUT({ 'type': 'submit', 'value': 'Send' }),
        close
    );

    /// the end
    doc.appendChild(form);
    document.body.appendChild(doc);
    return doc;
}

/*
 * Fills the popup with the correct related-list and display it at the
 * right position. It is hidden on mouseleave.
 * This function also creates the document, because it's a simple document
 * structure.
 */
setupRelatedList = function(event, list) {

    /// init
    var activator  = event.src();
    var list_clone = list.firstChild.cloneNode(true);
    var pos        = getElementPosition(activator);
    var doc        = $(sources_popup_id);

    /// make doc
    if(!doc) {
        doc = DIV({ 'id': sources_popup_id, 'class': 'sourcelist' });
        document.body.appendChild(doc);
        connect(doc, 'onmouseleave', hideDelayed);
        connect(doc, 'onmouseover', cancelHideDelayed);
    }

    doc.innerHTML = "";
    doc.appendChild(list_clone);
    threeStateShowElement(doc, '-block');

    var doc_dim       = getElementDimensions(doc);
    var activator_dim = getElementDimensions(activator);
    pos.x = pos.x - doc_dim.w / 2 + activator_dim.w / 2;
    pos.y = pos.y - doc_dim.h + 10;
    if(pos.x < 0) { pos.x = 0; }

    /// the end
    setElementPosition(doc, pos);
    threeStateShowElement(doc, 'block');
}

/*
 * Replaces H5 with A, in the relatedlists, and connects an onclick action
 * to it.
 */
setupRelatedActivators = function(relatedlists) {

    if(!relatedlists.length) { return; };

    forEach(relatedlists, function(s) {

        /// init
        var heading = getFirstElementByTagAndClassName('H5', null, s);
        var list    = getFirstElementByTagAndClassName('DIV', null, s);
        var anchor  = A(
                        { 'href': 'overblikk://sources', 'class': 'h5' },
                        heading.firstChild
                      );

        s.replaceChild(anchor, heading);

        connect(anchor, 'onclick', function(event) {
            event.stop();
            setupRelatedList(event, list);
        });
    });
}


/*############################################################################
 *                        Setup sortable tables
 */

/*
 * Add sortable action to table(s)
 */
onloadSortableTable = function(table) {
    var sortableManager = new SortableManager();
    sortableManager.initWithTable(table);
}

/*############################################################################
 *                          Master On/Unload
 */

/*
 * Master onload
 */
connect(window, 'onload', function() {

    /// init
    var utforsker_related = $('utforsker') || $('related');
    var tmp;

    /// init novus sub object
    if(!novus[BLOCK_NAME]) {
        novus[BLOCK_NAME] = {};
    }

    /// setup feed boxes
    onloadFeedboxSetup();

    /// setup standard search input
    setToggleInputValue($('search_input'), 'Nyhetss');

    /// setup advanced search
    if(tmp = $('advancedsearch')) {
        onloadAdvancedSearch(tmp);
    }

    /// setup related table
    if(tmp = $('related_table')) {
        onloadSortableTable(tmp);
    }

    /// setup bookmark and share with friend popups
    if(utforsker_related) {
        onloadBookmarkAndShare(utforsker_related);
    }

    /// setup the sources list in the right side panel
    onloadSourceList();

    /// the end
    hideElement('loading');
    threeStateShowElement('maincol', 'block');
});

/*
 * Master onunload
 */
connect(window, 'onunload', function() {

    if(document.location.href.indexOf('forgetme') == -1) {

        /// save date from this specific block/page to cookie
        if(tmp = novus[BLOCK_NAME]) {
            delete novus[BLOCK_NAME];
            Cookie.create(BLOCK_NAME, serializeJSON(tmp) );
        }
    
        /// save general data to cookie
        Cookie.create(NOVUS_COOKIE_NAME, serializeJSON(novus) );
    }
});


/*############################################################################
 *                           Cookie functions
 *          Adapted from http://www.quirksmode.org/js/cookies.html
 */

/* */
Cookie = function() {
}

/* */
Cookie.create = function(name, value, options) {

    var expires = "";
    var path    = ";path=/";

    if(!options) { options = { 'days': 365*2 }; }

    if(options && options.days) {
        var date = new Date();
        date.setTime(date.getTime() + (options.days * 24*60*60*1000));
        expires = "; expires=" + date.toGMTString();
    }
    if(options && options.path) {
        path = "; path=" + options.path;
    }

    if(DEBUG > 0) logDebug("(create) " + name + "=" + value + expires + path);
    document.cookie = name + "=" + value + expires + path;
};

/* */
Cookie.read = function(name) {
    name  += "=";
    var ca = document.cookie.split(';');

    for(var i = 0; i < ca.length; i++) {
        var c = ca[i];
            c = c.ltrim();
        if (c.indexOf(name) == 0) {
            var value = c.substring(name.length, c.length);
            if(DEBUG > 0) logDebug("(read) " + name + value);
            return value;
        }
    }

    return null;
};

/* */
Cookie.erase = function(name) {
    Cookie.create(name, "", { 'days': -1 });
};

/*
 * Reads a cookie and sets the master object 'novus' to it's value.
 */
Cookie.init = function() {

    /// init
    var tmp;

    if(0) {
        Cookie.erase(NOVUS_COOKIE_NAME);
        Cookie.erase(BLOCK_NAME);
    }

    /// read general cookie
    if(tmp = Cookie.read(NOVUS_COOKIE_NAME)) {
        novus = evalJSON(tmp);
    }

    /// read site specific cookie
    if(tmp = Cookie.read(BLOCK_NAME)) {
        novus[BLOCK_NAME] = evalJSON(tmp);
    }

    if(DEBUG > 1) logDebug(serializeJSON(novus));
}

/*
 * Temporarly function for converting and removing the old cookie-formats.
 * Should be removed after x-while...
 */
Cookie.convertOld = function() {

    /// init
    var tmp;

    /* category settings */

    novus.category  = { 'active': {}, 'active_tab': '', 'query': '' };

    if(tmp = Cookie.read('simple_frontend_category_search')) {
        Cookie.erase('simple_frontend_category_search');
        novus.category.query = tmp;
    }
    if(tmp = Cookie.read('simple_frontend_category_tab')) {
        Cookie.erase('simple_frontend_category_tab');
        novus.category.active_tab = tmp;
    }
    if(tmp = Cookie.read('simple_frontend_category_list')) {
        Cookie.erase('simple_frontend_category_list');
        novus.category.active = map(function(i) {
                                    return { i: 1 };
                                }, tmp.split(','));
    }

    /* index feed order */

    if(BLOCK_NAME == 'index') {
        novus.index = { 'order': [] };
    
        if(tmp = Cookie.read('simple_frontend_feed_order')) {
            Cookie.erase('simple_frontend_feed_order');
            novus.indexFeed.feedOrder = tmp.split(',');
        }
    }
}

/*
 * Do some initial cookie read/writing
 */
Cookie.convertOld();
Cookie.init();

