/*
opendots.js.php

Copyright (c) Gareth Hadfield 2008

The main Opendots code. Contains general initialisation, and code for the dot
objects.

See also coding_standards.txt
*/

var do_node_properties_if_active;
var getPropertiesDiv;
var refresh_opendots_list;

/* Initialisation on document load */

function opendot_stop_click(aEvent){
  if(!aEvent){aEvent=event;}

  var aOnClick = function(aEvent){
    if(!aEvent){aEvent=event;}
    hide_all_menus();

    // show document menu
    if((mouse_button(aEvent)==RIGHT_MOUSE_BUTTON)){
      show_menu(aEvent, opendots_document_menu_id);
    }
    
    var aX = mouseX(aEvent);
    var aY = mouseY(aEvent);
    if(aX != undefined){
	    document.lastClickX = aX;
  	  document.lastClickY = aY;
    }
  };

  return(stop_click(aEvent, aOnClick));
}

function opendots_init(){
  // init after load

  document.body.id = "document.body";// never change this

  if(OPENDOTS_MODE == OPENDOTS_EDIT_MODE){
    opendots_edit_init();
  }

  init_document_methods();
}

/* On document unload */

function do_before_unload(aEvent){
  if(!aEvent){aEvent=event;}

  if(OPENDOTS_MODE == OPENDOTS_EDIT_MODE){
    var result = "Make sure you have saved your opendots page.";

    aEvent.returnValue = result;

    return(result);
  }
}

window.onbeforeunload = do_before_unload;


/* Dots */

function is_dot(aNode){
  return(aNode.is_opendot != undefined);
}

function all_dots(aParentNode, aRecurseAllAncestors){
  // return an array containing all opendot elements
  return(all_divs(aParentNode, aRecurseAllAncestors, is_dot));
}

function next_dot_left(){
  GLOBAL_DOT_LEFT += GLOBAL_DOT_LEFT_INCREMENT;
  if(GLOBAL_DOT_LEFT > GLOBAL_DOT_LEFT_LIMIT){
    GLOBAL_DOT_LEFT = GLOBAL_DOT_LEFT_INCREMENT;
  }
  return(GLOBAL_DOT_LEFT);
}

function next_dot_top(){
  GLOBAL_DOT_TOP += GLOBAL_DOT_TOP_INCREMENT;
  if(GLOBAL_DOT_TOP > GLOBAL_DOT_TOP_LIMIT){
    GLOBAL_DOT_TOP = GLOBAL_DOT_TOP_INCREMENT;
  }
  return(GLOBAL_DOT_TOP);
}

function get_next_dot_id(){
  var NEW_DOT_NAME = "Dot_";
  var aDotIndex = 0;
  var aDots = all_dots();
  for(var i=0; i<aDots.length; i++){
    var aDotId = aDots[i].id;
    if(aDotId.substr(0, NEW_DOT_NAME.length)==NEW_DOT_NAME){
      aDotIndex = Math.max(aDotIndex, parseInt(aDotId.substr(NEW_DOT_NAME.length, aDotId.length), 10));
    }
  }
  return(NEW_DOT_NAME + (aDotIndex+1));
}

function get_dot_ancestor(aElement){
  // return the first opendot ancestor of aElement (or undefined if none)
  var result;
  if(aElement.parentNode!=undefined){
    if(is_dot(aElement.parentNode)){
      result = aElement.parentNode;
    }
    else{
      result = get_dot_ancestor(aElement.parentNode);
    }
  }
  return(result);
}

function refresh_sub_classes(){
  if(document.loading == 0){
    // go through all the dots and re-apply their css sub_class
    var aDots = all_dots();

    for(var i=0; i<aDots.length; i++){
      var aDot = aDots[i];
      if(aDot.refreshCSS != undefined){
        aDot.refreshCSS();
      }
    }
  }
}

function change_id_in_content_html(aParent){
  var contentHTML = getAttributeText(aParent, "content");

  if((contentHTML != undefined) && (contentHTML != "")){
    var aPrefix = aParent.id + "_";

    var reText =
      "(<[^<>]* id\\s*=\\s*\"?\'?)" + // () makes this become $1
      "([^<>\"\']*\"?\'?[^<>]*>)"; // () makes this become $2

    var re = new RegExp(reText, "gi");
    contentHTML = contentHTML.replace(re, "$1"+aPrefix+"$2");

    setAttributeText(aParent, "content", contentHTML);
  }
}

function copy_attribute(aAttributeName){
  // return true if aAttributeName should be included in a duplicate dot
  return(in_array(aAttributeName, COPY_ATTRIBUTES));
}

function duplicate_dot(aSourceDot){
  // content
  var newContent = aSourceDot.getContent();

  var aNewDot = new_dot(undefined, newContent);

  // attributes
  for(var iAttribute=0; iAttribute<aSourceDot.attributes.length; iAttribute++){
    var aAttribute = aSourceDot.attributes[iAttribute];

    if(copy_attribute(aAttribute.name)){
      aNewDot.sat(aAttribute.name, aSourceDot.getAttributeText(aAttribute.name));
    }
  }

  change_id_in_content_html(aNewDot);

  var aSourceChildDots = all_dots(aSourceDot, false);

  for(var i=0; i<aSourceChildDots.length; i++){
    var aSourceChildDot = aSourceChildDots[i];
    var aNewChild = duplicate_dot(aSourceChildDot);
    var aZIndex = aSourceChildDot.getAttributeText("z_index");
    var aParentId;

    if(aSourceChildDot.parentNode==aSourceDot){
      // This dot is a child of aSourceDot (all dots that have dot parents)
      aParentId = aNewDot.getAttributeText("id");
    }
    else{
      // This dot is not a child of aSourceDot (all dots that do not have dot parents)
      aParentId = aNewDot.id + "_" + aSourceChildDot.parentNode.id;
    }
    aNewChild.sat("parent_id", aParentId);
    aNewChild.sat("z_index", aZIndex);
  }


  // todo - this was added for benefit of controls - need to test that this has no side effects
  aNewDot.doOnAfterLoad();

  if(OPENDOTS_MODE==OPENDOTS_EDIT_MODE){
    refresh_opendots_list();

	  do_node_properties_if_active(aNewDot);
  }
  
  return(aNewDot);
}

function copy_dot(aSourceDot){
  // mark dot ready for paste
  document.globalCopySource = aSourceDot;
}

function paste_dot(aParent){
  // create duplicate of copied dot
  if(document.globalCopySource != undefined){
    var aNewDot = duplicate_dot(document.globalCopySource);
    aNewDot.sat("parent_id", aParent.id);
    keep_in_parent(aNewDot);
  }
}

function remove_child_dots(aParent){
  // remove children and return them in an array

  var aDots = all_dots(aParent, true);
  var i;
  var aDot;
  // save z_index
  for(i=0; i<aDots.length; i++){
    aDot = aDots[i];
    aDot.oldZIndex = aDot.getAttributeText("z_index");
  }

  // remove children
  for(i=0; i<aDots.length; i++){
    aDot = aDots[i];
    aDot.remove();
  }

  return(aDots);
}

function delete_dot(aDot, aRefresh){
  if(aRefresh==undefined){
    aRefresh = true;
  }

  if(getPropertiesDiv != undefined){
    var aPropertiesDiv = getPropertiesDiv(false);
    if((aPropertiesDiv != undefined) && (aPropertiesDiv.currentElement == aDot)){
      aPropertiesDiv.close();
    }
  }

  // delete the dot
  aDot.remove();

  if((refresh_opendots_list!=undefined) && aRefresh){
    refresh_opendots_list();
  }
}

function do_delete(aDot){
  if(confirm("This will delete the selected dot ("+aDot.id+") and all its child dots.")){
    delete_dot(aDot);
  }
}

function new_child_dot_at(aParent, aLeft, aTop){
  var aDot = new_dot();
  var aParentId = aParent.getAttributeText("id");
  aDot.sat("parent_id", aParentId);

  aDot.scssa("left", aLeft);
  aDot.scssa("top", aTop);
}

function do_new_child_dot(aEvent, aParent){
  if(!aEvent){aEvent=event;}

  var aDot = new_dot();
  var aParentId = aParent.getAttributeText("id");
  aDot.sat("parent_id", aParentId);

  aDot.scssa("left", "0px");
  aDot.scssa("top", "0px");

  var aNewLeft = mouseX(aEvent) - absoluteLeft(aDot);
  var aNewTop = mouseY(aEvent) - absoluteTop(aDot);

  aNewLeft = Math.max(0, Math.min(aParent.offsetWidth - aDot.offsetWidth, aNewLeft));
  aNewTop = Math.max(0, Math.min(aParent.offsetHeight - aDot.offsetHeight, aNewTop));

  aDot.scssa("left", aNewLeft + "px");
  aDot.scssa("top", aNewTop + "px");
  
  keep_in_parent(aDot);

  refresh_opendots_list();
}

function replace_child_dots(aDots){
  // restore children
  assign_parent_nodes(aDots);

  // restore z_index
  for(var i=0; i<aDots.length; i++){
    var aDot = aDots[i];
    aDot.sat("z_index", aDot.oldZIndex);
  }
  
}

function setContent(aElement, aNewContent, aSetAttribute){
  // a generic setContent function - customise this for each class

  if(aNewContent==undefined){
    aNewContent = "(undefined)";
  }
  
  if(aSetAttribute==undefined){
  	aSetAttribute = true;
  }
  
  var aChildDots = remove_child_dots(aElement);
  
  if(IS_IE && head_is(aNewContent, "<script")){
    aElement.innerHTML = "<div style='display:none;'>&nbsp;</div>" + aNewContent; // IE why oh why? // todo do this better
  }
  else{
    aElement.innerHTML = aNewContent;
  }
  
	if(aSetAttribute){
	  aElement.setAttribute("content", aNewContent); // keep copy of raw html since innerHTML changes things
	}
	
  if(aElement.execScripts != undefined){ // (!IS_OPERA) && opera execs automatically
    aElement.execScripts();
  }
  replace_child_dots(aChildDots);

  if(aElement.refreshShadow!=undefined){
    aElement.refreshShadow();
  }
}

function getContent(aElement){
  // a generic getContent function - customise this for each class
  var result = aElement.getAttribute("content");

//  if((result == undefined) && (aElement.innerHTML!=undefined)){
//    result = aElement.innerHTML;
//  }

  return(result);
}

function init_dot_methods(aDot){
  // methods
  aDot.execScripts = bind(aDot, function(){
    exec_scripts(this);
  });

  aDot.setCSS = bind(aDot, function(aNewCSS){
    setCSS(this, aNewCSS);
    this.refreshOpacity();
    this.refreshShadow();
    this.refreshHoverInfo();

    if(document.loading != 0){
      var aCSS = this.gcssa("overflow");
      if(aCSS==undefined){
        this.scssa("overflow", "hidden");
      }
    }
  });

  aDot.getCSS = bind(aDot, function(){
    return(getCSS(this));
  });

  aDot.setCSSAttributeFor = bind(aDot, function(aName, aValue, aAttributeName){
    setCSSAttributeFor(this, aName, aValue, aAttributeName);
  });

  aDot.setCSSAttribute = bind(aDot, function(aName, aValue){
    setCSSAttribute(this, aName, aValue);
  });

  aDot.scssa = bind(aDot, function(aName, aValue){
    // shorthand for setCSSAttribute
    this.setCSSAttribute(aName, aValue);
  });

  aDot.getCSSAttribute = bind(aDot, function(aName){
    return(getCSSAttribute(this, aName));
  });

  aDot.gcssa = bind(aDot, function(aName){
    // shorthand for getCSSAttribute
    return(this.getCSSAttribute(aName));
  });

  aDot.removeCSSAttribute = bind(aDot, function(aName){
    return(removeCSSAttribute(this, aName));
  });

  aDot.getAttributeText = bind(aDot, function(aName){
    return(getAttributeText(this, aName));
  });

  aDot.gat = bind(aDot, function(aName){
    // a shorthand for getAttributeText
    return(this.getAttributeText(aName));
  });

  aDot.getCacheContent = bind(aDot, function(){
    return(getAttributeText(this, "cache_content") == "true");
  });

  aDot.onLoadFail = bind(aDot, function(){
    var aMessage = "<div class='fake_link' onclick='document.getElementById(\""+this.id+"\").refreshContentUrl();'>Loading this content failed (Click to retry)</div>"; // todo use const
    this.setContent(load_message_html(aMessage));
  });

  aDot.setContentFromURL = bind(aDot, function(aContent){
    
    // remove up to <body> inclusive
    var aPos = aContent.search(/<body/);

    if(aPos != -1){
      aContent = aContent.substr(aPos+1);

      aPos = aContent.search(/>/); // the end of the body tag
      if(aPos != -1){
        aContent = aContent.substr(aPos+1);
      }

      // remove after </body> inclusive
      aPos = aContent.search(/<\/body>/); // the close body tag
      if(aPos != -1){
        aContent = aContent.substr(0, aPos);
      }
    }

    this.setContent(aContent);
  });

  aDot.refreshContentUrl = bind(aDot, function(showLoadingMessage){
    if(!this.refreshingContentUrl){
      if(showLoadingMessage == undefined){
        showLoadingMessage = true;
      }

      this.refreshingContentUrl = true;
      if(showLoadingMessage){
        this.setContent(load_message_html(OPENDOTS_LOADING_MESSAGE));
      }

      var aUrl = getAttributeText(this, "content_url");
      var aHTTPLoader = new THTTPLoader(this.setContentFromURL, undefined, true,
        this.getCacheContent(), true, this.onLoadFail, false);

      aHTTPLoader.loadText(aUrl);
      delete(aHTTPLoader);
    }
  });

  aDot.isUrlContent = bind(aDot, function(){
    var aUrlContent = getAttributeText(this, "content_url");
    return((aUrlContent!="") && (aUrlContent!=undefined));
  });

  aDot.setParentId = bind(aDot, function(aValue){
    var aParent = geid(aValue);

    if((aParent!=undefined) && (aParent!=this) && (aParent!=this.parentNode)){
      aParent.appendChild(this);
      setAttributeText(this, "parent_id", aValue);
    }

    var currentParent = geid(this.getAttributeText("parent_id"));
    if(currentParent==undefined){
      setAttributeText(this, "parent_id", "document.body");
      document.body.appendChild(this);
      if((aValue!=undefined) && (OPENDOTS_MODE == OPENDOTS_EDIT_MODE)){
        alert("Parent ("+aValue+") not found for dot ("+this.id+").\nDot will be added to document.body");
      }
    }
  });

  aDot.refreshZIndex = bind(aDot, function(aValue){
    set_z_index(this, this.getAttribute("z_index"));
  });

  aDot.activateLinkUrl = bind(aDot, function(){
    document.location = this.getAttributeText("link_url");
  });

  aDot.wrapInAnchor = bind(aDot, function(aUrl){
    if(this.getContent()!=undefined){
    	//alert(this.gat("content"));
    	// in display mode "content" is not set - so use innerHTML
    	var aContent = this.gat("content");
    	if((aContent=="") && (this.innerHTML != "")){
    		aContent = this.innerHTML;
    	}
      this.setContent("<a href='"+aUrl+"'>"+aContent+"</a>");
    }
  });

  aDot.replaceLinkUrlWithAnchorWrap = bind(aDot, function(){
    if(OPENDOTS_MODE == OPENDOTS_DISPLAY_MODE){
      var aUrl = this.getAttributeText("link_url");
      if((aUrl != undefined) && (aUrl != "")){
        this.wrapInAnchor(aUrl);
        this.setAttributeText("link_url", "");
      }
    }
  });

  aDot.setLinkUrl = bind(aDot, function(aValue){
    if(OPENDOTS_MODE == OPENDOTS_DISPLAY_MODE){

      var aCurrentValue = this.getAttributeText("link_url");
      if((aValue == undefined) || (aValue == "")){
        if(aCurrentValue != aValue){
          if(IS_IE){

            if(this.oldOnMouseOver == undefined){
              this.oldOnMouseOver = null;
            }
            if(this.oldOnMouseOut == undefined){
              this.oldOnMouseOut = null;
            }
          }
          this.onmouseover = this.oldOnMouseOver;
          this.onmouseout = this.oldOnMouseOut;
        }
      }
      else{
        this.oldOnMouseOver = this.onmouseover;
        this.onmouseover = bind(this, function(aEvent){
          if(!aEvent){aEvent=event;}
          this.oldStatus = window.status;
          window.status = this.getAttributeText("link_url");

          if(this.oldOnMosueOver!= undefined){
            this.oldOnMosueOver(aEvent);
          }
        });

        this.oldOnMosueOut = this.onmouseout;
        this.onmouseout = bind(this, function(aEvent){
          if(!aEvent){aEvent=event;}
          window.status = this.oldStatus;
          this.oldStatus = undefined;
          if(this.oldOnMosueOut != undefined){
            this.oldOnMosueOut(aEvent);
          }
        });
        this.scssa("cursor", "pointer");
      }

    }
    setAttributeText(this, "link_url", aValue);
  });
  
  aDot.attributeExists = bind(aDot, function(aName){
    return(this.getAttribute(aName) != undefined);
  });  

  aDot.setAttributeText = bind(aDot, function(aName, aValue){

    if(aName=="opacity"){
      setOpacity(this, aValue);
      setAttributeText(this, aName, aValue);
    }
    else if(aName=="parent_id"){
      this.setParentId(aValue);
    }
    else if(aName=="content_url"){
      setAttributeText(this, aName, aValue);
      if(this.isUrlContent()){
        this.refreshContentUrl();
      }
    }
    else if(aName=="content"){
      if(!this.isUrlContent()){
        // ignore this content if this dot has a remotely sourced content
        setAttributeText(this, aName, aValue);

        if(aValue==""){
        	this.sat("content_sample", this.gat("content_sample")); // refresh
        }
      }
    }
    else if(aName=="content_sample"){
    	if(aValue != ""){
		  	if((this.gat("content")=="") && /*(this.innerHTML=="") &&*/ ((!this.isTemplateDot()) || (OPENDOTS_MODE==OPENDOTS_EDIT_MODE)) && (!this.isUrlContent())){
		  		
	//	  		if(OPENDOTS_MODE==OPENDOTS_EDIT_MODE){
		  			//this.sat("opacity", 10);
	//	  		}
	
					var aSampleId = this.gat("id")+"_opendots_sample_wrapper";
	  			setContent(this, "<div id='"+aSampleId+"'>"+aValue+"</div>", false);
					if(this.isTemplateDot()){
		  			var aSampleDot = geid(aSampleId);
		  			setOpacity(aSampleDot, 10);
		  			aSampleDot.title = "This is sample content and can not be modified. To create a dot like this one that can be modified, select [Create Duplicate] from the menu. Sample content is only displayed during editing.";	  			
					}
	  		}
    	}
  		else if(this.gat("content_sample")!=aValue){
  			this.sat("content", this.gat("content")); // refresh	
  		}
      setAttributeText(this, aName, aValue);
    }
    else if(aName=="class"){
      setAttributeText(this, aName, aValue);
      this.refreshCSS();
    }
    else if(aName=="sub_class"){
      setAttributeText(this, aName, aValue);
      this.refreshCSS();
    }
    else if(aName=="link_url"){
      this.setLinkUrl(aValue);
    }
    else if(aName=="include"){
      var aIncludes = aValue.split(",");
      var aJsIncludes = new Array();
      for(var i=0; i<aIncludes.length; i++){
        // load and eval the code
        aIncludes[i] = remove_head_slash(aIncludes[i]);
        if( (aIncludes[i] != "") && ((extract_fileext(aIncludes[i])=="js")) || tail_is(aIncludes[i], ".js.php") ){
          aJsIncludes[aJsIncludes.length] = aIncludes[i];
          global_include(aIncludes[i]);
        }
      }
      setAttributeText(this, aName, aJsIncludes.join(","));
    }
    else{
      setAttributeText(this, aName, aValue);
    }


    if((this.onSetCustomAttribute != undefined) && (this.onSetCustomAttribute[aName] != undefined)){
      this.onSetCustomAttribute[aName](aName, aValue);
    }

  });

  aDot.sa = bind(aDot, function(aName, aValue){
    // a shorthand for setAttribute
    return(this.setAttribute(aName, aValue));
  });

  aDot.sat = bind(aDot, function(aName, aValue){
    // a shorthand for setAttributeText
    return(this.setAttributeText(aName, aValue));
  });

  aDot.refreshOpacity = bind(aDot, function(){
    setOpacity(this, this.getAttributeText("opacity"));
  });

  aDot.refreshShadow = bind(aDot, function(){
    var aValue = this.getAttributeText("sub_class");
    if(is_shadow_class(aValue)){
      var aStartColor = qs(aValue, "start_color");
      var aEndColor = qs(aValue, "end_color");
      var aSize = qs(aValue, "size");
      var aOpacity = qs(aValue, "opacity");
      var aRounded = qs(aValue, "rounded");
      var aWhichShadows = qs(aValue, "which");
      var aShadowNumber = qs(aValue, "number");

      add_shadow(this, aShadowNumber, aOpacity, "#"+aStartColor, "#"+aEndColor, aSize, aRounded, aWhichShadows);
    }
    else{
      remove_shadows(this);
    }
  });

  aDot.refreshHoverInfo = bind(aDot, function(){
    var aValue = this.getAttributeText("sub_class");
    if((OPENDOTS_MODE != OPENDOTS_EDIT_MODE) && (aValue != undefined)){
      init_item_hover_info(this, aValue);
    }
  });

  aDot.refreshCSS = bind(aDot, function(){
    if(document.loading == 0){
      this.setCSS(this.getCSS());
    }
  });

  aDot.assignParent = bind(aDot, function(){
    // refresh the parent_id to ensure this is child of parent
    this.setParentId(this.getAttributeText("parent_id"));
  });

  aDot.setContent = bind(aDot, function(aNewContent){
    // NB this is called by setAttributeText
    setContent(this, aNewContent);
    this.refreshingContentUrl = false;
  });

  aDot.getContent = bind(aDot, function(){
    if((OPENDOTS_MODE != OPENDOTS_EDIT_MODE) && this.isUrlContent()){
      this.refreshContentUrl();
    }

    var result = getContent(this);

    return(result);
  });

  aDot.doOnAfterLoad = bind(aDot, function(){
    // this is called after all dots have been loaded
    if(this.onAfterLoad != undefined){
      this.onAfterLoad(); // todo - why did i make this? :)
    }

    if((this.onload != undefined) && (this.onload != "")){
      this.onload();
    }
  });
  
/*
  aDot.un_highlight = bind(aDot,function(){
    remove_edit_controls(this);  
  });
*/
  aDot.highlight = bind(aDot, function(aColor){
    remove_all_edit_controls();
    highlight_element(this, aColor); // decided to keep using this since it puts the hightlight on top of everything

//    add_outline_box(this);
//    aDot.highlightTimeout = setTimeout(this.un_highlight, 500);      
  });

  aDot.remove = bind(aDot, function(){
    assert(this.parentNode != undefined, "In opendots.js.php function aDot.remove() parentNode should not be undefined. (this.parentNode="+this.parentNode+")");
    if(document.globalCopySource == this){
      document.globalCopySource = undefined;
    }
    this.parentNode.removeChild(this);
  });

  aDot.isTemplateDot = bind(aDot, function(){
    return(this.getAttributeText("template_dot")=="true");
  });

  aDot.xml = bind(aDot, function(){
    // return xml for this dot and all child dots
    var result = dot_xml(this) + all_dots_xml(this);
    return(result);
  });

  aDot.duplicateId = bind(aDot, function(aDots){
    // true if there is a dot with same id as this.id
    if(aDots == undefined){
      aDots = all_dots();
    }
    var result = false;
    for(var iDot=0; iDot < aDots.length; iDot++){
      var aDot = aDots[iDot];
      if((aDot != this) && (aDot.id == this.id)){
        result = true;
        break;
      }
    }
    return(result);
  });

  aDot.autoName = bind(aDot, function(){
    // rename this dot if there is a duplicate
    var aCounter = 0;
    var aOldName = this.id;
    var aDots = all_dots();
    while(this.duplicateId(aDots)){
      // this needs to be renamed
      this.sat("id", aOldName + "_" + aCounter);
      aCounter++;
    }

    var aChildDots = all_dots(this);

    for(var iChild=0; iChild<aChildDots.length; iChild++){
      aChildDots[iChild].autoName();
    }
  });

  aDot.moveChildren = bind(aDot, function(aNewParentId){
    // move all child dots to the given parent
    var aChildren = all_dots(this, false);
    for(var iChild=0; iChild<aChildren.length; iChild++){
      if(aChildren[iChild].is_opendot != undefined){
        aChildren[iChild].sat("parent_id", aNewParentId);
      }
    }
  });

  aDot.removeChildren = bind(aDot, function(){
    // Remove (delete) all child dots // todo test
    var aChildren = all_dots(this, false);
    for(var iChild=0; iChild<aChildren.length; iChild++){
      if(aChildren[iChild].is_opendot != undefined){
        aChildren[iChild].remove();
      }
    }
  });

  aDot.generation = bind(aDot, function(){
    // return the number of ancestors
    var result;
    if((this.parentNode==document.body) || (this.parentNode==undefined)){
    	result = 0;
    }
    else{
    	result = 1 + this.parentNode.generation();
    }
    return(result);
  });

  aDot.addCustomAttribute = bind(aDot, function(aName, aOnSetAttribute, aValue){
    // Use this when creating a control when you want a custom attribute that will be saved along with the other attributes

    if(aValue == undefined){
      aValue = "";
    }

    var aCurrentValue = this.getAttributeText(aName);

    if(aCurrentValue == undefined){
      this.sat(aName, aValue);
    }
    else if(aOnSetAttribute != undefined){
      aOnSetAttribute(aName, aCurrentValue);
    }

    if(this.onSetCustomAttribute == undefined){
      // an array of aOnSetAttribute functions for this object
      this.onSetCustomAttribute = new Array();
    }

    if(aOnSetAttribute != undefined){
      this.onSetCustomAttribute[aName] = aOnSetAttribute;
    }

    if(!in_array(aName, ALLOWED_ATTRIBUTES)){
      // add to the allowable attributes
      ALLOWED_ATTRIBUTES[ALLOWED_ATTRIBUTES.length] = aName;
    }

    if(!in_array(aName, COPY_ATTRIBUTES)){
      // add to the copyable attributes
      COPY_ATTRIBUTES[COPY_ATTRIBUTES.length] = aName;
    }
  });

  aDot.initCustomMenu = bind(aDot, function(){
    if(this.customMenuItems == undefined){
      this.customMenuItems = create_menu(this.gat("id")+"_custom_menu_items");
      document.body.appendChild(this.customMenuItems);
    }
  });

  aDot.addCustomMenuItem = bind(aDot, function(aCaption, aOnClick, aHintText, aIcon){
    this.initCustomMenu();
    // Add a menu item to the [Custom Menu] that appears at the top of the popup menu when this dot is clicked
    this.customMenuItems.add_menu_item(this.customMenuItems.id+"_"+this.customMenuItems.childNodes.length, aCaption, aOnClick, aHintText, aIcon);
  });

  // edit-only methods
  if(OPENDOTS_MODE == OPENDOTS_EDIT_MODE){
    init_dot_edit_methods(aDot);
  }
}

function idm(aDot){
  // shorthand for init_dot_methods
  return(init_dot_methods(aDot));
}

function init_dot_attributes(aDot){
  // initialise
  aDot.is_opendot = true;
  aDot.refreshingContentUrl = false;

  for(var i=0; i<DOT_ATTRIBUTES.length; i++){
    if(aDot.getAttributeText(DOT_ATTRIBUTES[i]) == undefined){
//      if((OPENDOTS_MODE==OPENDOTS_DISPLAY_MODE) || (!event_attribute(DOT_ATTRIBUTES[i]))){
      if(event_attribute(DOT_ATTRIBUTES[i])){
        aDot.sat(DOT_ATTRIBUTES[i], "");
      }
      else{
        aDot.sa(DOT_ATTRIBUTES[i], "");
      }
    }
  }
}

function ida(aDot){
  // shorthand for init_dot_attributes
  return(init_dot_attributes(aDot));
}

function default_dot_attributes(aDot, aName, aContent, aLeft, aTop, aWidth, aHeight, aInsertIntoBody){
  // default values
  if(aName==undefined){
    aName = get_next_dot_id();
  }
  if(aContent==undefined){
    aContent = "New Dot: " + aName;
  }
  if(aLeft==undefined){
    aLeft = next_dot_left() + "px";
  }
  if(aTop==undefined){
    aTop = next_dot_top() + "px";
  }
  if(aWidth==undefined){
    aWidth = DEFAULT_DOT_WIDTH + "px";
  }
  if(aHeight==undefined){
    aHeight = DEFAULT_DOT_HEIGHT + "px";
  }
	if(aInsertIntoBody == undefined){
		aInsertIntoBody = true;
	}

  // initialise
  aDot.is_opendot = true;

  aDot.refreshingContentUrl = false;

  aDot.sat("class", "");

  aDot.sat("id", aName);

  aDot.sat("content", aContent);

  aDot.scssa("left", aLeft);
  aDot.scssa("top", aTop);
  aDot.scssa("width", aWidth);
  aDot.scssa("height", aHeight);
  aDot.scssa("background-color", DEFAULT_BACKGROUND_COLOR);
  aDot.scssa("position", "absolute");

  aDot.scssa("overflow", "hidden");

  // NB this is NOT the CSS index
  aDot.sat("z_index", "0");

  aDot.sat("title", "");

  aDot.sat("content_url", "");

  aDot.sat("cache_content", "false");

  aDot.sat("sub_class", "");

  if(aInsertIntoBody){
  	aDot.sat("parent_id", "document.body");
  }
  
  aDot.sat("template_dot", "false");

  aDot.sat("link_url", "");

  aDot.sat("opacity", "100");

  aDot.sat("onclick", "");

  aDot.sat("onload", "");

  aDot.sat("onmouseover", "");

  aDot.sat("onmouseout", "");

  aDot.sat("include", "");

  aDot.sat("fixed", "false");
}

function init_dot(aDot, aName, aContent, aLeft, aTop, aWidth, aHeight, aInsertIntoBody){
  init_dot_methods(aDot);
  init_dot_attributes(aDot);
  default_dot_attributes(aDot, aName, aContent, aLeft, aTop, aWidth, aHeight, aInsertIntoBody);
}

function new_dot(aName, aContent, aLeft, aTop, aWidth, aHeight, aInsertIntoBody){
  // Dot constructor
	if(aInsertIntoBody == undefined){
		aInsertIntoBody = true;
	}
	
  var aDot = ce("div");
  init_dot(aDot, aName, aContent, aLeft, aTop, aWidth, aHeight, aInsertIntoBody);
  
  if(aInsertIntoBody){
	  document.body.appendChild(aDot);
	  keep_in_parent(aDot);
  }

  if(do_node_properties_if_active != undefined){
    do_node_properties_if_active(aDot);
  }

  return(aDot);
}

function drag_bar_allowed(aDot){
  var aPosition = aDot.gcssa("position");
  var result = (aPosition!="inherit");
  return(result);
}

function add_edit_controls(aDot){

  // add the outline border
  if(aDot.outlineBox == undefined){
    if(!aDot.isTemplateDot()){
      add_outline_box(aDot);
    }
  }  

  // add the drag bar
  if(drag_bar_allowed(aDot) && (aDot.dragBar == undefined)){
    assert(aDot.dragBar == undefined, "add_edit_controls");
    drag_bar(aDot);

    assert(aDot.dragBar != undefined, "add_edit_controls");
    aDot.dragBar.style.display="";
    var aHTML = "";

    aHTML += aDot.id;

    if(aDot.isTemplateDot()){
      aHTML += " (defined in template)";
      aDot.dragBar.title = "This dot cannot be moved since it is defined in a template";
      aDot.dragBar.enabled = false;
      aDot.dragBar.deleteButton.style.display = "none";
    }
    else{
      aDot.dragBar.title = "(" + strip_html(aHTML) + ") \nClick and drag. Hold CTRL and drag to snap to grid size ("+SNAP_GRID+").";
    }

    aDot.dragBar.setTitle(aHTML);

    if(IS_IE){
      aDot.dragBar.style.width = aDot.offsetWidth;
      if(aDot.dragBar.offsetLeft != 0){
        aDot.dragBar.style.left = -aDot.dragBar.offsetLeft;
      }
    }
  }

  // add the size box
  if(aDot.sizeBox == undefined){
    if(!aDot.isTemplateDot()){
      size_box(aDot).style.display="none";
      aDot.sizeBox.adjust_position();
      aDot.sizeBox.style.display="";
    }
  }

}

function remove_edit_controls(aDot){
  // delete the edit controls for the given dot
  if((aDot.dragBar != undefined) && (!aDot.dragBar.dragging)){
    deleteNode(aDot.dragBar);
    aDot.dragBar = undefined;
  }

  if((aDot.sizeBox != undefined) && (!aDot.sizeBox.dragging)){
    deleteNode(aDot.sizeBox);
    aDot.sizeBox = undefined;
  }

  if(aDot.outlineBox != undefined){
    remove_outline_box(aDot);
  }
}

function remove_all_edit_controls(){
  // hide edit controls on all dots
  var aDots = all_dots();
  for(var iDot=0; iDot<aDots.length; iDot++){
    remove_edit_controls(aDots[iDot]);
  }
}

function delete_all_dots(){
  var aDots = all_dots();
  for(var iDot=0; iDot<aDots.length; iDot++){
    var aDot = aDots[iDot];
    delete_dot(aDot, false);
  }
  refresh_opendots_list();
}

function delete_all_template_dots(aDots){
  var result = new Array();
  if(aDots == undefined){
    aDots = all_dots();
  }

  for(var iDot=0; iDot<aDots.length; iDot++){
    var aDot = aDots[iDot];
    if(aDot.getAttributeText("template_dot")=="true"){
      delete_dot(aDot);
    }
    else{
      result.push(aDot);
    }
  }

  return(result);
}

function assign_parent_nodes(aDots){
  if(aDots==undefined){
    aDots = all_dots();
  }

  for(var i=0; i<aDots.length; i++){
    var aDot = aDots[i];
    aDot.assignParent();
  }
}

/* URL functions */

function get_initial_page_name(){
  var aPageName = opendot_page_query();

  if(aPageName==undefined){
    aPageName = DEFAULT_OPENDOTS_PAGE;
  }

  return(aPageName);
}

function opendot_page_query(aQuery){
  if(aQuery==undefined){
    aQuery = document.location.search;
  }
  return(qs(document.location.search, "opendot_page"));
}

function current_page_display_url(){
  // return the url for displaying the current page

  if(document.currentPage==undefined){
    var aPageName = opendot_page_query();
    document.currentPage = aPageName;
  }

  return(document.location.pathname + "?opendot_page=" + document.currentPage);
}

function edit_current_page(){
  document.location = current_page_display_url() + "&edit=true";
}

function display_current_page(){
  document.location = current_page_display_url();
}
