Alien-GvaScript

 view release on metacpan or  search on metacpan

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

/*-------------------------------------------------------------------------*
 * GvaScript - Javascript framework born in Geneva.
 *
 *  Authors: Laurent Dami            <laurent.d...@etat.ge.ch>
 *           Mona Remlawi
 *           Jean-Christophe Durand
 *           Sebastien Cuendet

 *  LICENSE
 *  This library is free software, you can redistribute it and/or modify
 *  it under the same terms as Perl's artistic license.
 *
 *--------------------------------------------------------------------------*/

var GvaScript = {
  Version: '1.45',
  REQUIRED_PROTOTYPE: '1.7',
  load: function() {
    function convertVersionString(versionString) {
      var v = versionString.replace(/_.*|\./g, '');
      v = parseInt(v + '0'.times(4-v.length));
      return versionString.indexOf('_') > -1 ? v-1 : v;
    }
    if((typeof Prototype=='undefined') ||
       (typeof Element == 'undefined') ||
       (typeof Element.Methods=='undefined') ||
       (convertVersionString(Prototype.Version) <
        convertVersionString(GvaScript.REQUIRED_PROTOTYPE)))
       throw("GvaScript requires the Prototype JavaScript framework >= " +
        GvaScript.REQUIRED_PROTOTYPE);
  }
};

GvaScript.load();

//----------protoExtensions.js
//-----------------------------------------------------
// Some extensions to the prototype javascript framework
//-----------------------------------------------------

// fire value:change event when setValue method
// is used to change the value of a Form Element
Form.Element.Methods.setValue = Form.Element.Methods.setValue.wrap(
    function($p, element, value) {
        var oldvalue = $F(element);
        var _return = $p(element, value);
        element.fire('value:change', {oldvalue: oldvalue, newvalue: value});
        return _return;
    }
);
Element.addMethods();

// adds the method flash to SPAN, DIV, INPUT, BUTTON elements
// flashes an element by adding a classname for a brief moment of time
// options: {classname: // classname to add (default: flash)
//           duration:  // duration in ms to keep the classname (default: 100ms)}
var _element_list = ['DIV', 'INPUT',
                    'BUTTON', 'TEXTAREA', 'A',
                    'H1', 'H2', 'H3', 'H4', 'H5'];
// for the moment, SPAN not supported on WebKit (see prototype.js bug  in
// https://prototype.lighthouseapp.com/projects/8886/tickets/976-elementaddmethodsspan-fails-on-webkit)
if (!Prototype.Browser.WebKit) _element_list.push('SPAN');
Element.addMethods(_element_list, {
    flash: function(element, options) {
        if (element._IS_FLASHING) return;
        element = $(element);

        options = options || {};
        var duration  = options.duration  || 100;
        var classname = options.classname || 'flash';

        element._IS_FLASHING = true;

        var endFlash  = function() {
            this.removeClassName(classname);
            this._IS_FLASHING = false;
        };

        element.addClassName(classname);
        setTimeout(endFlash.bind(element), duration);
    }
});

// utilities for hash

// expands flat hash into a multi-level deep hash
// javascript version of Perl  CGI::Expand::expand_hash
Hash.expand = function(flat_hash) {
  var tree = {};

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

  // start at elem, walk nav_property until find any of wanted_classes
  navigateDom: function (elem, navigation_property,
                         wanted_classes, stop_condition) {
    while (elem){
       if (stop_condition && stop_condition(elem)) break;
       if (elem.nodeType == 1 &&
           Element.hasAnyClass(elem, wanted_classes))
         return $(elem);
       // else walk to next element
       elem = elem[navigation_property];
     }
     return null;
  },


  autoScroll: function(elem, container, percentage) {
    percentage = percentage || 20; // default
    container  = container  || elem.offsetParent;

    var offset = elem.offsetTop;
    var firstElementChild = container.firstElementChild
                          || $(container).firstDescendant();

    if (firstElementChild) {
      var first_child_offset = firstElementChild.offsetTop;
      if (first_child_offset == container.offsetTop)
        offset -= first_child_offset;
    }

    var min = offset - (container.clientHeight * (100-percentage)/100);
    var max = offset - (container.clientHeight * percentage/100);

    if      (container.scrollTop < min) container.scrollTop = min;
    else if (container.scrollTop > max) container.scrollTop = max;
  },

  outerHTML: function(elem) {
    var tag = elem.tagName;
    if (!tag)
      return elem;           // not an element node
    if (elem.outerHTML)
      return elem.outerHTML; // has builtin implementation
    else {
      var attrs = elem.attributes;
      var str = "<" + tag;
      for (var i = 0; i < attrs.length; i++) {
        var val = attrs[i].value;
        var delim = val.indexOf('"') > -1 ? "'" : '"';
        str += " " + attrs[i].name + "=" + delim + val + delim;
      }
      return str + ">" + elem.innerHTML + "</" + tag + ">";
    }
  }

});

Class.checkOptions = function(defaultOptions, ctorOptions) {
  ctorOptions = ctorOptions || {}; // options passed to the class constructor
  for (var property in ctorOptions) {
    if (defaultOptions[property] === undefined)
      throw new Error("unexpected option: " + property);
  }
  return Object.extend(Object.clone(defaultOptions), ctorOptions);
};


Object.extend(Event, {

  detailedStop: function(event, toStop) {
    if (toStop.preventDefault) {
      if (event.preventDefault) event.preventDefault();
      else                      event.returnValue = false;
    }
    if (toStop.stopPropagation) {
      if (event.stopPropagation) event.stopPropagation();
      else                       event.cancelBubble = true;
    }
  },

  stopAll:  {stopPropagation: true, preventDefault: true},
  stopNone: {stopPropagation: false, preventDefault: false}

});

function ASSERT (cond, msg) {
  if (!cond)
    throw new Error("Violated assertion: " + msg);
}

// detects if a global CSS_PREFIX has been set
// if yes, use it to prefix the css classes
// default to gva
function CSSPREFIX () {
    if(typeof CSS_PREFIX != 'undefined') {
        return (CSS_PREFIX)? CSS_PREFIX : 'gva';
    }
    return 'gva';
}

/**
 *
 * Cross-Browser Split 1.0.1 
 * (c) Steven Levithan <stevenlevithan.com>; MIT License
 * in order to fix a bug with String.prototype.split(RegExp) and Internet Explorer
 * [http://blog.stevenlevithan.com/archives/cross-browser-split]
 * An ECMA-compliant, uniform cross-browser split method
 *
 * */

var cbSplit;

// avoid running twice, which would break `cbSplit._nativeSplit`'s reference to the native `split`
if (!cbSplit) {

cbSplit = function (str, separator, limit) {
    // if `separator` is not a regex, use the native `split`
    if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
        return cbSplit._nativeSplit.call(str, separator, limit);
    }

    var output = [],
        lastLastIndex = 0,
        flags = (separator.ignoreCase ? "i" : "") +
                (separator.multiline  ? "m" : "") +
                (separator.sticky     ? "y" : ""),
        separator = RegExp(separator.source, flags + "g"), // make `global` and avoid `lastIndex` issues by working with a copy
        separator2, match, lastIndex, lastLength;

    str = str + ""; // type conversion
    if (!cbSplit._compliantExecNpcg) {
        separator2 = RegExp("^" + separator.source + "$(?!\\s)", flags); // doesn't need /g or /y, but they don't hurt
    }

    /* behavior for `limit`: if it's...
    - `undefined`: no limit.
    - `NaN` or zero: return an empty array.
    - a positive number: use `Math.floor(limit)`.
    - a negative number: no limit.
    - other: type-convert, then use the above rules. */
    if (limit === undefined || +limit < 0) {
        limit = Infinity;
    } else {
        limit = Math.floor(+limit);
        if (!limit) {
            return [];
        }
    }

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

  var id = 0, head = $$('head')[0];
  Prototype.getJSON = function(url, callback) {
    var script = document.createElement('script'), token = '__jsonp' + id;

    // callback should be a global function
    window[token] = callback;

    // url should have "?2" parameter which is to be replaced with a global callback name
    script.src = url.replace(/\?(&|$)/, '__jsonp' + id + '$1');

    // clean up on load: remove script tag, null script variable and delete global callback function
    script.onload = function() {
      script.remove();
      script = null;
      delete window[token];
    };
    head.appendChild(script);

    // callback name should be unique
    id++;
  }
})();

//----------event.js
// array holding fired events that are pending to be executed
// useful for avoiding accidental double firing of events
// events in queue are unique per eventType&eventTarget
GvaScript.eventsQueue = Class.create();
Object.extend(GvaScript.eventsQueue, {
    _queue: $A([]),
    hasEvent: function(target, name) {
        return (typeof this._queue.find(function(e) {
            return (e.target == target && e.name == name);
        }) == 'object');
    },
    pushEvent: function(target, name) {
        this._queue.push({target: target, name: name});
    },
    popEvent: function(target, name) {
        this._queue = this._queue.reject(function(e) {
            return (e.target == target && e.name == name);
        });
    }
});

// fireEvent : should be COPIED into controller objects, so that
// 'this' is properly bound to the controller

GvaScript.fireEvent = function(/* type, elem1, elem2, ... */) {

  var event;

  switch (typeof arguments[0]) {
  case "string" :
    event = {type: arguments[0]};
    break;
  case "object" :
    event = arguments[0];
    break;
  default:
    throw new Error("invalid first argument to fireEvent()");
  }

  var propName = "on" + event.type;
  var handler;
  var target   = arguments[1]; // first element where the event is triggered
  var currentTarget;           // where the handler is found

  // event already fired and executing
  if(GvaScript.eventsQueue.hasEvent(target, event.type)) return;

  // try to find the handler, first in the HTML elements, then in "this"
  for (var i = 1, len = arguments.length; i < len; i++) {
    var elem = arguments[i];
    if (handler = elem.getAttribute(propName)) {
      currentTarget = elem;
      break;
    }
  }
  if (currentTarget === undefined)
    if (handler = this[propName])
      currentTarget = this;

  if (handler) {
    // build context and copy into event structure
    var controller = this;
    if (!event.target)        event.target        = target;
    if (!event.srcElement)    event.srcElement    = target;
    if (!event.currentTarget) event.currentTarget = currentTarget;
    if (!event.controller)    event.controller    = controller;

    // add the event to the queue, it's about to be fired
    GvaScript.eventsQueue.pushEvent(target, event.type);

    var event_return = null; // return value of event execution
    if (typeof(handler) == "string") {
      // string will be eval-ed in a closure context where 'this', 'event',
      // 'target' and 'controller' are defined.
      var eval_handler = function(){return eval( handler ) };
      handler = eval_handler.call(currentTarget); // target bound to 'this'
    }

    if (handler instanceof Function) {
      // now call the eval-ed or pre-bound handler
      event_return = handler(event);
    }
    else {
      // whatever was returned by the string evaluation
      event_return = handler;
    }

    // event executed, pop from the queue
    // keep a safety margin of 1sec before allowing
    // the same event on the same element to be refired
    // TODO: is 1sec reasonable
    window.setTimeout(function() {
        GvaScript.eventsQueue.popEvent(target, event.type)
    }, 1000);

    return event_return;
  }
  else
    return null; // no handler found
};


//----------keyMap.js

//constructor
GvaScript.KeyMap = function (rules) {
    if (!(rules instanceof Object)) throw "KeyMap: invalid argument";
    this.rules = [rules];
    return this;
};


GvaScript.KeyMap.prototype = {
  destroy: function() {
    Event.stopObserving(this.elem, this.eventType, this.eventHandler);
  },

  eventHandler: function(event) {
    var keymap = this;

    // translate key code into key name
    event.keyName = GvaScript.KeyMap.KEYS.BUILTIN_NAMES[event.keyCode]
                 || String.fromCharCode(event.keyCode);

    // add Control|Shift|Alt modifiers
    event.keyModifiers = "";
    if (event.ctrlKey  && !this.options.ignoreCtrl)  event.keyModifiers += "C_";
    if (event.shiftKey && !this.options.ignoreShift) event.keyModifiers += "S_";
    if (event.altKey   && !this.options.ignoreAlt)   event.keyModifiers += "A_";

    // but cancel all modifiers if main key is Control|Shift|Alt
    if (event.keyName.search(/^(CTRL|SHIFT|ALT)$/) == 0)
      event.keyModifiers = "";

    // try to get the corresponding handler, and call it if found
    var handler = keymap._findInStack(event, keymap.rules);
    if (handler) {
      var toStop = handler.call(keymap, event);
      Event.detailedStop(event, toStop || this.options);
    }
  },

  observe: function(eventType, elem, options) {
    this.eventType = eventType || 'keydown';
    this.elem      = elem      || document;

    // "Shift" modifier usually does not make sense for keypress events
    if (eventType == 'keypress' && !options)
      options = {ignoreShift: true};

    this.options = Class.checkOptions(Event.stopNone, this.options || {});

    this.eventHandler = this.eventHandler.bindAsEventListener(this);
    Event.observe(this.elem, this.eventType, this.eventHandler);
  },

  _findInStack: function(event, stack) {
    for (var i = stack.length - 1; i >= 0; i--) {
      var rules = stack[i];

      // trick to differentiate between C_9 (digit) and C_09 (TAB)
      var keyCode = event.keyCode>9 ? event.keyCode : ("0"+event.keyCode);

      var handler = rules[event.keyModifiers + event.keyName]
                 || rules[event.keyModifiers + keyCode]
                 || this._regex_handler(event, rules.REGEX, true)
                 || this._regex_handler(event, rules.ANTIREGEX, false);

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

      node = this.parentNode(node);
    }
  },

  openAtLevel: function(elem, level) {
    var method = this[(level > 1) ? "open" : "close"];
    var node = this.firstSubNode(elem);
    while (node) {
      method.call(this, node); // open or close
      this.openAtLevel(node, level-1);
      node = this.nextSibling(node);
    }
  },

  loadContent: function (node) {
    var url = node.getAttribute('tn:contenturl');
    // TODO : default URL generator at the tree level

    if (url) {
      var content = this.content(node);
      if (!content) {
        content = document.createElement('div');
        content.className = this.classes.content;
        var content_type = node.getAttribute('content_type');
        if (content_type) content.className += " " + content_type;
        content.innerHTML = "loading " + url;
        node.insertBefore(content, null); // null ==> insert at end of node
      }
      this.fireEvent("BeforeLoadContent", node, this.rootElement);

      var treeNavigator = this; // needed for closure below
      var callback = function() {
        treeNavigator.initSubTree(content);
        treeNavigator.fireEvent("AfterLoadContent", node, this.rootElement);
      };
      new Ajax.Updater(content, url, {onComplete: callback});
      return true;
    }
  },

  select: function (node) {
    var previousNode = this.selectedNode;

    // re-selecting the current node is a no-op
    if (node == previousNode) return;

    // deselect the previously selected node
    if (previousNode) {
        var label = this.label(previousNode);
        if (label) {
          Element.removeClassName(label, this.classes.selected);
        }
    }

    // select the new node
    this.selectedNode = node;
    if (node) {
      this._assertNodeOrLeaf(node, 'select node');
      var label = this.label(node);
      if (!label) {
        throw new Error("selected node has no label");
      }
      else {
        Element.addClassName(label, this.classes.selected);

        if (this.isVisible(label)) {
          // focus has not yet been given to label
          if(! label.hasAttribute('hasFocus'))
            label.focus();
        }
      }
    }

    // cancel if there was any select execution pending
    if (this._selectionTimeoutId) clearTimeout(this._selectionTimeoutId);

    // register code to call the selection handlers after some delay
    var callback = this._selectionTimeoutHandler.bind(this, previousNode);
    this._selectionTimeoutId =
      setTimeout(callback, this.options.selectDelay);
  },

  scrollTo: function(node, with_content) {
    if(!node) return;

    var container = this.options.scrollingContainer;
    if(typeof container == 'string') {
      container = $(container);
    }
    if(!container) return;

    // donot invoke scroll if scrolling is disabled
    // first test if scrolling is enabled on the scrolling container
    if(container.tagName.toLowerCase() == 'html') {
      // on document body
      if(document.body.style.overflow == 'hidden'
        || document.body.style.overflowY == 'hidden'
        || document.body.scroll == 'no') // IE
      return;
    }
    else {
      // on element
      if(container.style.overflow == 'hidden'
        || container.style.overflowY == 'hidden')
      return;
    }

    // test if the node in 'in view'
    _container_y_start = container.scrollTop;
    _container_y_end   = _container_y_start + container.clientHeight;
    _node_y  = Element.cumulativeOffset(node).top + (with_content? node.offsetHeight: 0);

    // calculate padding space between the selected node and
    // the edge of the scrollable container
    _perc = this.options.autoScrollPercentage || 0;
    _padding = container.clientHeight * _perc / 100;

    // calculate delta scroll to affect on scrollingContainer
    _delta = 0;

    // node is beneath scrolling area

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

      this.select  (znode);
      Event.stop(event);
    }
  },

  _endHandler: function (event) {
    if (this.selectedNode) {
      var znode = this.lastVisibleSubnode();
      this.scrollTo(znode);
      this.select  (znode);
      Event.stop(event);
    }
  },

  _ctrlPgUpHandler: function (event) {
    var node = this.enclosingNode(Event.element(event));
    if (node) {
      this.scrollTo(node);
      this.select  (node);
      Event.stop(event);
    }
  },

  _ctrlPgDownHandler: function (event) {
    var node = this.enclosingNode(Event.element(event));
    if (node) {
      node = this.nextDisplayedNode(node);
      if (node) {
        this.scrollTo(node);
        this.select  (node);
        Event.stop(event);
      }
    }
  },

  _chooseLevel: function(event) {
    var level = event.keyCode - "0".charCodeAt(0);
    this.openAtLevel(this.rootElement, level);

    // stop the default Ctrl-num event
    // FF: jump to tab#num
    // IE: Ctrl-5 Select-All
    Event.stop(event);
  },

  _showAll: function(event, toggle) {
    var method = toggle ? Element.addClassName : Element.removeClassName;
    method(this.rootElement, this.classes.showall);
  }

};

//----------choiceList.js

//----------------------------------------------------------------------
// CONSTRUCTOR
//----------------------------------------------------------------------

GvaScript.ChoiceList = function(choices, options) {
  if (! (choices instanceof Array) )
    throw new Error("invalid choices argument : " + choices);
  this.choices = choices;

  var defaultOptions = {
    labelField       : "label",
    classes          : {},        // see below for default classes
    idForChoices     : "CL_choice",
    keymap           : null,
    grabfocus        : false,
    mouseovernavi    : true,
    scrollCount      : 5,
    choiceItemTagName: "div",
    htmlWrapper      : function(html) {return html;},
    paginator        : null
  };


  this.options = Class.checkOptions(defaultOptions, options);

  var defaultClasses = {
    choiceItem      : "CL_choiceItem",
    choiceHighlight : "CL_highlight"
  };
  this.classes = Class.checkOptions(defaultClasses, this.options.classes);

  // handy vars
  this.hasPaginator = this.options.paginator != null;
  this.pageSize = (
                    // the step size of the paginator if any
                    (this.hasPaginator && this.options.paginator.options.step)
                    ||
                    // scroll count
                    this.options.scrollCount
                  );

  // prepare some stuff to be reused when binding to inputElements
  this.reuse = {
    onmouseover : this._listOverHandler.bindAsEventListener(this),
    onclick     : this._clickHandler.bindAsEventListener(this),
    ondblclick  : this._dblclickHandler.bindAsEventListener(this),
    navigationRules: {
      DOWN:      this._highlightDelta.bindAsEventListener(this, 1),
      UP:        this._highlightDelta.bindAsEventListener(this, -1),

      PAGE_DOWN: this._highlightDelta.bindAsEventListener(this, this.pageSize),
      PAGE_UP:   this._highlightDelta.bindAsEventListener(this, -this.pageSize),

      HOME:      this._jumpToIndex.bindAsEventListener(this, 0),
      END:       this._jumpToIndex.bindAsEventListener(this, 99999),

      RETURN:    this._returnHandler .bindAsEventListener(this),
      ESCAPE:    this._escapeHandler .bindAsEventListener(this)
    }
  };

  if(this.hasPaginator) {
    // next/prev page
    this.reuse.navigationRules.RIGHT
        = this._highlightDelta.bindAsEventListener(this, this.pageSize)
    this.reuse.navigationRules.LEFT
        = this._highlightDelta.bindAsEventListener(this, -this.pageSize);

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

    this.choices = list;
    Element.update(this.container, this.htmlForChoices());
    this._highlightChoiceNum(0, true);
  },

  htmlForChoices: function(){ // creates the innerHTML
    var html = "";
    for (var i = 0; i < this.choices.length; i++) {
      var choice = this.choices[i];
      var label  =
        typeof choice == "string" ? choice : choice[this.options.labelField];

      var id = this.container.id ? this.container.id + "." : '';
      id += this.options.idForChoices + "." + i;
      html += this.choiceElementHTML(label, id);
    }
    return this.options.htmlWrapper(html);
  },

  choiceElementHTML: function(label, id) {
    return "<" + this.options.choiceItemTagName + " class='" 
               + this.classes.choiceItem +  "' id='" + id + "'>"
               + label + "</" + this.options.choiceItemTagName + ">";
  },

  fireEvent: GvaScript.fireEvent, // must be copied here for binding "this"


//----------------------------------------------------------------------
// PRIVATE METHODS
//----------------------------------------------------------------------


  //----------------------------------------------------------------------
  // conversion index <=> HTMLElement
  //----------------------------------------------------------------------

  _choiceElem: function(index) { // find DOM element from choice index
    var prefix = this.container.id ? this.container.id + "." : '';
    return $(prefix + this.options.idForChoices + "." + index);
  },

  _choiceIndex: function(elem) {
    return parseInt(elem.id.match(/\.(\d+)$/)[1], 10);
  },


  //----------------------------------------------------------------------
  // highlighting
  //----------------------------------------------------------------------

  _highlightChoiceNum: function(newIndex, autoScroll) {

    // do nothing if newIndex is invalid
    if (newIndex > this.choices.length - 1) return;

    Element.removeClassName(this._choiceElem(this.currentHighlightedIndex),
                            this.classes.choiceHighlight);
    this.currentHighlightedIndex = newIndex;
    var elem = this._choiceElem(newIndex);
    // not to throw an arrow when user is holding an UP/DN keys while
    // paginating
    if(! $(elem)) return;

    Element.addClassName(elem, this.classes.choiceHighlight);

    if (autoScroll)
      Element.autoScroll(elem, this.container, 30); // 30%

    this.fireEvent({type: "Highlight", index: newIndex}, elem, this.container);
  },

  // this method restricts navigation to the current page
  _jumpToIndex: function(event, nextIndex) {
    var autoScroll = event && event.keyName; // autoScroll only for key events

    this._highlightChoiceNum(
        Math.max(0, Math.min(this.choices.length-1, nextIndex)),
        autoScroll
    );

    if (event) Event.stop(event);
  },


  // TODO: jump to page numbers would be a nice addition
  _jumpToPage: function(event, pageIndex) {
    if(pageIndex <=1) return this.options.paginator.getFirstPage();
    if(pageIndex == 99999) return this.options.paginator.getLastPage();

    if (event) Event.stop(event);
  },

  // would navigate through pages if index goes out of bound
  _highlightDelta: function(event, deltax, deltay) {
    var currentIndex = this.currentHighlightedIndex;
    var nextIndex    = currentIndex + deltax;

    // first try to flip a page
    // if first page -> go top of list
    if (nextIndex < 0) {
        if(this.hasPaginator) {
            if(this.options.paginator.getPrevPage()) return;
        }
        nextIndex = 0;
    }

    if (nextIndex >= this.choices.length) {
        if(this.hasPaginator) {
            if(this.options.paginator.getNextPage()) return;
        }
        nextIndex = this.choices.length -1;
    }

    // we're still on the same page
    this._jumpToIndex(event, nextIndex);
  },

  //----------------------------------------------------------------------
  // navigation
  //----------------------------------------------------------------------

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

// CONSTRUCTOR
//----------------------------------------------------------------------

GvaScript.AutoCompleter = function(datasource, options) {

  var defaultOptions = {
    minimumChars     : 1,
    labelField       : "label",
    valueField       : "value",
    autoSuggest      : true,      // will dropDown automatically on keypress
    autoSuggestDelay : 100,       // milliseconds, (OBSOLETE)
    checkNewValDelay : 100,       // milliseconds
    typeAhead        : true,      // will fill the inputElement on highlight
    classes          : {},        // see below for default classes
    maxHeight        : 200,       // pixels
    minWidth         : 200,       // pixels
    offsetX          : 0,         // pixels
    strict           : false,     // will complain on illegal values
    blankOK          : true,      // if strict, will also accept blanks
    colorIllegal     : "red",     // background color when illegal values
    scrollCount      : 5,
    multivalued      : false,
    multivalue_separator :  /[;,\s]\s*/,
    choiceItemTagName: "div",
    htmlWrapper      : function(html) {return html;},
    observed_scroll  : null,      // observe the scroll of a given element and
                                  // move the dropdown accordingly (useful in
                                  // case of scrolling windows)
    additional_params: null,      // additional parameters with optional default
                                  // values (only in the case where the
                                  // datasource is a URL)
    http_method      : 'get',     // method for Ajax requests
    dependentFields  : {},
    deltaTime_tolerance : 50,      // added msec. for imprecisions in setTimeout
    ignorePrefix : false,
    caseSensitive: false

  };

  // more options for array datasources
  if (typeof datasource == "object" && datasource instanceof Array) {
    defaultOptions.ignorePrefix  = false;  // if true, will always display
                                           // the full list
    defaultOptions.caseSensitive = true;
  }

  this.options = Class.checkOptions(defaultOptions, options);

  // autoSuggestDelay cannot be smaller than checkNewValueDelay
  this.options.autoSuggestDelay = Math.max(this.options.autoSuggestDelay,
                                           this.options.checkNewValDelay);

  var defaultClasses = {
    loading         : "AC_loading",
    dropdown        : "AC_dropdown",
    message         : "AC_message"
  };
  this.classes = Class.checkOptions(defaultClasses, this.options.classes);

  if (this.options.multivalued && this.options.strict) {
    throw new Error("options 'strict' and 'multivalue' are incompatible");
  }

  this.dropdownDiv = null;
  // array to store running ajax requests
  // of same autocompleter but for different input element
  this._runningAjax = [];

  this.setdatasource(datasource);

  // prepare an initial keymap; will be registered at first
  // focus() event; then a second set of keymap rules is pushed/popped
  // whenever the choice list is visible
  var basicHandler = this._keyDownHandler.bindAsEventListener(this);
  var detectedKeys = /^(BACKSPACE|DELETE|KP_.*|.)$/;
                   // catch any single char, plus some editing keys
  var basicMap     = { DOWN: this._ArrowDownHandler.bindAsEventListener(this),
                       REGEX: [[null, detectedKeys, basicHandler]] };
  this.keymap = new GvaScript.KeyMap(basicMap);

  // prepare some stuff to be reused when binding to inputElements
  this.reuse = {
    onblur  : this._blurHandler.bindAsEventListener(this),
    onclick : this._clickHandler.bindAsEventListener(this)
  };
}


GvaScript.AutoCompleter.prototype = {

//----------------------------------------------------------------------
// PUBLIC METHODS
//----------------------------------------------------------------------

  // autocomplete : called when the input element gets focus; binds
  // the autocompleter to the input element
  autocomplete: function(elem) {
    elem = $(elem);// in case we got an id instead of an element

    if (!elem) throw new Error("attempt to autocomplete a null element");

    // elem is readonly => no action
    if (elem.getAttribute('readonly') || elem.readOnly) return;

    // if already bound, no more work to do
    if (elem === this.inputElement) return;

    // bind to the element; if first time, also register the event handlers
    this.inputElement = elem;
    if (!elem._autocompleter) {
      elem._autocompleter = this;
      this.keymap.observe("keydown", elem, Event.stopNone);
      Element.observe(elem, "blur", this.reuse.onblur);
      Element.observe(elem, "click", this.reuse.onclick);

      // prevent browser builtin autocomplete behaviour
      elem.writeAttribute("autocomplete", "off");
    }

    // initialize time stamps
    this._timeLastCheck = this._timeLastKeyDown = 0;

    // more initialization, but only if we did not just come back from a
    // click on the dropdownDiv
    if (!this.dropdownDiv) {
      this.lastTypedValue = this.lastValue = "";
      this.choices = null;
      this.fireEvent("Bind", elem);
    }

    this._checkNewValue();
  },

  detach: function(elem) {
    elem._autocompleter = null;
    Element.stopObserving(elem, "blur", this.reuse.onblur);
    Element.stopObserving(elem, "click", this.reuse.onclick);
    Element.stopObserving(elem, "keydown", elem.onkeydown);
  },

  displayMessage : function(message) {
    this._removeDropdownDiv();
    if(_div = this._mkDropdownDiv()) {
      _div.innerHTML = message;
      Element.addClassName(_div, this.classes.message);
    }
  },

  // set additional params for autocompleters that have more than 1 param;
  // second param is the HTTP method (post or get)
  // DALNOTE 10.01.09 : pas de raison de faire le choix de la  méthode HTTP
  // dans  setAdditionalParams()! TOFIX. Apparemment, utilisé une seule fois
  // dans DMWeb (root\src\tab_composition\form.tt2:43)
  setAdditionalParams : function(params, method) {
    this.additional_params = params;
    if (method) this.options.http_method = method;
  },

  addAdditionalParam : function(param, value) {
    if (!this.additional_params)
      this.additional_params = {};
    this.additional_params[param] = value;
  },

  setdatasource : function(datasource) {

    // remember datasource in private property
    this._datasource = datasource;

    // register proper "updateChoices" function according to type of datasource
    var ds_type = typeof datasource;
    this._updateChoicesHandler
      = (ds_type == "string")   ? this._updateChoicesFromAjax
      : (ds_type == "function") ? this._updateChoicesFromCallback
      : (ds_type == "object" && datasource instanceof Array)
                                ? this._updateChoicesFromArray
      : (ds_type == "object" && datasource instanceof Object)
                                ? this._updateChoicesFromJSONP
      : undefined;
     if (!this._updateChoicesHandler)
      throw new Error("unexpected datasource type");
  },

  // 'fireEvent' function is copied from GvaScript.fireEvent, so that "this"
  // in that code gets properly bound to the current object
  fireEvent: GvaScript.fireEvent,

  // Set the element for the AC to look at to adapt its position. If elem is
  // null, stop observing the scroll.
  // DALNOTE 10.01.09 : pas certain de l'utilité de "set_observed_scroll"; si
  // l'élément est positionné correctement dans le DOM par rapport à son parent,
  // il devrait suivre le scroll automatiquement. N'est utilisé dans DMWeb que
  // par "avocat.js".
  set_observed_scroll : function(elem) {
    if (!elem) {
        Event.stopObserving(this.observed_scroll, 'scroll',
                            correct_dropdown_position);
        return;
    }

    this.observed_scroll = elem;
    this.currentScrollTop = elem.scrollTop;
    this.currentScrollLeft = elem.scrollLeft;
    var correct_dropdown_position = function() {
      if (this.dropdownDiv) {
        var dim = Element.getDimensions(this.inputElement);
        var pos = this.dropdownDiv.positionedOffset();
        pos.top  -= this.observed_scroll.scrollTop - this.currentScrollTop;
        pos.left -= this.observed_scroll.scrollLeft;
        this.dropdownDiv.style.top  = pos.top   + "px";
        this.dropdownDiv.style.left = pos.left  + "px";
      }
      this.currentScrollTop = this.observed_scroll.scrollTop;
      this.currentScrollLeft = this.observed_scroll.scrollLeft;
    }

    Event.observe(elem, 'scroll',
                  correct_dropdown_position.bindAsEventListener(this));
  },


//----------------------------------------------------------------------
// PRIVATE METHODS
//----------------------------------------------------------------------

  _updateChoicesFromAjax: function (val_to_complete, continuation) {

    // copies into local variables, needed for closures below (can't rely on
    // 'this' because 'this' may have changed when the ajax call comes back)
    var autocompleter = this;
    var inputElement  = this.inputElement;

    inputElement.style.backgroundColor = ""; // remove colorIllegal

    // abort prev ajax request on this input element
    if (this._runningAjax[inputElement.name])
      this._runningAjax[inputElement.name].transport.abort();

    Element.addClassName(inputElement, this.classes.loading);

    // encode value to complete 

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

       contentType: "text/javascript",
       evalJSON: 'force', // will evaluate even if header != 'application/json'
       onSuccess: function(xhr) {
          // aborted by the onblur handler
          if (xhr.transport.status == 0) return;

          autocompleter._runningAjax[inputElement.name] = null;

          if (xhr.responseJSON) continuation(xhr.responseJSON);

          // autocompleter input already blurred without _blurHandler being
          // called (autocompleter is strict and needs its choices to
          // be able to fire its final status
          if (xhr['blurAfterSuccess']) autocompleter._blurHandler();
       },
       onFailure: function(xhr) {
          autocompleter._runningAjax[inputElement.name] = null;
          autocompleter.displayMessage("pas de réponse du serveur");
       },
       onComplete: function(xhr) {
          Element.removeClassName(inputElement,
                                  autocompleter.classes.loading);
       }
      });
  },

  _updateChoicesFromCallback : function(val_to_complete, continuation) {
     continuation(this._datasource(val_to_complete));
  },

  _updateChoicesFromJSONP : function(val_to_complete, continuation) {
      if(val_to_complete) {
        var _url = this._datasource.json_url.replace(/\?1/, val_to_complete).replace(/\?2/, '?');
        var that = this;

        Element.addClassName(that.inputElement, that.classes.loading);
        Prototype.getJSON(_url, function(data) {
          var _data_list = data;

          if(that._datasource.json_list)
          that._datasource.json_list.split('/').each(function(p) {
            _data_list = _data_list[p];
          });
          Element.removeClassName(that.inputElement, that.classes.loading);

          continuation(_data_list);
        });
      }
  },

  _updateChoicesFromArray : function(val_to_complete, continuation) {
    if (this.options.ignorePrefix) {
      // store the index of the initial value
      if (val_to_complete) {
        this._idx_to_hilite = (val_to_complete == ''? 0 : -1);
        $A(this._datasource).each(function(choice, index) {
          switch(typeof choice) {
            case "object" : value = choice[this.options.valueField]; break;
            case "number" : value = choice.toString(10); break;
            case "string" : value = choice; break;
            default: throw new Error("unexpected type of value");
          }
          if(value.toLowerCase().startsWith(val_to_complete.toLowerCase())) {
            this._idx_to_hilite = index;
            throw $break;
          }
        }, this);
      }
      continuation(this._datasource);
    }
    else {
      var regex = new RegExp("^" + RegExp.escape(val_to_complete),
                             this.options.caseSensitive ? "" : "i");
      var matchPrefix = function(choice) {
        var value;
        switch(typeof choice) {
          case "object" : value = choice[this.options.valueField]; break;
          case "number" : value = choice.toString(10); break;
          case "string" : value = choice; break;
          default: throw new Error("unexpected type of value");
        }
        return value.search(regex) > -1;
      };
      continuation(this._datasource.select(matchPrefix.bind(this)));
    }
  },


  _updateChoices : function (continuation) {
    var value = this._getValueToComplete();

//     if (window.console) console.log('updateChoices', value);

    this._updateChoicesHandler(value, continuation);
  },


  // does the reverse of "autocomplete()"
  // doesnot fire if input blurred from click on choice list
  _blurHandler: function(event) {

    // remove choice list
    if (this.dropdownDiv)  this._removeDropdownDiv();

    // xhr is still active: waiting for response from server
    if (_xhr = this._runningAjax[this.inputElement.name]) {

      // if autocompleter is strict, need to wait for xhr to
      // finish before calling the _blurHandler to fire the
      // autocompleter's finalState
      if (this.options.strict) {
        _xhr['blurAfterSuccess'] = true;
        return;
      }

      _xhr.transport.abort();
      _xhr = null;
      Element.removeClassName(this.inputElement, this.classes.loading);
    }

    // if strict mode, inform client about the final status
    if (this.options.strict) {
      var value = this._getValueToComplete();

      // if value has changed, invalidate previous list of choices
      if (value != this.lastValue) {
        this.choices = null;
      }

      // if blank and blankOK, this is a legal value
      if (!value && this.options.blankOK) {
        this._updateDependentFields(this.inputElement, "");
        this.fireEvent({ type       : "LegalValue",
                         value      : "",
                         choice     : null,
                         controller : null  }, this.inputElement);
      }

      // if choices are known, just inspect status
      else if (this.choices) {

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN


      // repetition block gets an event
      placeholder.fireEvent("Add", insertion_block);

      // deal with nested repeated sections
      this._init_repeat_elements(insertion_block, repeat.path);

      // restore initial path
      repeat.path = path_ini;
    }

    return repeat.count;
  },


  remove: function(repetition_block, live_update) {
    // default behavior to live update all blocks below
    // the removed block
    if(typeof live_update == 'undefined') live_update = true;

    // find element, placeholder and repeat info
    var elem = $(repetition_block);
    elem.id.match(/(.*)\.(\d+)$/);
    var repeat_name = RegExp.$1;
    var remove_ix   = RegExp.$2;
    var placeholder = this._find_placeholder(repeat_name);
    var max         = placeholder.repeat.count;

    // fire onRemove event
    // remove block from DOM
    var block = $(repeat_name + "." + remove_ix);
    placeholder.fireEvent("Remove", block);
    block.remove();

    // if live_update
    if(live_update) {
      // update repeat.count as first block has been deleted
      placeholder.repeat.count -= 1;
      // remove all blocks below
      var _start_ix = remove_ix;
      while(++_start_ix < max) {
        block = $(repeat_name + "." + _start_ix);
        block.remove();
        placeholder.repeat.count -= 1;
      }

      // re-add the blocks above so that they will be renumbered
      var n_add = max - remove_ix - 1;
      if (n_add > 0) this.add(placeholder, n_add);
    }
  },

//-----------------------------------------------------
// Private methods
//-----------------------------------------------------

  _find_placeholder: function(name) {
    if (typeof name == "string" && !name.match(/.placeholder$/))
        name += ".placeholder";
    var placeholder = $(name);
    if (!placeholder) throw new Error("no such element: " + name);
    return placeholder;
  },

  _init_repeat_elements: function(elem, path) {
    elem = $(elem);
    if (elem) {
      var elements = this._find_repeat_elements(elem);
      for (var i = 0; i < elements.length; i++) {
        this._init_repeat_element(elements[i], path);
      }
    }
  },

  _find_repeat_elements: function(elem) {
    var result = [];

    // navigate DOM, do not recurse under "repeat" nodes
    for (var child = elem.firstChild; child; child = child.nextSibling) {
      var has_repeat = child.nodeType == 1 && child.getAttribute('repeat');
      result.push(has_repeat ? child : this._find_repeat_elements(child));
    }
    return result.flatten();
  },

  _init_repeat_element: function(element, path) {
    element = $(element);
    path = path || element.getAttribute('repeat-prefix');

    // number of initial repetition blocks
    var n_blocks = element.getAttribute('repeat-start');
    if (n_blocks == undefined) n_blocks = 1;

    // hash to hold all properties of the repeat element
    var repeat = {};
    repeat.name  = element.getAttribute('repeat');
    repeat.min   = element.getAttribute('repeat-min') || 0;
    repeat.max   = element.getAttribute('repeat-max') || 99;
    repeat.count = 0;
    repeat.path  = (path ? path + "." : "") + repeat.name;

    // create a new element (placeholder for new insertion blocks)
    var placeholder_tag = element.tagName.match(/^(TR|TD|TBODY|THEAD|TH)$/i)
                          ? element.tagName
                          : 'SPAN';
    var placeholder     = document.createElement(placeholder_tag);
    placeholder.id = repeat.path + ".placeholder";
    placeholder.fireEvent = GvaScript.fireEvent;
    element.parentNode.insertBefore(placeholder, element);

    // take this elem out of the DOM and into a string ...
    {
      // a) force the id that will be needed in the template)
      element.id = "#{" + repeat.name + ".path}";

      // b) remove "repeat*" attributes (don't want them in the template)
      var attrs = element.attributes;
      var repeat_attrs = [];
      for (var i = 0; i < attrs.length; i++) {
        var name = attrs[i].name;
        if (name.match(/^repeat/i)) repeat_attrs.push(name);

lib/Alien/GvaScript/lib/GvaScript.js  view on Meta::CPAN

      var old_value = null; // needed for value:change custom event
      var new_value = null;

      switch (elem.type) {

        case "text" :
        case "textarea" :
        case "hidden" :
          old_value  = elem.value;
          elem.value = new_value = val.join(",");
        break;

        case "checkbox" :
        case "radio":
          var elem_val  = elem.value;
          old_value = elem.checked ? elem_val : null;

          // hand-crafted loop through val array (because val.include() is too slow)
          elem.checked = false;
          for (var count = val.length; count--;) {
            if (val[count] == elem_val) {
                elem.checked = true;
                break;
            }
          }
          new_value = elem.checked ? elem_val : null;
        break;

        case "select-one" :
        case "select-multiple" :
          var options = elem.options;
          var old_values = [],
              new_values = [];
          for (var i=0, len=options.length; i<len; i++) {
            var opt = options[i];
            var opt_value = opt.value || opt.text;
            if (opt.selected) old_values.push(opt_value);
            // hand-crafted loop through val array (because val.include() is too slow
            opt.selected = false;
            for (var count = val.length; count--;) {
              if (val[count] == opt_value) {
                new_values.push(opt_value);
                opt.selected = true;
                break;
              }
            }
          }
          old_value = old_values.join(",");
          new_value = new_values.join(",");
        break;

        default:
          // if no element type, might be a node list
          var elem_length = elem.length;
          if (elem_length !== undefined) {
            for (var i=0; i < elem_length; i++) {
              _fill_from_value(form, elem.item(i), val, is_init);
            }
          }
          else
            throw new Error("unexpected elem type : " + elem.type);
        break;
      } // end switch

      // if initializing form
      //   and form has an init handler registered to its inputs
      //   and elem has a new_value set
      // => fire the custom 'value:init' event
      if (is_init) {
        if (form.has_init_registered)
          if (new_value)
            Element.fire(elem, 'value:init', {newvalue: new_value}); 
      }
      else {
        if (new_value != old_value)
          Element.fire(elem, 'value:change', {oldvalue: old_value, newvalue: new_value});
      }
    }

    var _fill_from_array = function (form, field_prefix, array, is_init) {
      for (var i=0, len=array.length; i < len; i++) {
        var new_prefix = field_prefix + "." + i;

        // if form has a corresponding named element, fill it
        var elem = form[new_prefix];
        if (elem) {
          _fill_from_value(form, elem, array[i], is_init);
          continue;
        }

        // otherwise try to walk down to a repetition block

        // try to find an existing repetition block
        elem = doc.getElementById(new_prefix);  // TODO : check: is elem in form ?

        // no repetition block found, try to instanciate one
        if (!elem) {
          var placeholder = doc.getElementById(field_prefix + ".placeholder");
          if (placeholder && placeholder.repeat) {
            GvaScript.Repeat.add(placeholder, i + 1 - placeholder.repeat.count);
            elem = doc.getElementById(new_prefix);
          }
        }
        // recurse to the repetition block

        // mremlawi: sometimes multi-value fields are filled without
        // passing by the repeat moduleearly
        // -> no id's on repeatable blocks are set but need to recurse anyway
//         if (elem)
        GvaScript.Form.fill_from_tree(form, new_prefix, array[i], is_init);
      }
    }

    function fill_from_tree(form, field_prefix, tree, is_init)  {
      if (Object.isString(form)) form = $(form);

      for (var key in tree) {
        if (!tree.hasOwnProperty(key)) continue;

        var val = tree[key];
        var new_prefix = field_prefix ? field_prefix+'.'+key : key;



( run in 0.334 second using v1.01-cache-2.11-cpan-119454b85a5 )