
/*
 * Utility functions
 * Copyright 2008-2010 Yaroslav Stavnichiy
 */

function noNull(s) {
  return s==null?'':s;
}

function zeroPad(s, n) {
  s=s?s.toString():'';
  while (s.length<n) s='0'+s;
  return s;
}

function escapeHTML(s) {
  if (!s) return '';
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/'/g,'&#39;').replace(/"/g,'&quot;');
}

var win1251table="ЂЃ‚ѓ„…†‡€‰Љ‹ЊЌЋЏђ‘’“”•–— ™љ›њќћџ ЎўЈ¤Ґ¦§Ё©Є«¬­®Ї°±Ііґµ¶·ё№є»јЅѕї";
var win1251cache=new Array();
function getWin1251Code(c) {
  if (win1251cache[c]) return win1251cache[c];
  if (c>0 && c<=127) {
    win1251cache[c]=c;
    return c;
  }
  else if (c>=0x410 && c<=0x44f) { // А-Яа-я
    win1251cache[c]=c-0x350;
    return c-0x350; // (0x410-0xc0)=0x350
  }
  else {
    var idx=win1251table.indexOf(String.fromCharCode(c));
    if (idx>=0) {
      win1251cache[c]=0x80+idx;
      return 0x80+idx;
    }
  }
  return 0;
}

function getUnicodeFromWin1251(c) {
  if (c>0 && c<=127) return c;
  if (c>=0xc0) return c+0x350;
  return win1251table.charCodeAt(c-0x80);
}

function checkWin1251(s) {
  if (!s) return '';
  var res='';
  for (var i=0; i<s.length; i++) {
    var c=getWin1251Code(s.charCodeAt(i));
    if (c>=32 && c<=255 || c==9 || c==10 /*|| c==13*/) {
      res+=s.charAt(i);
    }
    else if (c==13) { // remove CR's
      if (i+1<s.length && s.charCodeAt(i+1)==10) {
        // CR followed by LF => skip
      }
      else {
        // CR not followed by LF => replace with LF
        res+='\n';
      }
    }
  }
  return trim(res);
}

function trim(s) {
  return s.replace(/^\s+/, '').replace(/\s+$/, '');
}

function decodeURIComponentWin1251(s) {
  if (!s) return '';
  var us='';
  for (var i=0; i<s.length; i++) {
    var c=s.charAt(i);
    if (c=='%') {
      if ('u'==s.charAt(i+1)) {
        us+=String.fromCharCode(parseInt(s.substring(i+2, i+6), 16));
        i+=5;
      }
      else {
        us+=String.fromCharCode(getUnicodeFromWin1251(parseInt(s.substring(i+1, i+3), 16)));
        i+=2;
      }
    }
    else if (c=='+') us+=' ';
    else us+=c;
  }
  return us;
}

function encodeURIComponentWin1251(s) {
  if (!s) return '';
  var us='';
  for (var i=0; i<s.length; i++) {
    var c=getWin1251Code(s.charCodeAt(i));
    if (c>=32 && c<=255 || c==9 || c==10) {
      if (c==32) us+='+';
      else if (c<=127) us+=encodeURIComponent(s.charAt(i));
      else us+='%'+zeroPad(c.toString(16), 2);
    }
  }
  return us;
}


function setCookie(name, value, expire, domain, path) {
  if ( expire == null ) {
    expire = new Date();
    expire.setTime(expire.getTime()+2*365*24*3600*1000); // +2 years
  }
  document.cookie=name+"="+encodeURIComponentWin1251(value)
   +((expire==null) ? "" : ("; expires="+expire.toGMTString()))
   +((domain==null) ? "" : ("; domain="+domain /*encodeURIComponent(domain)*/))
   +((path==null) ? "" : ("; path="+path /*encodeURIComponent(path)*/));
  return document.cookie;
}

function getCookie(name) {
  var cookies=document.cookie.split(/\s*;\s*/);
  for (var i=0;i<cookies.length;i++) {
    var pair=cookies[i].split("=");
    if (name==pair[0]) return decodeURIComponentWin1251(pair[1]);
  }
  return null;
}

function getParam(name) {
  var query = window.location.search + "";
  if (!query) return null;
  var qp=query.split(/\s*[\?&]\s*/);
  for (var i=0; i<qp.length;i++) {
    var pair=qp[i].split("=");
    if (name==pair[0]) return decodeURIComponentWin1251(pair[1]);
  }
  return null;
}

function getInnerText(node) {
  return (typeof(node.innerText)!="undefined") ? node.innerText : ( (typeof(node.text)!="undefined") ? node.text : node.textContent);
}

function setInnerText(node, s) {
  if (typeof(node.innerText)!="undefined") node.innerText=s;
  else if (typeof(node.text)!="undefined") node.text=s;
  else node.textContent=s;
}

function serializeXML(doc) {
  if (typeof(doc.xml)!="undefined") { //userAgent.isInternetExplorer
    return doc.xml;
  }
  else  {
    try { // if (userAgent.isMozilla || userAgent.isOpera)
      var s=new XMLSerializer();
      return s.serializeToString(doc);
    } catch(e) {
      return doc.xml;
    }
  }
}

function parseSqlDate(str) {
  var year, month, day, hour, minute, second, tzOffset;
  if (str==null) return null;
  if (str.match(/^(\d{4})-(\d+)-(\d+)(\s+|T)(\d+):(\d+):(\d+)/i)) {
    year=parseInt(RegExp.$1, 10);
    month=parseInt(RegExp.$2, 10);
    day=parseInt(RegExp.$3, 10);
    hour=parseInt(RegExp.$5, 10);
    minute=parseInt(RegExp.$6, 10);
    second=parseInt(RegExp.$7, 10);
    return new Date(year, month-1, day, hour, minute, second);
  }
  else if (str.match(/^(\d{4})-(\d+)-(\d+)/)) {
    year=parseInt(RegExp.$1, 10);
    month=parseInt(RegExp.$2, 10);
    day=parseInt(RegExp.$3, 10);
    hour=0;
    minute=0;
    second=0;
    return new Date(year, month-1, day, hour, minute, second);
  }
  else return null;
}

function formatAtomDate(d) {
  if (d==null) return '';
  return zeroPad(d.getFullYear(),4)+'-'+zeroPad(d.getMonth()+1,2)+'-'+zeroPad(d.getDate(),2)
    +'T'+zeroPad(d.getHours(),2)+':'+zeroPad(d.getMinutes(),2)+':'+zeroPad(d.getSeconds(),2)
    +'+'+zeroPad(-d.getTimezoneOffset()/60,2)+':00';
}

function formatSqlDate(d) {
  if (d==null) return '';
  return zeroPad(d.getFullYear(),4)+'-'+zeroPad(d.getMonth()+1,2)+'-'+zeroPad(d.getDate(),2)
    +' '+zeroPad(d.getHours(),2)+':'+zeroPad(d.getMinutes(),2)+':'+zeroPad(d.getSeconds(),2);
}


// Source: http://www.movable-type.co.uk/scripts/sha1.html

function sha1win1251(str) {
  var f=function(s, x, y, z) {
    switch (s) {
    case 0: return (x & y) ^ (~x & z);           // Ch()
    case 1: return x ^ y ^ z;                    // Parity()
    case 2: return (x & y) ^ (x & z) ^ (y & z);  // Maj()
    case 3: return x ^ y ^ z;                    // Parity()
    }
  }
  var ROTL=function(x, n) { return (x<<n) | (x>>>(32-n)); }
  var toHexStr=function(n) {
    var s="", v;
    for (var i=7; i>=0; i--) { v=(n>>>(i*4)) & 0xf; s+=v.toString(16); }
    return s;
  }
  var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
  // PREPROCESSING
  var msg=checkWin1251(str)+String.fromCharCode(0x402); //+String.fromCharCode(0x80); // getWin1251Code(0x402)=>0x80
  // add trailing '1' bit to string [§5.1.1]
  // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
  var l = Math.ceil(msg.length/4) + 2;  // long enough to contain msg plus 2-word length
  var N = Math.ceil(l/16);              // in N 16-int blocks
  var M = new Array(N);
  for (var i=0; i<N; i++) {
    M[i] = new Array(16);
    for (var j=0; j<16; j++) {  // encode 4 chars per integer, big-endian encoding
      M[i][j] = (getWin1251Code(msg.charCodeAt(i*64+j*4))<<24) | (getWin1251Code(msg.charCodeAt(i*64+j*4+1))<<16) |
                (getWin1251Code(msg.charCodeAt(i*64+j*4+2))<<8) | (getWin1251Code(msg.charCodeAt(i*64+j*4+3)));
    }
  }
  // add length (in bits) into final pair of 32-bit integers (big-endian) [5.1.1]
  // note: most significant word would be ((len-1)*8 >>> 32, but since JS converts
  // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
  M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14])
  M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
  // set initial hash value [§5.3.1]
  var H0 = 0x67452301;
  var H1 = 0xefcdab89;
  var H2 = 0x98badcfe;
  var H3 = 0x10325476;
  var H4 = 0xc3d2e1f0;
  // HASH COMPUTATION [§6.1.2]
  var W = new Array(80); var a, b, c, d, e;
  for (var i=0; i<N; i++) {
      // 1 - prepare message schedule 'W'
      for (var t=0;  t<16; t++) W[t] = M[i][t];
      for (var t=16; t<80; t++) W[t] = ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
      // 2 - initialise five working variables a, b, c, d, e with previous hash value
      a = H0; b = H1; c = H2; d = H3; e = H4;
      // 3 - main loop
      for (var t=0; t<80; t++) {
          var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants
          var T = (ROTL(a,5) + f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff;
          e = d;
          d = c;
          c = ROTL(b, 30);
          b = a;
          a = T;
      }
      // 4 - compute the new intermediate hash value
      H0 = (H0+a) & 0xffffffff;  // note 'addition modulo 2^32'
      H1 = (H1+b) & 0xffffffff;
      H2 = (H2+c) & 0xffffffff;
      H3 = (H3+d) & 0xffffffff;
      H4 = (H4+e) & 0xffffffff;
  }
  return toHexStr(H0) + toHexStr(H1) + toHexStr(H2) + toHexStr(H3) + toHexStr(H4);
}

function madr(a,d) {
  return a+String.fromCharCode(64)+d.replace(/#/g,'\056');
}

function mto(a,d) {
  var m = madr(a,d);
  document.write(String.fromCharCode(60,97,32,104)+'ref=\"mai'+String.fromCharCode(108,116,111,58)+m+'\">'+m+'\074/a\076');
}

// request img counter
var reqImg=new Array();
function requestImg(u) {
  var i=new Image();
  i.src=u;
  return reqImg.push(i);
}

var traceBuf='';

function trace(s) {
  var traceDiv=$('traceOutput');
  if (!traceDiv) return;
  if (traceBuf.length>10000) traceBuf=traceBuf.substr(5000);
  traceBuf+=s+'\n';
  traceDiv.set('text', traceBuf);
  traceDiv.scrollTo(0, traceDiv.getScrollSize().y);
}

/*
 * Fixes for mootools: encodeURIComponent -> encodeURIComponentWin1251
 */

Hash.implement({
  toQueryString: function(base){
    var queryString = [];
    Hash.each(this, function(value, key){
      if (base) key = base + '[' + key + ']';
      var result;
      switch ($type(value)){
        case 'object': result = Hash.toQueryString(value, key); break;
        case 'array':
          var qs = {};
          value.each(function(val, i){
            qs[i] = val;
          });
          result = Hash.toQueryString(qs, key);
        break;
        default: result = key + '=' + encodeURIComponentWin1251(value);
      }
      if (value != undefined) queryString.push(result);
    });

    return queryString.join('&');
  }
});

Element.implement({
  toQueryString: function(){
    var queryString = [];
    this.getElements('input, select, textarea', true).each(function(el){
      if (!el.name || el.disabled || el.type == 'submit' || el.type == 'reset' || el.type == 'file') return;
      var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
        return opt.value;
      }) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;
      $splat(value).each(function(val){
        if (typeof val != 'undefined') queryString.push(el.name + '=' + encodeURIComponentWin1251(val));
      });
    });
    return queryString.join('&');
  }
});


Cookie.implement({
  write: function(value){
    value = encodeURIComponentWin1251(value);
    if (this.options.domain) value += '; domain=' + this.options.domain;
    if (this.options.path) value += '; path=' + this.options.path;
    if (this.options.duration){
      var date = new Date();
      date.setTime(date.getTime() + this.options.duration * 24 * 60 * 60 * 1000);
      value += '; expires=' + date.toGMTString();
    }
    if (this.options.secure) value += '; secure';
    this.options.document.cookie = this.key + '=' + value;
    return this;
  }
});

/*
 * The following Element extensions are based on: Element.Forms.js
 * http://clientside.cnet.com/wiki/cnet-libraries
 * Copyright 2006 CNET Networks, Inc.
 */

Element.implement({
  getTextInRange: function(start, end) {
    return this.get('value').substring(start, end);
  },
  getSelectedText: function() {
    //if(Browser.Engine.trident) return document.selection.createRange().text;
    return this.get('value').substring(this.getSelectionStart(), this.getSelectionEnd());
  },
  _getSelectionPos: function(startOrEnd) {
    if(Browser.Engine.trident) {
      //this.focus(); - removed because this had side effects
      var range=null;
      if (document.activeElement==this) range=document.selection.createRange();
      if (!range) {
        range=this.retrieve('ys.caretwatcher.selectionRange'); // this property must be stored by monitoring process
        //trace('using saved selectionRange; range='+range);
      }
      if (!range) return 0; // can't create range -> returning 0
      if (range.compareEndPoints("StartToEnd", range)!=0) { range=range.duplicate(); range.collapse(startOrEnd); }
      var bm1=range.getBookmark().charCodeAt(2)-2;
      if (this.get('tag')=='textarea') {
        var taRange=range.duplicate();
        taRange.moveToElementText(this);
        var bm2=taRange.getBookmark().charCodeAt(2)-2;
        var count=bm1-bm2;
        var val=this.value;
        for (var charCount=0; count>0; charCount++) if (val.charAt(charCount)!='\r') count--;
        return charCount;
      }
      else {
        return bm1;
      }
    }
    else {
      return startOrEnd? this.selectionStart : this.selectionEnd;
    }
  },
  getSelectionStart: function() {
    return this._getSelectionPos(true);
  },
  getSelectionEnd: function() {
    return this._getSelectionPos(false);
  },
  getSelectedRange: function() {
    return {
      start: this.getSelectionStart(),
      end: this.getSelectionEnd()
    }
  },
  setCaretPosition: function(pos) {
    if(pos == 'end') pos = this.get('value').length;
    this.selectRange(pos, pos);
    return this;
  },
  getCaretPosition: function() {
    return this.getSelectionStart();
  },
  selectRange: function(start, end) {
    this.focus();
    if(Browser.Engine.trident) {
      var range = this.createTextRange();
      range.collapse(true);
      var ncr=0;
      for (var i=0; i<start; i++) if (this.value.charAt(i)=='\r') ncr++;
      range.moveStart('character', start - ncr);
      if (end>start) {
        ncr=0;
        for (; i<end; i++) if (this.value.charAt(i)=='\r') ncr++;
        range.moveEnd('character', end - start - ncr);
      }
      range.select();
      return this;
    }
    else {
      this.setSelectionRange(start, end);
    }
    return this;
  },
  insertAtCursor: function(value, select) {
    var start=this.getSelectionStart();
    var end=this.getSelectionEnd();
    var oldValue=this.get('value');
    var savedScrollTop, savedScrollLeft;
    if (Browser.Engine.gecko) { savedScrollTop=this.scrollTop; savedScrollLeft=this.scrollLeft; }
    this.set('value', oldValue.substring(0, start) + value + oldValue.substring(end, oldValue.length));
    if (Browser.Engine.gecko) { this.scrollTop=savedScrollTop; this.scrollLeft=savedScrollLeft; }
    var newValue=this.get('value');
    var insertedLength=newValue.length - (oldValue.length - (end-start)); // might be different from value.length
    if($pick(select, true)) this.selectRange(start, start + insertedLength);
    else this.setCaretPosition(start + insertedLength);
    return this;
  },
  insertAroundCursor: function(options, select) { // not tested with textarea; rather buggy
    options = $extend({
      before: '',
      defaultMiddle: '', // used when no text selected
      after: ''
    }, options);
    var text = this.getSelectedText() || options.defaultMiddle;
    return this.insertAtCursor(options.before + text + options.after, select); // simple workaround
//    var start = this.getSelectionStart();
//    var end = this.getSelectionEnd();
//    if(start == end) {
//      var text = this.get('value');
//      this.set('value', text.substring(0, start) + options.before + value + options.after + text.substring(end, text.length));
//      this.selectRange(start + options.before.length, end + options.before.length + value.length);
//      text = null;
//    } else {
//      text = this.get('value').substring(start, end);
//      this.set('value', this.get('value').substring(0, start) + options.before + text + options.after + this.get('value').substring(end, this.get('value').length));
//      var selStart = start + options.before.length;
//      if($pick(select, true)) this.selectRange(selStart, selStart + text.length);
//      else this.setCaretPosition(selStart + text.length);
//    }
//    return this;
  }
});


Element.Properties.inputValue = {

    get: function(){
       switch(this.get('tag')) {
         case 'select':
          vals = this.getSelected().map(function(op){
            var v = $pick(op.get('value'),op.get('text'));
            return (v=="")?op.get('text'):v;
          });
          return this.get('multiple')?vals:vals[0];
        case 'input':
          switch(this.get('type')) {
            case 'checkbox':
              return this.get('checked')?this.get('value'):false;
            case 'radio':
              var checked;
              if (this.get('checked')) return this.get('value');
              $(this.getParent('form')||document.body).getElements('input').each(function(input){
                if (input.get('name') == this.get('name') && input.get('checked')) checked = input.get('value');
              }, this);
              return checked||null;
          }
         case 'input': case 'textarea':
          return this.get('value');
        default:
          return this.getProperty('inputValue');
       }
    },

    set: function(value){
      switch(this.get('tag')){
        case 'select':
          this.getElements('option').each(function(op){
            var v = $pick(op.get('value'), op.get('text'));
            if (v=="") v = op.get('text');
            op.set('selected', $splat(value).contains(v));
          });
          break;
        case 'input':
          if (['radio','checkbox'].contains(this.get('type'))) {
            this.set('checked', $type(value)=="boolean"?value:$splat(value).contains(this.get('value')));
            break;
          }
        case 'textarea': case 'input':
          this.set('value', $type(value)=="array"?value[0]:value);
        default:
          this.setProperty('inputValue', $type(value)=="array"?value[0]:value);
      }
      return this;
    },

    erase: function() {
      switch(this.get('tag')) {
        case 'select':
          this.getElements('option').each(function(op) {
            op.set('selected', false);
          });
          break;
        case 'input':
          if (['radio','checkbox'].contains(this.get('type'))) {
            this.set('checked', false);
            break;
          }
        case 'input': case 'textarea': case 'hidden':
          this.set('value', '');
        default:
          this.set('inputValue', '');
      }
      return this;
    }

};


var ysScroller = Scroller; // Scroller bug fixed - no need for override

/*
 * AutoSuggest class
 * Copyright 2008 Yaroslav Stavnichiy
 */

var AutoSuggest=new Class({

  Implements: [/*Events,*/ Options],

  options: {
    showDelay: 750,
    className: null,
    multiChoice: ','
  },

  initialize: function(elements, options) {
    //var params = Array.link(arguments, {options: Object.type, elements: $defined});
    this.setOptions(options || null);

    this.tip=new Element('div', {'class':'autosuggest_tips', html:'',
      styles:{position: 'absolute', top:0, left:0, visibility:'hidden'}}).inject(document.body);

    if (this.options.className) this.tip.addClass(this.options.className);
    else this.tip.setStyles({backgroundColor:'#fff', 'z-index':1001, border:'solid 1px #808080'});

    this.tip.addEvents({mouseover:this.tipOver.bindWithEvent(this), mousedown:this.tipClick.bindWithEvent(this)});

    if (elements) this.attach(elements);
  },

  attach: function(elements) {
    CaretWatcher.attach(elements);
    $$(elements).each(function(element){
      var events={};
      events['blur']=this.elementBlur.bindWithEvent(this, element);
      events[(Browser.Engine.trident || Browser.Engine.webkit) ? 'keydown':'keypress']=this.elementKeyPress.bindWithEvent(this, element); // Opera can't cancel keydown
      element.store('ys.autosuggest.events', events);
      element.addEvents(events);
      element.set('autocomplete', 'off');
    }, this);
    return this;
  },

  detach: function(elements) {
    CaretWatcher.detach(elements);
    $$(elements).each(function(element){
      var events=element.retrieve('ys.autosuggest.events');
      if (events) {
        for (var evtType in events) {
          element.removeEvent(evtType, events[evtType]);
        }
        element.eliminate('ys.autosuggest.events');
      }
      element.erase('autocomplete');
    });
    return this;
  },

  elementKeyPress: function(event, element) {
    this.inFocus=element;
    if (this.keyTimer) { $clear(this.keyTimer); this.keyTimer=null; }
    //alert(event.key);
    var items=this.tip.getChildren();
    if (event.code==40) { // event.key=='down' is buggy
      if (this.shown) {
        if (this.position && this.position<items.length) {
          items[this.position-1].removeClass('selected');
          this.position=this.position+1;
          items[this.position-1].addClass('selected');
        }
        else if (!this.position) {
          this.position=1;
          items[this.position-1].addClass('selected');
        }
      }
      else { // show tips
        if (this.position>=1) items[this.position-1].removeClass('selected');
        this.position=0;
        this.show(element);
        this.queryTag(element); // if you need to add delay: this.queryTag.delay(1000, this, element);
      }
      return false;
    }
    else if (event.key=='up') { // up
      if (this.shown && this.position) {
        items[this.position-1].removeClass('selected');
        this.position=this.position-1;
        if (this.position>=1) items[this.position-1].addClass('selected');
      }
      return false;
    }
    else if (event.key=='esc') { // esc
      if (this.shown) {
        this.hide();
        return false;
      }
    }
    else if (event.key=='enter') { // enter
      if (this.shown) {
        if (this.position) {
          this.chooseValue(element, items[this.position-1].get('text'));
          this.hide();
          event.stop();
          return false;
        }
        else {
          this.hide();
        }
      }
    }
    else if (event.code<33 || event.code>40) { // do not react on left/right/up/dn/home/end/pgup/pgdn arrows
      this.keyTimer=this.queryTag.delay(this.options.showDelay, this, element);
    }
    return true;
  },

  elementBlur: function(event) {
    this.inFocus=null;
    this.hide();
    if (this.keyTimer) { $clear(this.keyTimer); this.keyTimer=null; }
  },

  tipOver: function(event) {
    var items=this.tip.getChildren();
    if (!items.length) return;
    var i;
    for (i=0; i<items.length; i++) {
      if (items[i]==event.target) {
        if (this.position!=i+1) {
          if (this.position) items[this.position-1].removeClass('selected');
          this.position=i+1;
          items[this.position-1].addClass('selected');
        }
        return;
      }
    }
  },

  tipClick: function(event) {
    var items=this.tip.getChildren();
    var element=this.shown;
    if (!items.length || !element) return;
    var i;
    for (i=0; i<items.length; i++) {
      if (items[i]==event.target) {
        if (this.position!=i+1) {
          if (this.position) items[this.position-1].removeClass('selected');
          this.position=i+1;
          items[this.position-1].addClass('selected');
        }
        this.chooseValue(element, items[this.position-1].get('text'));
        this.hide();
        (function(){element.focus();}).delay(100);
        return;
      }
    }
  },

  queryTag: function(element) {
    var tag=element.value;
    if (this.options.multiChoice) {
      var pos=element.getCaretPosition();
      var start=tag.lastIndexOf(this.options.multiChoice, pos-1)+1;
      var end=tag.indexOf(this.options.multiChoice, pos);
      if (end<0) end=tag.length;
      tag=tag.substring(start, end);
    }
    tag=tag.trim();
    if (tag==this.tagQueryPrevValue) {
      return;
    }
    this.tagQueryPrevValue=tag;
    this.getChoices(tag, element);
  },

  getChoices: function(tag, element) { // to be overriden in subclasses; must call this.loadTips(element, array) to load result
    this.loadTips(element, ['item 1','item 2','item 3']);
  },

  chooseValue: function(element, newValue) {
    if (!this.options.multiChoice) {
      element.value=newValue;
      element.setCaretPosition('end');
    }
    else {
      var pos=element.getCaretPosition();
      var val=element.value;
      var start=val.lastIndexOf(this.options.multiChoice, pos-1)+1;
      var end=val.indexOf(this.options.multiChoice, pos);
      if (end<0) end=val.length;
      //alert(pos+'|'+val+'|'+start);
      if (start) newValue=' '+newValue;
      if (end==val.length) newValue=newValue+', ';
      element.value=val.substr(0, start)+newValue+val.substr(end);
      element.setCaretPosition(start+newValue.length);
    }
  },

  loadTips: function(element, array) {
    this.position=0;
    this.tip.empty();
    for (var i=0; i<array.length; i++) {
      var item=array[i];
      if ($type(item)=='string') {
        new Element('div', {text:item}).inject(this.tip);
      }
      else {
        var div=new Element('div', {text:item.text});
        if (item.bold) div.setStyle('font-weight', 'bold');
        if (item.gray) div.setStyle('color', '#808080');
        div.inject(this.tip);
      }
    }
    if (i) this.show(element);
    else this.hide();
  },

  show: function(element) {
    //trace('show: '+element.value);
    if (!this.tip.getChildren().length || this.inFocus!=element) { this.hide(); return; }
    var size=element.getSize(), pos=element.getOffsets();
    this.tip.setStyles({'width':(size.x-2)+'px', 'left':pos.x+'px', 'top':pos.y+size.y+'px', 'visibility':'visible'});
    this.shown=element;
  },

  hide: function() {
    if (this.shown) {
      this.tip.setStyles({'left':'0', 'top':'0', 'visibility':'hidden'});
      this.shown=null;
    }
  }

});

var CaretWatcherClass=new Class({
  initialize: function(elements) {
    if (elements) this.attach(elements);
  },

  attach: function(elements) {
    if (Browser.Engine.trident) {
      $$(elements).each(function(element){
        var events={};
        Element.NativeEvents['beforedeactivate']=1; // mootools hack: enable this event type
        events['beforedeactivate']=this.elementSaveSelection.bindWithEvent(this, element);
        element.store('ys.caretwatcher.events', events);
        element.addEvents(events);
      }, this);
    }
    return this;
  },

  detach: function(elements) {
    if (Browser.Engine.trident) {
      $$(elements).each(function(element){
        var events=element.retrieve('ys.caretwatcher.events');
        if (events) {
          for (var evtType in events) {
            element.removeEvent(evtType, events[evtType]);
          }
          element.eliminate('ys.caretwatcher.events');
          element.eliminate('ys.caretwatcher.selectionRange');
        }
      });
    }
    return this;
  },

  elementSaveSelection: function(event, element) {
    element.store('ys.caretwatcher.selectionRange', document.selection.createRange().duplicate());
  }
});

var CaretWatcher=new CaretWatcherClass();

var TagSuggest=new Class({

  Extends: AutoSuggest,

  options: {
    taggedBy: null,
    defaultNS: null,
    normalizeTag: true,
    dupTags: false,
    finalTags: true,
    newTags: false,
    byRating: true
  },

  initialize: function(elements, options) {
    this.parent(elements, options); //this executes the initialize() function in the parent class
    this.req=new Request({method:'get', url:'/t4/TagQuery', link:'cancel', encoding:'windows-1251'});
    this.req.addEvent('success', this.onRequestSuccess.bind(this));
  },

  getChoices: function(tag, element) { // to be overriden in subclasses; must call this.loadTips(element, array) to load result
    this.reqElement=element;
    //trace('getChoices1: this.reqElement='+this.reqElement);
    this.req.send('tag='+encodeURIComponentWin1251(tag)
      +(this.options.defaultNS?'&defaultNS='+this.options.defaultNS:'')
      +(this.options.taggedBy?'&taggedBy='+encodeURIComponentWin1251(this.options.taggedBy):'')
      +(this.options.finalTags?'&finalTags=yes':'')
      +(this.options.normalizeTag?'&normalizeTag=yes':'')
      +(this.options.dupTags?'&dupTags=yes':'')
      +(this.options.newTags?'&newTags=yes':'')
      +(this.options.byRating?'&byRating=yes':'')
    );
    //trace('getChoices2: this.reqElement='+this.reqElement);
  },

  onRequestSuccess: function(responseText, responseXML) {
    var tagsElement=responseXML.documentElement;
    if (tagsElement.nodeName!='tags') {
      //e.innerHTML='<span style="color:#808080;">-</span>';
      if (tagsElement.nodeName=='error') {
        //alert('Ошибка:\n'+getTextContent(tagsElement));
        return;
      }
      //alert('tags expected, received: {'+tagsElement.namespaceURI+'}'+tagsElement.nodeName);
      return;
    }
    var array=[];
    for (var i=0; i<tagsElement.childNodes.length; i++) {
      var n=tagsElement.childNodes[i];
      if (n.nodeType==1) { // ELEMENT_NODE
        if (n.nodeName=='tag') {
          var item={};
          item.bold=n.getAttribute('direct-match');
          item.gray=!n.getAttribute('final') && !this.options.finalTags;
          item.text=getInnerText(n);
          array.push(item);
        }
      }
    }
    //trace('onRequestSuccess: this.reqElement='+this.reqElement);
    this.loadTips(this.reqElement, array);
    this.reqElement=null;
  }

});


var PopupPanel=new Class({

  Implements: [/*Events,*/ Options],

  options: {
    offsetX: 0,
    offsetY: 0,
    html: '',
    className: null
  },

  initialize: function(elements, options) {
    //var params=Array.link(arguments, {options: Object.type, elements: $defined});
    this.setOptions(options || null);

    this.popup=new Element('div', {'class':'popupPanel', html:this.options.html,
      styles:{position: 'absolute', top:0, left:0, visibility:'hidden'}}).inject(document.body);

    if (this.options.className) this.popup.addClass(this.options.className);
    //else this.popup.setStyles({backgroundColor:'#fff', 'z-index':1001, border:'solid 1px #808080'});

    this.popup.addEvents({mouseover:this.popupMouseOver.bindWithEvent(this), mouseleave:this.popupMouseLeave.bindWithEvent(this)});

    this.windowMouseListener=this.windowMouseMove.bindWithEvent(this);

    if (elements) this.attach(elements);
  },

  attach: function(elements) {
    $$(elements).each(function(element){
      var events={};
      events['mouseover']=this.elementMouseOver.bindWithEvent(this, element);
      events['mouseleave']=this.elementMouseLeave.bindWithEvent(this, element);
      element.store('ys.popuppanel.events', events);
      element.addEvents(events);
    }, this);
    return this;
  },

  detach: function(elements) {
    $$(elements).each(function(element){
      var events=element.retrieve('ys.popuppanel.events');
      if (events) {
        for (var evtType in events) {
          element.removeEvent(evtType, events[evtType]);
        }
        element.eliminate('ys.popuppanel.events');
      }
    });
    return this;
  },

  popupMouseOver: function(event) {
    //trace('poMO');
    this.mouseInsidePopup=true;
    if (!Browser.Engine.trident) $(document.body).addEvent('mousemove', this.windowMouseListener);
  },

  popupMouseLeave: function(event) {
    //trace('poML');
    this.hide();
  },

  windowMouseMove: function(event) {
    //trace('poML');
    var pos=this.popup.getOffsets(), size=this.popup.getSize();
    //trace('pos='+pos.x+','+pos.y+' size='+size.x+','+size.y+' mouse='+event.page.x+','+event.page.y+' mouse_client='+event.event.clientX+','+event.event.clientY);
    if (event.page.x<pos.x || event.page.y<pos.y || event.page.x>pos.x+size.x || event.page.y>pos.y+size.y)
      this.hide();
  },

  elementMouseOver: function(event, element) {
    //trace('elMO');
    this.show(element);
  },

  elementMouseLeave: function(event, element) {
    (function(){
      //trace('elML');
      if (!this.mouseInsidePopup && this.shown==element) this.hide();
    }).delay(100, this);
  },

  show: function(element) {
    if (this.shown) {
      if (this.shown==element) return;
      this.hide();
    }
    var pos=element.getOffsets();
    this.popup.setStyles({'left':(pos.x+this.options.offsetX)+'px', 'top':(pos.y+this.options.offsetY)+'px'});
    this.updatePanel(element);
    this.popup.setStyle('visibility', 'visible');
    this.shown=element;
    //trace('popup shown');
  },

  hide: function() {
    this.popup.style.visibility='hidden';
    this.mouseInsidePopup=false;
    if (!Browser.Engine.trident) $(document.body).removeEvent('mousemove', this.windowMouseListener);
    this.shown=null;
    //trace('popup hidden');
  },

  updatePanel: function(element) {
    // to be overriden in subclasses
    // subclasses can update this.popup element (innerHTML, styles, etc.)
  }
});

var UserPicPalette=new Class({

  Extends: PopupPanel,

  options: {
    selectedImagesTitle: "Выбранные&nbsp;картинки:&nbsp;&nbsp;&nbsp;&nbsp;<a href='/t4/media/' target='_blank'>Галерея &raquo;</a>",
    uploadedImagesTitle: "Загруженные&nbsp;картинки:&nbsp;&nbsp;&nbsp;&nbsp;<a href='/t4/UserMediaUpload' target='_blank' onclick='window.open(\"/t4/UserMediaUpload\", \"_blank\", \"width=450,height=550,menubar=no,toolbar=no,location=no,resizable=yes,scrollbars=yes,status=no\");return false;'>Загрузить &raquo;</a>",
    selectedImages: true,
    uploadedImages: true,
    replaceValue: false, // select replaces target's value; otherwise inserts at cursor
    width:'450px'
  },

  initialize: function(anchor, target, options) {
    this.parent(anchor, options); //this executes the initialize() function in the parent class
    this.target=$(target);
    this.anchor=$(anchor);
    this.refreshRequired=true;
    CaretWatcher.attach(target);
    if (this.options.selectedImages) {
      new Element('div', {'class':'selectedImagesTitle', html:this.options.selectedImagesTitle,
        styles:{}}).inject(this.popup);
      this.selectedBar=new Element('div', {'class':'selectedImagesBar',
        styles:{width:this.options.width, overflow:'hidden'}}).inject(this.popup);
      this.selectedScroller=new ysScroller(this.selectedBar, {area:50});
      var ssStart=this.selectedScroller.start.bind(this.selectedScroller);
      var ssStop=this.selectedScroller.stop.bind(this.selectedScroller);
      this.selectedBar.addEvents({mouseover:ssStart, mouseleave:ssStop, mousedown:ssStop, mouseup:ssStart});
      this.selectedBar.addEvent('click', this.selectedClick.bindWithEvent(this));
    }
    if (this.options.uploadedImages) {
      new Element('div', {'class':'uploadedImagesTitle', html:this.options.uploadedImagesTitle,
        styles:{}}).inject(this.popup);
      this.uploadedBar=new Element('div', {'class':'uploadedImagesBar',
        styles:{width:this.options.width, overflow:'hidden'}}).inject(this.popup);
      this.uploadedScroller=new ysScroller(this.uploadedBar, {area:50});
      var usStart=this.uploadedScroller.start.bind(this.uploadedScroller);
      var usStop=this.uploadedScroller.stop.bind(this.uploadedScroller);
      this.uploadedBar.addEvents({mouseover:usStart, mouseleave:usStop, mousedown:usStop, mouseup:usStart});
      this.uploadedBar.addEvent('click', this.uploadedClick.bindWithEvent(this));
    }
    this.req=new Request({method:'get', url:'/t4/UserPalette', link:'cancel', encoding:'windows-1251'});
    this.req.addEvent('success', this.onRequestSuccess.bind(this));
    $(window).addEvent('blur', this.windowBlur.bind(this));
  },

  selectImage: function(event) {
    var sel=$(event.target);
    var imgCode=null;
    while (sel && !(imgCode=sel.get('imgCode'))) sel=sel.getParent();
    if (imgCode) {
      if (ysFCKattached[this.target.get('id')]) {
        var oEditor=FCKeditorAPI.GetInstance(this.target.get('id'));
        if (oEditor.EditMode==FCK_EDITMODE_WYSIWYG) oEditor.InsertHtml(imgCode);
        oEditor.Focus();
      }
      else {
        if (this.options.replaceValue) this.target.value=imgCode;
        else this.target.insertAtCursor(imgCode, false);
      }
      this.hide();
    }
  },

  selectedClick: function(event) {
    this.selectImage(event);
  },

  uploadedClick: function(event) {
    this.selectImage(event);
  },

  updatePanel: function(element) {
    if (this.refreshRequired) {
      this.refreshRequired=false;
      this.req.send('selective=yes'
        +(this.options.selectedImages?'&selectedImages=yes':'')
        +(this.options.uploadedImages?'&uploadedImages=yes':'')
      );
    }
  },

  windowBlur: function() {
    this.refreshRequired=true;
  },

  onRequestSuccess: function(responseText, responseXML) {
    var tagsElement=responseXML.documentElement;
    if (tagsElement.nodeName!='user-palette') {
      if (tagsElement.nodeName=='error') {
        alert('Ошибка:\n'+getTextContent(tagsElement));
        return;
      }
      alert('<user-palette> expected, received: {'+tagsElement.namespaceURI+'}'+tagsElement.nodeName);
      return;
    }
    for (var i=0; i<tagsElement.childNodes.length; i++) {
      var n=tagsElement.childNodes[i];
      if (n.nodeType==1) { // ELEMENT_NODE
        if (n.nodeName=='selected-images' && this.options.selectedImages) {
          this.selectedBar.innerHTML='<div>'+serializeXML(n)+'</div>';
        }
        else if (n.nodeName=='uploaded-images' && this.options.uploadedImages) {
          this.uploadedBar.innerHTML='<div>'+serializeXML(n)+'</div>';
        }
      }
    }
  }
});


var SmileyPalette=new Class({

  Extends: PopupPanel,

  options: {
    icons: [
      ['smile','sad','wink2','mosking','biggrin','lol','rofl','boredom','cray','mda','nea','scratch_one-s_head','umnik2','unknw','wacko2'],
      ['yes3','i-m_so_happy','good','hi','ok','drinks','friends','padonak','SHABLON_padonak_01','don-t_mention'],
      ['clapping','acute','aggressive','rtfm','new_russian','russian_ru','big_boss','punish','dash2','fool','bad','ireful2'],
      ['crazy','beee','blum','on_the_quiet','sarcastic','sarcastic_blum','sarcastic_hand','boast','blush','pardon','bye'],
      ['help','heat','suicide2','bomb','diablo','girl_devil','hunter'],
      ['heart','give_heart2','give_rose','air_kiss','man_in_love','angel','flirt','girl_in_love','kiss2','kiss3','parting']
    ],
    moreIcons: [
      ['girl_smile','girl_sad','girl_wink','girl_haha','girl_impossible','hysteric','girl_to_take_umbrage','girl_cray3','girl_cray','girl_cray2'],
      ['girl_crazy','girl_wacko','girl_blum','curtsey','girl_dance','girl_hospital','to_babruysk','girl_witch'],
      ['sun_bespectacled','victory','yahoo','dance2','dance4','whistle3','music2','gamer2','mail1','moil','beach'],
      ['to_become_senile','training1','bb','paint2','popcorm1','wizard','dirol','mamba','lazy']
    ]
  },

  initialize: function(anchor, target, options) {
    this.parent(anchor, options); //this executes the initialize() function in the parent class
    this.target=$(target);
    this.anchor=$(anchor);
    this.refreshRequired=true;
    CaretWatcher.attach(target);
    this.popup.addEvent('click', this.selectImage.bindWithEvent(this));
  },

  selectImage: function(event) {
    var sel=$(event.target);
    var imgCode=null;
    while (sel && !(imgCode=sel.get('imgCode'))) sel=sel.getParent();
    if (imgCode) {
      if (ysFCKattached[this.target.get('id')]) {
        var oEditor=FCKeditorAPI.GetInstance(this.target.get('id'));
        if (oEditor.EditMode==FCK_EDITMODE_WYSIWYG) oEditor.InsertHtml(imgCode);
        oEditor.Focus();
      }
      else {
        this.target.insertAtCursor(imgCode, false);
      }
      this.hide();
    }
  },

  updatePanel: function(element) {
    if (this.refreshRequired) {
      this.refreshRequired=false;
      var html='';
      for (var i=0; i<this.options.icons.length; i++) {
        var row=this.options.icons[i];
        for (var j=0; j<row.length; j++) {
          var ic=row[j];
          html+="<a imgCode='{{smiley:"+ic+"}}' href='#' onclick='return false'><img src='/i/smiley/"+ic+".gif' /></a>";
        }
        html+="<br/>";
      }
      new Element('div', {'class':'smileyPalette', html:html}).inject(this.popup);
    }
  }
});

var WikiHelpPalette=new Class({

  Extends: PopupPanel,

  options: {
    html: "<h3>Справка по wiki-формату</h3>"
        +"<p>Пустая строка - новый абзац. \\\\ - перевод строки.</p>"
        +"<p><b>Выделение текста:</b> <b>**жирный текст**</b>, <i>//курсив//</i>,<br/>"
        +"__ <u>подчеркнутый текст</u> __, <tt>##шрифт печатной машинки##</tt>.</p>"
        +"<p><b>Ссылки:</b> [[метка]] - ссылка на домашнюю страницу метки (энциклопедия). [[http://www.domain.ru/aaa/bbb.htm]] - внешняя ссылка.<br/>"
        +"[[метка|текст ссылки]], [[http://www.../b.htm|текст ссылки]] - так можно задать текст ссылки.</p>"
        +"<p><b>Картинки:</b> {{метка}} - картинка из галереи. {{http://www.../i.jpg}} - внешняя картинка.<br/>"
        +"{{метка|текст}}, {{http://www.../i.jpg|текст}} - так можно задать альтернативный текст.<br/>"
        +"{{метка|100x100}}, {{http://www.../i.jpg|текст|100}} - так можно задать размер картинки.</p>"
        +"<p>---- (четыре минуса в строке) - горизонтальная разделительная линия.</p>"
        +"<p>* пункт списка<br/>"
        +"# пункт пронумерованного списка<br/>"
        +"&gt; цитата<br/>"
        +": левый отступ<br/>"
        +"&gt;*# списки, цитаты и отступы могут быть вложенными</p>"
        +"<p>{{{ внутри тройных фигурных скобок текст не форматируется }}}</p>"
        +"<p>| Это | таблица |<br/>| из двух | колонок. |</p>",
    className: 'wikiHelpPalette'
  }
});


var ysModalBox=new Class({
  Implements: [Events, Options],

  options: {
  },

  initialize: function(formElement, options) {
    this.setOptions(options || null);
    this.form=$(formElement);
    this.form.addEvents({'click':this.formClick.bindWithEvent(this), 'submit':this.formSubmit.bindWithEvent(this)});
    this.form.setStyles({visibility:'hidden', position:'absolute', left:'0px', top:'0px', 'z-index':2001});
    $(document.body).adopt(this.form);

    this.modalBg=new Element('div', {'opacity':0.25, 'styles':{'display':'none', 'backgroundColor':'#000', 'position':'absolute', 'z-index':2000}});
    this.modalBg.addEvent('click', this.bgClick.bindWithEvent(this));
    $(document.body).adopt(this.modalBg);
    window.addEvent('resize', this.repositionModalBg.bindWithEvent(this));
    window.addEvent('scroll', this.repositionModalBg.bindWithEvent(this));
  },

  show: function(anchor, opts) {
    this.repositionModalBg();
    this.modalBg.setStyle('display','');
    anchor=$(anchor);
    var size=anchor.getSize(), pos=anchor.getOffsets(), bsize=this.form.getSize();
    //trace('sz='+size.x+'x'+size.y+' pos='+pos.x+'x'+pos.y+' bsz='+bsize.x+'x'+bsize.y);
    this.form.setStyles({left:(pos.x+size.x/2-bsize.x/2)+'px', top:(pos.y+size.y/2-bsize.y/2)+'px', visibility:'visible'});
  },

  hide: function() {
    this.form.setStyles({left:'0px', top:'0px', visibility:'hidden'});
    this.modalBg.setStyle('display','none');
  },

  repositionModalBg: function() {
    this.modalBg.setStyles({'left':'0px', 'top':'0px', 'width':'100%', 'height':window.getScrollHeight()+'px'});
  },

  formClick: function(event) {
    var sel=$(event.target);
    while (sel && !('input'==sel.get('tag') && 'button'==sel.get('type'))) sel=sel.getParent();
    if (sel) {
      var btnName=sel.get('name');
      this.hide();
      this.fireEvent('buttonclick', btnName);
    }
  },

  bgClick: function(event) {
    this.hide();
    this.fireEvent('buttonclick', 'close');
  },

  formSubmit: function(event) {
    this.hide();
    this.fireEvent('buttonclick', 'submit');
    event.preventDefault();
    return false;
  }
});


var WikiEditorToolbar=new Class({

  Implements: [/*Events,*/ Options],

  options: {
    //buttons_: ['bold','italic','underline','tt','link','image','hr','ul','ol','quote','indent'],
    buttons: {
      preview: {
        icon: 5, tip:'Предварительный просмотр'
      },
      bold: {
        gap:'8px', icon: 20, mark:'**', tip:'Жирный шрифт'
      },
      italic: {
        icon: 21, mark:'//', tip:'Курсив'
      },
      underline: {
        icon: 22, mark:'__', tip:'Подчеркивание'
      },
      link: {
        gap:'8px', icon: 34, tip:'Ссылка'
      },
      image: {
        icon: 37, tip:'Картинка'
      },
      hr: {
        icon: 40, ins:'\n\n----\n\n', tip:'Горизонтальная разделительная черта'
      },
      expand: {
        gap:'8px', icon: 66, tip:'Увеличить окно ввода'
      },
      help: {
        gap:'8px', icon: 47, tip:'Справка по wiki-формату'
      }
    },
    helpText: "<h3>Справка по wiki-формату</h3>"
      +"<p>Пустая строка - новый абзац. \\\\ - перевод строки.</p>"
      +"<p><b>Выделение текста:</b> <b>**жирный текст**</b>, <i>//курсив//</i>,<br/>"
      +"__ <u>подчеркнутый текст</u> __, <tt>##шрифт печатной машинки##</tt>.</p>"
      +"<p><b>Ссылки:</b> [[метка]] - ссылка на домашнюю страницу метки (энциклопедия). [[http://www.domain.ru/aaa/bbb.htm]] - внешняя ссылка.<br/>"
      +"[[метка|текст ссылки]], [[http://www.../b.htm|текст ссылки]] - так можно задать текст ссылки.</p>"
      +"<p><b>Картинки:</b> {{метка}} - картинка из галереи. {{http://www.../i.jpg}} - внешняя картинка.<br/>"
      +"{{метка|текст}}, {{http://www.../i.jpg|текст}} - так можно задать альтернативный текст.<br/>"
      +"{{метка|100x100}}, {{http://www.../i.jpg|текст|100}} - так можно задать размер картинки.</p>"
      +"<p>---- (четыре минуса в строке) - горизонтальная разделительная линия.</p>"
      +"<p>* пункт списка<br/>"
      +"# пункт пронумерованного списка<br/>"
      +"&gt; цитата<br/>"
      +": левый отступ<br/>"
      +"&gt;*# списки, цитаты и отступы могут быть вложенными</p>"
      +"<p>{{{ внутри тройных фигурных скобок текст не форматируется }}}</p>"
      +"<p>| Это | таблица |<br/>| из двух | колонок. |</p>"
      +"<p>Чтобы отключить функцию любого спецсимвола введите перед ним знак ~</p>"
  },

  initialize: function(anchor, target, options) {
    this.setOptions(options || null);
    this.target=$(target);
    this.anchor=$(anchor);
    if (!this.anchor || !this.target) return;
    this.anchor.store('ys.WikiEditorToolbar', this);
    CaretWatcher.attach(this.target);

    var html='';
    for (var btnName in this.options.buttons) {
      var btn=this.options.buttons[btnName];
      if (btn.gap) {
        html+='<img src="/i/p.gif" style="width:'+btn.gap+';height:1px;vertical-align:middle;" />';
      }
      if (btn.icon) {
        html+='<img src="/i/p.gif" class="wetb_btn" wetbCmd="'+btnName+'" style="background-position:0 -'+((btn.icon-1)*16)+'px;" title="'+escapeHTML(btn.tip)+'" />';
      }
    }
    var toolbar=new Element('div', {'class':'wikiEditorToolbar', html:html}).inject(this.anchor);
    toolbar.addEvent('click', this.clickButton.bindWithEvent(this));

    this.linkForm=new Element('form', {'class':'wikiEditorLinkDialog', html:'<div class="h">Вставить ссылку</div><div align="right"><label>URL: <input class="t" type="text" name="url" value="" size="30" /></label><br/><label>Текст: <input class="t" type="text" name="txt" value="" size="30" /></label></div><div align="center"><input class="b" type="submit" name="ok" value="Вставить" /> <input class="b" type="button" name="cancel" value="Отмена" /></div>'});
    this.linkBox=new ysModalBox(this.linkForm, {onButtonclick:this.linkActionDlgClick.bindWithEvent(this)});

    this.imageForm=new Element('form', {'class':'wikiEditorImageDialog', html:'<div class="h">Вставить картинку</div><div align="right"><label>URL: <input class="t" type="text" name="url" value="" size="30" /></label><br/><label>Текст: <input class="t" type="text" name="txt" value="" size="30" /></label></div><div align="center"><label>Ширина: <input class="t" type="text" name="picwidth" value="" size="8" /> пикселов</label></div><div align="center"><input class="b" type="submit" name="ok" value="Вставить" /> <input class="b" type="button" name="cancel" value="Отмена" /></div>'});
    this.imageBox=new ysModalBox(this.imageForm, {onButtonclick:this.imageActionDlgClick.bindWithEvent(this)});

    this.winClickHandler=this.winClick.bindWithEvent(this);
  },

  clickButton: function(event) {
    var sel=$(event.target);
    var btnName=null;
    while (sel && !(btnName=sel.get('wetbCmd'))) sel=sel.getParent();
    if (btnName) {
      var btn=this.options.buttons[btnName];
      if (btn.mark) {
        this.hideOverlay(null, true);
        this.target.insertAroundCursor({before:btn.mark,after:btn.mark,defaultMiddle:btn.tip}, true);
      }
      else if (btn.ins) {
        this.hideOverlay(null, true);
        this.target.insertAtCursor(btn.ins, false);
      }
      else {
        var action=this[btnName+'Action'];
        if (typeof(action)=='function') action.call(this);
      }
    }
  },

  linkAction: function() {
    this.hideOverlay();
    var text=this.target.getSelectedText();
    this.linkBox.show(this.target);
    this.linkForm.elements['url'].value='http://';
    this.linkForm.elements['txt'].value=text;
    this.linkForm.elements['url'].focus();
    this.linkForm.elements['url'].select();
  },

  linkActionDlgClick: function(btnName) {
    if (btnName=='ok' || btnName=='submit') {
      var text=this.target.getSelectedText();
      var title=this.linkForm.elements['txt'].value;
      this.target.insertAtCursor('[['+this.linkForm.elements['url'].value+(title?'|'+title:'')+']]', true);
    }
    else {
      this.target.focus();
    }
  },

  imageAction: function() {
    this.hideOverlay();
    var text=this.target.getSelectedText();
    this.imageBox.show(this.target);
    this.imageForm.elements['url'].value='http://';
    this.imageForm.elements['txt'].value=text;
    this.imageForm.elements['picwidth'].value='';
    this.imageForm.elements['url'].focus();
    this.imageForm.elements['url'].select();
  },

  imageActionDlgClick: function(btnName) {
    if (btnName=='ok' || btnName=='submit') {
      var text=this.target.getSelectedText();
      var title=this.imageForm.elements['txt'].value;
      var width=parseInt(this.imageForm.elements['picwidth'].value, 10);
      if (width>500) width=500;
      this.target.insertAtCursor('{{'+this.imageForm.elements['url'].value+(title?'|'+title:'')+(width?'|'+width:'')+'}}', true);
    }
    else {
      this.target.focus();
    }
  },

  expandAction: function() {
    this.hideOverlay();
    var newWidth=this.anchor.getSize().x-40;
    var newHeight=this.target.getSize().y+100;
    if (newHeight>700) newHeight=700;
    this.target.setStyles({width:newWidth+'px', height:newHeight+'px'});
    this.target.focus();
  },

  winClick: function() {
    $(document.body).removeEvent('click', this.winClickHandler);
    this.hideOverlay();
  },

  previewAction: function() {
    this.helpHide();
    if (this.previewBoxShown) {
      this.previewHide(null, true);
      return;
    }
    if (this.previewJustHidden || this.previewBox) return;
    var size=this.target.getSize();
    var pos=this.target.getOffsets();
    //trace('previewBox: w='+size.x+' h='+size.y+' x='+pos.x+' y='+pos.y);
    this.previewBox=new Element('div', {'class':'wikiPreview',
      styles:{position:'absolute', left:pos.x+'px', top:pos.y+'px', width:(size.x-2)+'px', height:(size.y-2)+'px', border:'solid 1px #808080'},
      events:{click:this.previewHide.bindWithEvent(this, true)},
      html:'<div style="height:14px;overflow:hidden;background-color:#808080;color:white;font:10px/13px sans-serif;text-align:right;cursor:default;">Предварительный просмотр <a class="close" href="#" onclick="return false;" style="text-decoration:none;background-color:#404040;color:white;font:bold 7px/8px sans-serif;padding:3px 6px;">X</a></div>'
        +'<div class="msgText" style="height:'+(size.y-2-14-8)+'px;overflow:auto;padding:4px;background-color:white;"><span style="color:#606060;">Ждите...</span></div>'});
    this.previewBox.inject(this.target, 'before');
    (function(){
      var text=this.target.value;
      var holder=this.previewBox.getElement('.msgText');
      if (text.trim().length==0) {
        holder.empty();
      }
      else {
        new Request.HTML({url:'/t4/WikiPreview', method:'post', update:holder}).send("wikiText="+encodeURIComponentWin1251(text));
      }
      $(document.body).addEvent('click', this.winClickHandler);
      this.previewBoxShown=true;
    }).delay(100, this);
  },

  previewHide: function(event, focus) {
    if (this.previewBoxShown) {
      this.previewBox.dispose();
      this.previewBox.destroy();
      this.previewBox=null;
      this.previewBoxShown=false;
      if (focus) this.target.focus();
      this.previewJustHidden=true;
      (function(){this.previewJustHidden=false;}).delay(500, this);
    }
  },

  helpAction: function() {
    this.previewHide();
    if (this.helpBoxShown) {
      this.helpHide(null, true);
      return;
    }
    if (this.helpJustHidden || this.helpBox) return;
    var size=this.target.getSize();
    var pos=this.target.getOffsets();
    //trace('helpBox: w='+size.x+' h='+size.y+' x='+pos.x+' y='+pos.y);
    this.helpBox=new Element('div', {'class':'wikiHelpPalette',
      styles:{position:'absolute', left:pos.x+'px', top:pos.y+'px', width:(size.x-2)+'px', height:(size.y-2)+'px', border:'solid 1px #808080'},
      events:{click:this.helpHide.bindWithEvent(this, true)},
      html:'<div style="height:14px;overflow:hidden;background-color:#808080;color:white;font:10px/13px sans-serif;text-align:right;cursor:default;">Справка <a class="close" href="#" onclick="return false;" style="text-decoration:none;background-color:#404040;color:white;font:bold 7px/8px sans-serif;padding:3px 6px;">X</a></div>'
        +'<div style="height:'+(size.y-2-14-8)+'px;overflow:auto;padding:4px;background-color:#f0f0f0;">'+this.options.helpText+'</div>'});
    this.helpBox.inject(this.target, 'before');
    (function(){
      $(document.body).addEvent('click', this.winClickHandler);
      this.helpBoxShown=true;
    }).delay(100, this);
  },

  helpHide: function(event, focus) {
    if (this.helpBoxShown) {
      this.helpBox.dispose();
      this.helpBox.destroy();
      this.helpBox=null;
      this.helpBoxShown=false;
      if (focus) this.target.focus();
      this.helpJustHidden=true;
      (function(){this.helpJustHidden=false;}).delay(500, this);
    }
  },

  hideOverlay: function(event, focus) {
    this.previewHide(event, focus);
    this.helpHide(event, focus);
  }
});

var ysFCKattached={};

function isFCKattached(ta) {
  return ysFCKattached[$(ta).get('id')];
}

function attachFCK(ta, btn) {
  ta=$(ta);
  if (ysFCKattached[ta.get('id')]) return;
  if (typeof(FCKeditor)!='function') {
    Asset.javascript('/fckeditor/fckeditor.js', {onload:attachFCKcomplete.pass([ta, btn])});
  }
  else {
    attachFCKcomplete(ta, btn);
  }
}

function attachFCKcomplete(ta, btn) {
  ta=$(ta);
  if (typeof(FCKeditor)!='function') {
    alert("Запустить редактор не удалось");
    return;
  }
  if (btn) $(btn).setStyle('display', 'none');
  ysFCKattached[ta.get('id')]=true;
  var oFCKeditor=new FCKeditor(ta.get('id'));
  oFCKeditor.BasePath="/fckeditor/";
  oFCKeditor.Config["CustomConfigurationsPath"]="/t4/user_fckconfig.js?v4";
  oFCKeditor.ToolbarSet='t4';
  oFCKeditor.Height=400;
  oFCKeditor.ReplaceTextarea();
}

function FCKeditor_OnComplete(editorInstance) {
  editorInstance.Focus();
}

var Storage=new Class({
  // Origin: http://forum.mootools.net/viewtopic.php?id=3331
  initialize:function(name){
    this.name=name || "storage";
    if (Browser.Engine.trident) {
      this.storage=new Element("span").setStyle("behavior","url('#default#userData')").inject(document.body);
    } else if (window.localStorage) {
      this.storage=window.localStorage;
    } else if (window.globalStorage) {
      this.storage=window.globalStorage[location.hostname];
    }
  },

  get:function (key) {
    if (Browser.Engine.trident) {
      this.storage.load(this.name);
      //trace('retrieveing '+key+' => '+this.storage.getAttribute(key));
      return JSON.decode(this.storage.getAttribute(key));
    } else if (this.storage) {
      var stored=this.storage.getItem(this.name);
      if (stored) {
        //trace('retrieveing '+key+' => '+JSON.decode(stored.value)[key]);
        return JSON.decode(JSON.decode(stored.value)[key]);
      }
    } else {
      return false;
    }
  },

  set:function (key,value) {
    value=JSON.encode(value);
    if (Browser.Engine.trident) {
      this.storage.setAttribute(key,value);
      this.storage.save(this.name);
      //trace('storing '+key+' => '+value);
    } else if (this.storage) {
      var stored=this.storage.getItem(this.name);
      if (stored) stored=JSON.decode(stored);
      stored=stored || {};
      stored[key]=value;
      this.storage.setItem(this.name,JSON.encode(stored));
      //trace('storing '+key+' => '+value);
    } else {
      return false;
    }
    return true;
  },

  erase:function (key) {
    if (Browser.Engine.trident) {
      this.storage.removeAttribute(key);
      this.storage.save(this.name);
    } else if (this.storage) {
      var stored=this.storage.getItem(this.name);
      if (stored) {
        stored=JSON.decode(stored.value);
        delete stored[key];
        this.storage.setItem(this.name,JSON.encode(stored));
      }
    } else {
      return false;
    }
    return true;
  },

  eraseAll:function () {
    if (Browser.Engine.trident) {
      $A(this.storage.attributes).each(function (attr) {
        this.storage.removeAttribute(attr.nodeName);
      });
      this.storage.save(this.name);
    } else if (this.storage) {
      this.storage.removeItem(this.name);
    } else {
      return false;
    }
    return true;
  }
});
$extend(Storage,{
  load:function (name) {
    return new Storage(name);
  }
});


// HTML-form save & load
Element.implement({
  saveToStorage: function(storageName) {
    var store=new Storage(storageName);
    var formData={}; // hash in the form {param1:['val1','val2',...],...}
    this.getElements('input, select, textarea').each(function(el){
      if (!el.name || el.disabled) return;
      var tagName=el.get('tag');
      var type=tagName=='textarea'? 'text' : (tagName=='select'?'select':el.get('type'));
      if (type=='hidden'||type=='password') type='text';
      var key=el.name+':'+type;
      var v=formData[key] || [];
      if (['select','radio','checkbox'].contains(type)) { // store unique values in a set
        v.combine($splat(el.get('inputValue')));
      }
      else { // store values in a list
        v.push(el.get('inputValue'));
      }
      formData[key]=v;
    });
    return store.set('formData', formData);
  },

  loadFromStorage: function(storageName) {
    var store=new Storage(storageName);
    var formData=store.get('formData');
    if (formData) {
      this.getElements('input, select, textarea').each(function(el){
        if (!el.name || el.disabled) return;
        var tagName=el.get('tag');
        var type=tagName=='textarea'? 'text' : (tagName=='select'?'select':el.get('type'));
        if (type=='hidden'||type=='password') type='text';
        var key=el.name+':'+type;
        var v=formData[key];
        if (v) {
          el.set('inputValue', v);
          if (!['select','radio','checkbox'].contains(type)) {
            v.shift(); // shift the list
          }
        }
      });
    }
  }
});

Element.implement({
  calculateQuerySHA1: function(excludes){
    var query={};
    excludes=$splat(excludes);
    this.getElements('input, select, textarea').each(function(el){
      if (!el.name || el.disabled || el.type == 'submit' || el.type == 'button' || excludes.contains(el.name)) return;
      var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
        return opt.value;
      }) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;
      $splat(value).each(function(val){
        if (val!=null && val!=undefined) {
          if (query.hasOwnProperty(el.name)) // multiple values for the same name
            query[el.name]='MULTIVAL';
          else
            query[el.name]=trim(val);
          //queryKeys.push(el.name);
          //queryValues.push(trim(val));
        }
      });
    });
    var keys='', values='';
    for (var key in query) {
      if (query[key]=='MULTIVAL') continue; // skip multiple-value fields
      keys+='|'+key;
      values+='|'+query[key];
    }
    return sha1win1251(values)+keys;
  }
});

var ysRequest=new Class({
  initialize: function(url, method) {
    if (!method) method='get';
    this.request=new Request({method:method, url:url, link:'cancel', encoding:'windows-1251', onSuccess:this.processSuccess.bind(this), onFailure:this.processFailure.bind(this)});
  },

  send: function(data) {
    this.request.send(data);
  },

  processSuccess: function(responseText, responseXML) {
    this.success(responseText, responseXML);
  },

  processFailure: function(reqXHR) {
    var msgCode=0;
    var msgString='';
    try {
      msgCode=parseInt(reqXHR.responseXML.documentElement.getAttribute('code'));
      msgString=getInnerText(reqXHR.responseXML.documentElement);
    }
    catch (e) {}
    this.failure(reqXHR.status, reqXHR.statusText, msgCode, msgString);
  },

  success: function(responseText, responseXML) {
  },

  failure: function(statusCode, statusText, msgCode, msgString) {
    alert("Передать данные не удалось:\n"+statusText+"\n"+msgString);
  }
});

function formatQuotesHandler(event) {
  if ((event.key=='q' && event.control)||(event.key=='2' && event.control)) { // replace quotes
    var targetTagName=event.target.tagName.toLowerCase();
    if (targetTagName=='input' || targetTagName=='textarea') {
      var target=$(event.target);
      var text=target.getSelectedText();
      if (text) {
        text=text.replace(/^"(\S)/g, '«$1').replace(/(\s)"(\S)/g, '$1«$2').replace(/(\S)"([\s\.,:!\?\/-])/g, '$1»$2').replace(/(\S)"$/g, '$1»');
        target.insertAtCursor(text, true);
      }
      return false;
    }
  }
  return true;
}


var Voter=new Class({
  initialize: function(element) {
    this.rating=0;
    this.votedRating=0;
    this.votedConfirmed=false;
    this.voter=$(element);
    var events={'mouseenter':this.starEnter.bindWithEvent(this), 'click':this.starClick.bindWithEvent(this)};
    this.voter.getChildren('img').each(function(star){
      star.addEvents(events);
      if (star.hasClass('r')) this.rating++;
      star.setStyle('cursor','pointer');
    }, this);
    this.voter.addEvent('mouseleave', this.starsLeave.bindWithEvent(this));
  },

  starEnter: function(event) {
    if (this.votedRating) return;
    var star=$(event.target);
    if (star.hasClass('y')) {
      while (star=star.getNext('img')) {
        ['r','v','y'].each(function(c){this.removeClass(c)}, star);
      }
    }
    else {
      ['r','v'].each(function(c){this.removeClass(c)}, star);
      star.addClass('y');
      while (star=star.getPrevious('img')) {
        ['r','v'].each(function(c){this.removeClass(c)}, star);
        star.addClass('y');
      }
      var star=$(event.target);
      while (star=star.getNext('img')) {
        ['r','v','y'].each(function(c){this.removeClass(c)}, star);
      }
    }
  },

  starsLeave: function(event) {
    if (this.votedRating) return;
    this.restore();
  },

  restore: function() {
    var va=this.voter.getChildren('img');
    var t=this.votedRating || this.rating;
    var c=this.votedRating ? (this.votedConfirmed?'v':'y'):'r';
    for (var i=0; i<va.length; i++) {
      var star=va[i];
      ['r','y','v'].each(function(c){this.removeClass(c)}, star);
      if (i<t) star.addClass(c);
      if (this.votedRating) star.setStyle('cursor','default');
    }
  },

  starClick: function(event) {
    if (this.votedRating) return;
    var star=$(event.target);
    this.votedRating++;
    while (star=star.getPrevious('img')) this.votedRating++;
    this.restore();
    var request=new Request({method:'get', url:'/t4/vote', link:'cancel', encoding:'windows-1251', onComplete:this.votingComplete.bind(this)});
    request.send('item='+this.voter.getAttribute('voteItem')+'&vote='+this.votedRating);
  },

  votingComplete: function() {
    this.votedConfirmed=true;
    this.restore();
  }
});
