/*

Textile Editor v0.2
created by: dave olsen, wvu web services
created on: march 17, 2007
project page: slateinfo.blogs.wvu.edu

inspired by: 
 - Patrick Woods, http://www.hakjoon.com/code/38/textile-quicktags-redirect & 
 - Alex King, http://alexking.org/projects/js-quicktags

features:
 - supports: IE7, FF2, Safari2
 - ability to use "simple" vs. "extended" editor
 - supports all block elements in textile except footnote
 - supports all block modifier elements in textile
 - supports simple ordered and unordered lists
 - supports most of the phrase modifiers, very easy to add the missing ones
 - supports multiple-paragraph modification
 - can have multiple "editors" on one page, access key use in this environment is flaky
 - access key support
 - select text to add and remove tags, selection stays highlighted
 - seamlessly change between tags and modifiers
 - doesn't need to be in the body onload tag
 - can supply your own, custom IDs for the editor to be drawn around

todo:
 - a clean way of providing image and link inserts
 - get the selection to properly show in IE

more on textile:
 - Textism, http://www.textism.com/tools/textile/index.php
 - Textile Reference, http://hobix.com/textile/

*/

// Define Button Object
function TextileEditorButton(id, display, tagStart, tagEnd, access, title, sve, open) {
  this.id = id;       // used to name the toolbar button
  this.display = display;   // label on button
  this.tagStart = tagStart;   // open tag
  this.tagEnd = tagEnd;   // close tag
  this.access = access;   // set to -1 if tag does not need to be closed
  this.title = title;     // sets the title attribute of the button to give 'tool tips'
  this.sve = sve;       // sve = simple vs. extended. add an 's' to make it show up in the simple toolbar
  this.open = open;     // set to -1 if tag does not need to be closed
  this.standard = true;  // this is a standard button
  // this.framework = 'prototype'; // the JS framework used
}

function TextileEditorButtonSeparator(sve) {
  this.separator = true;
  this.sve = sve;
}

var TextileEditor = function() {};
TextileEditor.buttons = new Array();
TextileEditor.Methods = {
  // class methods

  // create the toolbar (edToolbar)
  initialize: function(canvas, view) {
    var toolbar = document.createElement("div");
    toolbar.id = "textile-toolbar-" + canvas;
    toolbar.className = 'textile-toolbar';
    this.canvas = document.getElementById(canvas);
    this.canvas.parentNode.insertBefore(toolbar, this.canvas);
    this.openTags = new Array();

    // Create the local Button array by assigning theButtons array to edButtons
    var edButtons = new Array();
    edButtons = this.buttons;
    
    var standardButtons = new Array();
    for(var i = 0; i < edButtons.length; i++) {
      var thisButton = this.prepareButton(edButtons[i]);
      if (view == 's') {
        if (edButtons[i].sve == 's') {
          toolbar.appendChild(thisButton);
          standardButtons.push(thisButton);
        }
      } else {
        if (typeof thisButton == 'string') {
          toolbar.innerHTML += thisButton;
        } else {
          toolbar.appendChild(thisButton);
          standardButtons.push(thisButton);
        }
      }
    } // end for
    
    var te = this;
    var buttons = toolbar.getElementsByTagName('button');
    for(var i = 0; i < buttons.length; i++) {
    //$A(toolbar.getElementsByTagName('button')).each(function(button) {
      if (!buttons[i].onclick) {
        buttons[i].onclick = function() { te.insertTag(this); return false; }
      } // end if
      
      buttons[i].tagStart = buttons[i].getAttribute('tagStart');
      buttons[i].tagEnd = buttons[i].getAttribute('tagEnd');
      buttons[i].open = buttons[i].getAttribute('open');
      buttons[i].textile_editor = te;
      buttons[i].canvas = te.canvas;
      // console.log(buttons[i].canvas);
    //});
    }
  }, // end initialize
  
  // draw individual buttons (edShowButton)
  prepareButton: function(button) {
    if (button.separator) {
      var theButton = document.createElement('span');
      theButton.className = 'ed_sep';
      return theButton;
    }

    if (button.standard) {
      var theButton = document.createElement("button");
      theButton.id = button.id;
      theButton.setAttribute('class', 'standard');
      theButton.setAttribute('tagStart', button.tagStart);
      theButton.setAttribute('tagEnd', button.tagEnd);
      theButton.setAttribute('open', button.open);

      var img = document.createElement('img');
      img.src = '/images/textile-editor/' + button.display;
      theButton.appendChild(img);
    } else {
      return button;
    } // end if !custom

    theButton.accessKey = button.access;
    theButton.title = button.title;
    return theButton; 
  }, // end prepareButton
  
  // if clicked, no selected text, tag not open highlight button
  // (edAddTag)
  addTag: function(button) {
    if (button.tagEnd != '') {
      this.openTags[this.openTags.length] = button;
      //var el = document.getElementById(button.id);
      //el.className = 'selected';
      button.className = 'selected';
    }
  }, // end addTag

  // if clicked, no selected text, tag open lowlight button
  // (edRemoveTag)
  removeTag: function(button) {
    for (i = 0; i < this.openTags.length; i++) {
      if (this.openTags[i] == button) {
        this.openTags.splice(button, 1);
        //var el = document.getElementById(button.id);
        //el.className = 'unselected';
        button.className = 'unselected';
      }
    }
  }, // end removeTag

  // see if there are open tags. for the remove tag bit...
  // (edCheckOpenTags)
  checkOpenTags: function(button) {
    var tag = 0;
    for (i = 0; i < this.openTags.length; i++) {
      if (this.openTags[i] == button) {
        tag++;
      }
    }
    if (tag > 0) {
      return true; // tag found
    }
    else {
      return false; // tag not found
    }
  }, // end checkOpenTags

  // insert the tag. this is the bulk of the code.
  // (edInsertTag)
  insertTag: function(button, tagStart, tagEnd) {
    // console.log(button);
    var myField = button.canvas;
    myField.focus();

    if (tagStart) {
      button.tagStart = tagStart;
      button.tagEnd = tagEnd ? tagEnd : '\n';
    }

    var textSelected = false;
    var finalText = '';
    var FF = false;

    // grab the text that's going to be manipulated, by browser
    if (document.selection) { // IE support
      sel = document.selection.createRange();

      // set-up the text vars
      var beginningText = '';
      var followupText = '';
      var selectedText = sel.text;

      // check if text has been selected
      if (sel.text.length > 0) {
        textSelected = true;  
      }

      // set-up newline regex's so we can swap tags across multiple paragraphs
      var newlineReplaceRegexClean = /\r\n\s\n/g;
      var newlineReplaceRegexDirty = '\\r\\n\\s\\n';
      var newlineReplaceClean = '\r\n\n';
    }
    else if (myField.selectionStart || myField.selectionStart == '0') { // MOZ/FF/NS/S support

      // figure out cursor and selection positions
      var startPos = myField.selectionStart;
      var endPos = myField.selectionEnd;
      var cursorPos = endPos;
      var scrollTop = myField.scrollTop;
      FF = true; // note that is is a FF/MOZ/NS/S browser

      // set-up the text vars
      var beginningText = myField.value.substring(0, startPos);
      var followupText = myField.value.substring(endPos, myField.value.length);

      // check if text has been selected
      if (startPos != endPos) {
        textSelected = true;
        var selectedText = myField.value.substring(startPos, endPos); 
      }

      // set-up newline regex's so we can swap tags across multiple paragraphs
      var newlineReplaceRegexClean = /\n\n/g;
      var newlineReplaceRegexDirty = '\\n\\n';
      var newlineReplaceClean = '\n\n';
    }


    // if there is text that has been highlighted...
    if (textSelected) {

      // set-up some defaults for how to handle bad new line characters
      var newlineStart = '';
      var newlineStartPos = 0;
      var newlineEnd = '';
      var newlineEndPos = 0;
      var newlineFollowup = '';

      // set-up some defaults for how to handle placing the beginning and end of selection
      var posDiffPos = 0;
      var posDiffNeg = 0;
      var mplier = 1;

      // remove newline from the beginning of the selectedText.
      if (selectedText.match(/^\n/)) {
        selectedText = selectedText.replace(/^\n/,'');
        newlineStart = '\n';
        newlineStartpos = 1;
      }

      // remove newline from the end of the selectedText.
      if (selectedText.match(/\n$/g)) {
        selectedText = selectedText.replace(/\n$/g,'');
        newlineEnd = '\n';
        newlineEndPos = 1;
      }

      // remove space from the end of the selectedText.
      // Fixes a bug that causes any browser running under Microsoft Internet Explorer 
      // to append an additional space before the closing element.
      // *Bold text *here => *Bold text*
      if (selectedText.match(/\s$/g)) {
        selectedText = selectedText.replace(/\s$/g,'');
        followupText = ' ';
      }
      
      // no clue, i'm sure it made sense at the time i wrote it
      if (followupText.match(/^\n/)) {
        newlineFollowup = '';
      }
      else {
        newlineFollowup = '\n\n';
      }

      // first off let's check if the user is trying to mess with lists
      if ((button.tagStart == ' * ') || (button.tagStart == ' # ')) {

        listItems = 0; // sets up a default to be able to properly manipulate final selection

        // set-up all of the regex's
        re_start = new RegExp('^ (\\*|\\#) ','g');
        if (button.tagStart == ' # ') {
          re_tag = new RegExp(' \\# ','g'); // because of JS regex stupidity i need an if/else to properly set it up, could have done it with a regex replace though
        }
        else {
          re_tag = new RegExp(' \\* ','g');
        }
        re_replace = new RegExp(' (\\*|\\#) ','g');

        // try to remove bullets in text copied from ms word **Mac Only!** 
        re_word_bullet_m_s = new RegExp('• ','g'); // mac/safari
        re_word_bullet_m_f = new RegExp('∑ ','g'); // mac/firefox
        selectedText = selectedText.replace(re_word_bullet_m_s,'').replace(re_word_bullet_m_f,'');

        // if the selected text starts with one of the tags we're working with...
        if (selectedText.match(re_start)) {

          // if tag that begins the selection matches the one clicked, remove them all
          if (selectedText.match(re_tag)) {
            finalText = beginningText
                    + newlineStart
                    + selectedText.replace(re_replace,'')
                    + newlineEnd
                    + followupText;
            if (matches = selectedText.match(/ (\*|\#) /g)) {
              listItems = matches.length;
            }
            posDiffNeg = listItems*3; // how many list items were there because that's 3 spaces to remove from final selection
          }

          // else replace the current tag type with the selected tag type
          else {
            finalText = beginningText
                    + newlineStart
                    + selectedText.replace(re_replace,button.tagStart)
                    + newlineEnd
                    + followupText;
          }
        }

        // else try to create the list type
        // NOTE: the items in a list will only be replaced if a newline starts with some character, not a space
        else {
          finalText = beginningText
                  + newlineStart
                        + button.tagStart
                  + selectedText.replace(newlineReplaceRegexClean,newlineReplaceClean + button.tagStart).replace(/\n(\S)/g,'\n' + button.tagStart + '$1')
                  + newlineEnd
                  + followupText;
          if (matches = selectedText.match(/\n(\S)/g)) {
            listItems = matches.length;
          }
          posDiffPos = 3 + listItems*3;
        } 
      }

      // now lets look and see if the user is trying to muck with a block or block modifier
      else if (button.tagStart.match(/^(h1|h2|h3|h4|h5|h6|bq|p|\>|\<\>|\<|\=|\(|\))/g)) {

        var insertTag = '';
        var insertModifier = '';
        var tagPartBlock = '';
        var tagPartModifier = '';
        var tagPartModifierOrig = ''; // ugly hack but it's late
        var drawSwitch = '';
        var captureIndentStart = false;
        var captureListStart = false;
        var periodAddition = '\\. ';
        var periodAdditionClean = '. ';
        var listItemsAddition = 0;

        var re_list_items = new RegExp('(\\*+|\\#+)','g'); // need this regex later on when checking indentation of lists

        var re_block_modifier = new RegExp('^(h1|h2|h3|h4|h5|h6|bq|p| [\\*]{1,} | [\\#]{1,} |)(\\>|\\<\\>|\\<|\\=|[\\(]{1,}|[\\)]{1,6}|)','g');
        if (tagPartMatches = re_block_modifier.exec(selectedText)) {
          tagPartBlock = tagPartMatches[1];
          tagPartModifier = tagPartMatches[2];
          tagPartModifierOrig = tagPartMatches[2];
          tagPartModifierOrig = tagPartModifierOrig.replace(/\(/g,"\\(");
        }

        // if tag already up is the same as the tag provided replace the whole tag
        if (tagPartBlock == button.tagStart) { 
          insertTag  = tagPartBlock + tagPartModifierOrig; // use Orig because it's escaped for regex
          drawSwitch = 0; 
        }
        // else if let's check to add/remove block modifier
        else if ((tagPartModifier == button.tagStart) || (newm = tagPartModifier.match(/[\(]{2,}/g))) {
          if ((button.tagStart == '(') || (button.tagStart == ')')) {
            var indentLength = tagPartModifier.length;
            if (button.tagStart == '(') {
              indentLength = indentLength + 1;
            }
            else {
              indentLength = indentLength - 1;
            }
            for (var i = 0; i < indentLength; i++) {
              insertModifier = insertModifier + '(';
            }
            insertTag = tagPartBlock + insertModifier;
          }
          else {
            if (button.tagStart == tagPartModifier) {
              insertTag =  tagPartBlock;
              } // going to rely on the default empty insertModifier
            else {

              if (button.tagStart.match(/(\>|\<\>|\<|\=)/g)) {
                insertTag = tagPartBlock + button.tagStart;
              }
              else {
                insertTag = button.tagStart + tagPartModifier;
              }
            }

          }
          drawSwitch = 1;
        }
        // indentation of list items
        else if (listPartMatches = re_list_items.exec(tagPartBlock)) {
            var listTypeMatch = listPartMatches[1];
            var indentLength = tagPartBlock.length - 2;
            var listInsert = '';
            if (button.tagStart == '(') {
              indentLength = indentLength + 1;
            }
            else {
              indentLength = indentLength - 1;
            }
            if (listTypeMatch.match(/[\*]{1,}/g)) {
              var listType = '*';
              var listReplace = '\\*';
            }
            else {
              var listType = '#';
              var listReplace = '\\#';
            }
            for (var i = 0; i < indentLength; i++) {
              listInsert = listInsert + listType;
            }
            if (listInsert != '') {
              insertTag = ' ' + listInsert + ' ';
            }
            else {
              insertTag = '';
            }
            tagPartBlock = tagPartBlock.replace(/(\*|\#)/g,listReplace);
            drawSwitch = 1;
            captureListStart = true;
            periodAddition = '';
            periodAdditionClean = '';
            if (matches = selectedText.match(/\n\s/g)) {
              listItemsAddition = matches.length;
            }
        }
        // must be a block modification e.g. p>. to p<.
        else {

          // if this is a block modification/addition
          if (button.tagStart.match(/(h1|h2|h3|h4|h5|h6|bq|p)/g)) { 
            if (tagPartBlock == '') {
              drawSwitch = 2;
            }
            else {
              drawSwitch = 1;
            }

            insertTag = button.tagStart + tagPartModifier;
          }

          // else this is a modifier modification/addition
          else {
            if ((tagPartModifier == '') && (tagPartBlock != '')) {
              drawSwitch = 1;
            }
            else if (tagPartModifier == '') {
              drawSwitch = 2;
            }
            else {
              drawSwitch = 1;
            }

            // if no tag part block but a modifier we need at least the p tag
            if (tagPartBlock == '') {
              tagPartBlock = 'p';
            }

            //make sure to swap out outdent
            if (button.tagStart == ')') {
              tagPartModifier = '';
            }
            else {
              tagPartModifier = button.tagStart;
              captureIndentStart = true; // ugly hack to fix issue with proper selection handling
            }

            insertTag = tagPartBlock + tagPartModifier;
          }
        }

        mplier = 0;
        if (captureListStart || (tagPartModifier.match(/[\(\)]{1,}/g))) {
          re_start = new RegExp(insertTag.escape + periodAddition,'g'); // for tags that mimic regex properties, parens + list tags
        }
        else {
          re_start = new RegExp(insertTag + periodAddition,'g'); // for tags that don't, why i can't just escape everything i have no clue
        }
        re_old = new RegExp(tagPartBlock + tagPartModifierOrig + periodAddition,'g');
        re_middle = new RegExp(newlineReplaceRegexDirty + insertTag.escape + periodAddition.escape,'g');
        re_tag = new RegExp(insertTag.escape + periodAddition.escape,'g');

        // *************************************************************************************************************************
        // this is where everything gets swapped around or inserted, bullets and single options have their own if/else statements
        // *************************************************************************************************************************
        if ((drawSwitch == 0) || (drawSwitch == 1)) {
          if (drawSwitch == 0) { // completely removing a tag
            finalText = beginningText
                    + newlineStart
                    + selectedText.replace(re_start,'').replace(re_middle,newlineReplaceClean)
                    + newlineEnd
                    + followupText;
            if (matches = selectedText.match(newlineReplaceRegexClean)) {
              mplier = mplier + matches.length;
            }
            posDiffNeg = insertTag.length + 2 + (mplier*4);
          }
          else { // modifying a tag, though we do delete bullets here
            finalText = beginningText
                    + newlineStart
                    + selectedText.replace(re_old,insertTag + periodAdditionClean)
                    + newlineEnd
                    + followupText;

            if (matches = selectedText.match(newlineReplaceRegexClean)) {
              mplier = mplier + matches.length;
            }
            // figure out the length of various elements to modify the selection position
            if (captureIndentStart) { // need to double-check that this wasn't the first indent
              tagPreviousLength = tagPartBlock.length;
              tagCurrentLength = insertTag.length;
            }
            else if (captureListStart) { // if this is a list we're manipulating
              if (button.tagStart == '(') { // if indenting
                tagPreviousLength = listTypeMatch.length + 2;
                tagCurrentLength = insertTag.length + listItemsAddition;
              }
              else if (insertTag.match(/(\*|\#)/g)) { // if removing but still has bullets
                tagPreviousLength = insertTag.length + listItemsAddition;
                tagCurrentLength = listTypeMatch.length;
              }
              else {  // if removing last bullet
                tagPreviousLength = insertTag.length + listItemsAddition;
                tagCurrentLength = listTypeMatch.length - (3*listItemsAddition) - 1;
              }
            }
            else { // everything else
              tagPreviousLength = tagPartBlock.length + tagPartModifier.length;
              tagCurrentLength = insertTag.length;
            }
            if (tagCurrentLength > tagPreviousLength) {
              posDiffPos = (tagCurrentLength - tagPreviousLength) + (mplier*(tagCurrentLength - tagPreviousLength));
            }
            else {
              posDiffNeg = (tagPreviousLength - tagCurrentLength) + (mplier*(tagPreviousLength - tagCurrentLength));
            }
          }
        }
        else { // for adding tags other then bullets (have their own statement)
          finalText = beginningText
                  + newlineStart
                        + insertTag + '. '
                  + selectedText.replace(newlineReplaceRegexClean,button.tagEnd + '\n' + insertTag + '. ')
                  + newlineFollowup
                  + newlineEnd
                  + followupText;
          if (matches = selectedText.match(newlineReplaceRegexClean)) {
            mplier = mplier + matches.length;
          }
          posDiffPos = insertTag.length + 2 + (mplier*4);
        }       
      }

      // swap in and out the simple tags around a selection like bold
      else {

        mplier = 1; // the multiplier for the tag length
        re_start = new RegExp('^\\' + button.tagStart,'g');
        re_end =  new RegExp('\\' + button.tagEnd + '$','g');
        re_middle = new RegExp('\\' + button.tagEnd + newlineReplaceRegexDirty + '\\' + button.tagStart,'g');
        if (selectedText.match(re_start) && selectedText.match(re_end)) {
          finalText = beginningText
                  + newlineStart
                  + selectedText.replace(re_start,'').replace(re_end,'').replace(re_middle,newlineReplaceClean)
                  + newlineEnd
                  + followupText;
          if (matches = selectedText.match(newlineReplaceRegexClean)) {
            mplier = mplier + matches.length;
          }
          posDiffNeg = button.tagStart.length*mplier + button.tagEnd.length*mplier;
        }
        else {
          if (button.id == 'ed_link') { // set tagEnd to add URL (Added by matt@mattking.org 10/19/2007)
            url = prompt('Enter URL:', 'http://');
            button.tagEnd = '":' + url;
          }
          
          finalText = beginningText
                  + newlineStart
                        + button.tagStart
                  + selectedText.replace(newlineReplaceRegexClean,button.tagEnd + newlineReplaceClean + button.tagStart)
                  + button.tagEnd
                  + newlineEnd
                  + followupText;
          if (matches = selectedText.match(newlineReplaceRegexClean)) {
            mplier = mplier + matches.length;
          }
          posDiffPos = (button.tagStart.length*mplier) + (button.tagEnd.length*mplier);
        }
      }

      cursorPos += button.tagStart.length + button.tagEnd.length;

    }

    // just swap in and out single values, e.g. someone clicks b they'll get a *
    else {
      var buttonStart = '';
      var buttonEnd = '';
      var re_p = new RegExp('(\\<|\\>|\\=|\\<\\>|\\(|\\))','g');
      var re_h = new RegExp('^(h1|h2|h3|h4|h5|h6|p|bq)','g');
      if (!this.checkOpenTags(button) || button.tagEnd == '') { // opening tag

        if (button.tagStart.match(re_h)) {
          buttonStart = button.tagStart + '. ';
        }
        else {
          buttonStart = button.tagStart;
        }
        if (button.tagStart.match(re_p)) { // make sure that invoking block modifiers don't do anything
          finalText = beginningText 
                     + followupText;
          cursorPos = startPos;
        }
        else {
          finalText = beginningText 
                      + buttonStart
                      + followupText;
          this.addTag(button);
          cursorPos = startPos + buttonStart.length;
        }

      }
      else {  // closing tag
        if (button.tagStart.match(re_p)) {
          buttonEnd = '\n\n';
        }
        else if (button.tagStart.match(re_h)) {
          buttonEnd = '\n\n';
        }
        else {
          buttonEnd = button.tagEnd
        }
        finalText = beginningText 
                    + button.tagEnd
                    + followupText;
        this.removeTag(button);
        cursorPos = startPos + button.tagEnd.length;
      }
    }

    // set the appropriate DOM value with the final text
    if (FF == true) {
      myField.value = finalText;
      myField.scrollTop = scrollTop;
    }
    else {
      sel.text = finalText;
    }

    // build up the selection capture, doesn't work in IE
    if (textSelected) {
      myField.selectionStart = startPos + newlineStartPos;
      myField.selectionEnd = endPos + posDiffPos - posDiffNeg - newlineEndPos;
      //alert('s: ' + myField.selectionStart + ' e: ' + myField.selectionEnd + ' sp: ' + startPos + ' ep: ' + endPos + ' pdp: ' + posDiffPos + ' pdn: ' + posDiffNeg)
    }
    else {
      myField.selectionStart = cursorPos;
      myField.selectionEnd = cursorPos;
    }
  } // end insertTag
}; // end class

// add class methods
// Object.extend(TextileEditor, TextileEditor.Methods);
destination = TextileEditor
source = TextileEditor.Methods
for(var property in source) destination[property] = source[property];

//document.write('<script src="/javascripts/textile-editor-config.js" type="text/javascript"></script>');
