<commit_before>define(function (require) {

var behaviours = require('../../behaviours'),
      shoots     = behaviours.shoots,
      moves      = behaviours.moves,
      rectangle  = behaviours.rectangle;

  function update(params) {
    this.timeUntilNextMovementChange -= params.elapsed;
    if(this.timeUntilNextMovementChange <= 0){
      this.changeMovementDirection();
      this.timeUntilNextMovementChange = 500 * Math.floor(Math.random()*4)+1 ;
    }
  }

  function percentageToShootAgain() {
    return Math.min(1, this.timeSinceLastShot / this.timeRequiredBetweenShots);
  }

  function undoLastMovement() {
    if (this.previousX === undefined || this.previousY === undefined) { return; }
    this.x = this.previousX;
    this.y = this.previousY;
  }

  function vanish () {
    this.disposed = true;
  }

  function changeMovementDirection () {
    var directions = [{velocityX: this.speed, velocityY: 0},
                      {velocityX: -1 * this.speed, velocityY: 0},
                      {velocityX: 0, velocityY: this.speed},
                      {velocityX: 0, velocityY: -1 * this.speed}
                     ];
    var newDirection = Math.floor(Math.random()*4);

    _.extend(this,directions[newDirection]);
  }

  function create(specs) {
    return _.extend(
      rectangle({ width: 15, height: 15 }),
      moves(),
      {
        type: 'wanderer',
        collidesWith: {
          rock: undoLastMovement,
          hole: undoLastMovement,
          boundaries: undoLastMovement,
          shoot: vanish
        }
      },
      shoots({ update: update,
               changeMovementDirection : changeMovementDirection,
               timeUntilNextMovementChange : 0,
               speed : 1}),
      specs || {}
    );
  }
  return create;
});

<commit_msg>wanderer improved to be less in love with screen corners.<commit_after>define(function (require) {

var behaviours = require('../../behaviours'),
      shoots     = behaviours.shoots,
      moves      = behaviours.moves,
      rectangle  = behaviours.rectangle;

  function update(params) {
    this.timeUntilNextMovementChange -= params.elapsed;
    if(this.timeUntilNextMovementChange <= 0){
      this.changeMovementDirection();
      this.timeUntilNextMovementChange = 500 * Math.floor(Math.random()*2)+1 ;
    }
  }

  function percentageToShootAgain() {
    return Math.min(1, this.timeSinceLastShot / this.timeRequiredBetweenShots);
  }

  function undoLastMovement() {
    if (this.previousX === undefined || this.previousY === undefined) { return; }
    this.x = this.previousX;
    this.y = this.previousY;
  }

  function vanish () {
    this.disposed = true;
  }

  function changeMovementDirection () {
    var directions = [{velocityX: this.speed, velocityY: 0},
                      {velocityX: -1 * this.speed, velocityY: 0},
                      {velocityX: 0, velocityY: this.speed},
                      {velocityX: 0, velocityY: -1 * this.speed}
                     ];
    var newDirection = Math.floor(Math.random()*4);

    _.extend(this,directions[newDirection]);
  }

  function create(specs) {
    return _.extend(
      rectangle({ width: 15, height: 15 }),
      moves(),
      {
        type: 'wanderer',
        collidesWith: {
          rock: undoLastMovement,
          hole: undoLastMovement,
          boundaries: undoLastMovement,
          shoot: vanish
        }
      },
      shoots({ update: update,
               changeMovementDirection : changeMovementDirection,
               timeUntilNextMovementChange : 0,
               speed : 1}),
      specs || {}
    );
  }
  return create;
});

<|endoftext|>


<commit_before>'use strict';

/* Controllers */

angular.module('ten20Angular.controllers').
  controller('UserCtrl', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
  // init user and trackers information
  $scope.user = {};
  $scope.trackers = [];

  // get user account info
  $http.get('/user/info').success(function(userinfo) {
    $scope.user = userinfo;
  });
  // get trackers info
  $http.get('/trackers').success(function(data) {
    $scope.trackers = data.items;
    $scope.$broadcast('InitTrackers');
  });

  var delay = 1; // delay seconds for get trip message
  function getMessages() {
    var start = new Date().valueOf();
    $http.get('/message/notify').success(function (tracker) {
      var newTracker = true;
      var oldPos = {};

      // server response time less than One secend
      if ((new Date().valueOf()) - start < 5000) {
        delay *= 5;
      } else {
        // reset delay
        delay = 1;
      }

      console.log(tracker.name + ' udpated');
      // update or add tracker
      for (var i = 0; i < $scope.trackers.length; i++) {
        if ($scope.trackers[i]._id === tracker._id) {
          // add point to recent
          if ($scope.trackers[i].lastMessage.location) {
            $scope.trackers[i].recent = $scope.trackers[i].recent || {msgs:[]};
            $scope.trackers[i].recent.msgs.unshift(
              $scope.trackers[i].lastMessage.location);
            $scope.trackers[i].recent.msgs.splice(
              $scope.trackers[i].recent.msgs.length - 1, 1);

            $scope.trackers[i].path = $scope.trackers[i].recent.msgs;
            $scope.$broadcast('PathUpdate', $scope.trackers[i], false);
          }
          // override with new info
          for (var key in tracker) {
            $scope.trackers[i][key] = tracker[key];
          }

          newTracker = false;
        }
      }

      if (newTracker) {
        $scope.trackers.push(tracker);
      }

      $scope.$broadcast('TrackerUpdate', tracker);
      $timeout(getMessages, delay * 1000);

    }).error(function() {
      delay *= 2;
      $timeout(getMessages, delay * 1000);
    });
  }

  getMessages();

  // refresh tracker
  $scope.refreshTracker = function(t) {
    $scope.activeTracker = t;
    $scope.$broadcast('FocusTracker', t);
    $scope.recentMsg(t);
  };

  // load recent msg of a tracker
  $scope.recentMsg = function(t) {

    if (t.recent && t.recent.msgs.length !== 0) {
      return;
    }

    $http.get('/recent_messages?trackerId=' + t._id).success(function(data) {
      console.log('------recent_message-----');
      t.recent = t.recent || {};
      t.recent.msgs = _filterValidMsg(data.items);
      if (t.recent.msgs.length > 1) {
        t.path = t.recent.msgs;
        $scope.$broadcast('PathUpdate', t, true);
      }
    });
  }

  // load trips of at tracker
  $scope.showTrip = function(t, index) {
      t.path = t.trips.data[index].messages;
      t.trips.active = index;
      $scope.$broadcast('PathUpdate', t, true);
  };

  $scope.loadTrips = function (tracker, init) {
    var BATCH = 3; // load trip counts per time

    if (tracker.trips && tracker.trips.error !== '') {
      return false;
    }

    // prevent load more if user secondly click trip tab
    if (init && tracker.trips &&
        tracker.trips.data.length !==0) {
      $scope.showTrip(tracker, 0);
      return;
    }

    tracker.trips ? null: tracker.trips = { data:[], destCnt: 0, error: '' };
    tracker.trips.destCnt = tracker.trips.data.length + BATCH;
    tracker.trips.loading = true;

    _getOneTrip(tracker);
  };

  // get one trip from server
  function _getOneTrip(tracker) {
    var trips = tracker.trips;
    var before = '';
    var url, filtered;

    if (trips.data.length > 0) {
      before = '&_id=before$' + trips.data[trips.data.length - 1]._id;
    }

    url = '/trips?sortBy=_id$desc&limit=1&trackerId=' + tracker._id + before;
    console.log(url);

    $http.get(url).success(function (documents) {
      if (documents.items.length > 0) {
        filtered = _simplifyTripMsg(documents.items[0]);
        if (filtered.messages.length === 0) {
          tracker.trips.loading = false;
          tracker.trips.error = 'trips data invalid, retry later';
          $timeout(function() { tracker.trips.error = ''; }, 10 * 1000);
          return;
        }
        trips.data.push(filtered);
        // get next trip
        if (trips.data.length < tracker.trips.destCnt) {
          _getOneTrip(tracker);
        } else {
          tracker.trips.loading = false;
        }
        tracker.trips.error = '';
      } else {
        tracker.trips.loading = false;
        tracker.trips.error = 'no more trips, retry later';
        $timeout(function() { tracker.trips.error = ''; }, 10 * 1000);
      }
    }).error(function(data) {
      tracker.trips.error = data;
      tracker.trips.loading = false;
      $timeout(function() { tracker.trips.error = ''; }, 5000);
    });
  };

  function _simplifyTripMsg(trip) {
    // cut useless info in trip messages
    trip.messages = _filterValidMsg(trip.messages);

    return trip;
  }

  function _filterValidMsg(data) {
    var validMsg = [];

    // filter out useless messages
    for (var i = 0; i < data.length; i++) {
      if (data[i].message && data[i].message.location
          && data[i].message.location.latitude) {
        if (!data[i].message.location.timestamp) {
          data[i].message.location.timestamp =
            data[i].receivedTime || data[i].message.receiveTime;
        }
        validMsg.push(data[i].message.location);
      }
    }

    return validMsg;
  }

}]);

<commit_msg>Load recent messages in descending order<commit_after>'use strict';

/* Controllers */

angular.module('ten20Angular.controllers').
  controller('UserCtrl', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
  // init user and trackers information
  $scope.user = {};
  $scope.trackers = [];

  // get user account info
  $http.get('/user/info').success(function(userinfo) {
    $scope.user = userinfo;
  });
  // get trackers info
  $http.get('/trackers').success(function(data) {
    $scope.trackers = data.items;
    $scope.$broadcast('InitTrackers');
  });

  var delay = 1; // delay seconds for get trip message
  function getMessages() {
    var start = new Date().valueOf();
    $http.get('/message/notify').success(function (tracker) {
      var newTracker = true;
      var oldPos = {};

      // server response time less than One secend
      if ((new Date().valueOf()) - start < 5000) {
        delay *= 5;
      } else {
        // reset delay
        delay = 1;
      }

      console.log(tracker.name + ' udpated');
      // update or add tracker
      for (var i = 0; i < $scope.trackers.length; i++) {
        if ($scope.trackers[i]._id === tracker._id) {
          // add point to recent
          if ($scope.trackers[i].lastMessage.location) {
            $scope.trackers[i].recent = $scope.trackers[i].recent || {msgs:[]};
            $scope.trackers[i].recent.msgs.unshift(
              $scope.trackers[i].lastMessage.location);
            $scope.trackers[i].recent.msgs.splice(
              $scope.trackers[i].recent.msgs.length - 1, 1);

            $scope.trackers[i].path = $scope.trackers[i].recent.msgs;
            $scope.$broadcast('PathUpdate', $scope.trackers[i], false);
          }
          // override with new info
          for (var key in tracker) {
            $scope.trackers[i][key] = tracker[key];
          }

          newTracker = false;
        }
      }

      if (newTracker) {
        $scope.trackers.push(tracker);
      }

      $scope.$broadcast('TrackerUpdate', tracker);
      $timeout(getMessages, delay * 1000);

    }).error(function() {
      delay *= 2;
      $timeout(getMessages, delay * 1000);
    });
  }

  getMessages();

  // refresh tracker
  $scope.refreshTracker = function(t) {
    $scope.activeTracker = t;
    $scope.$broadcast('FocusTracker', t);
    $scope.recentMsg(t);
  };

  // load recent msg of a tracker
  $scope.recentMsg = function(t) {

    if (t.recent && t.recent.msgs.length !== 0) {
      return;
    }

    var url = '/recent_messages?sortBy=_id$desc&trackerId=' + t._id;
    console.log('recent message url ' + url);
    $http.get(url).success(function(data) {
      console.log('------recent_message-----');
      t.recent = t.recent || {};
      t.recent.msgs = _filterValidMsg(data.items);
      if (t.recent.msgs.length > 1) {
        t.path = t.recent.msgs;
        $scope.$broadcast('PathUpdate', t, true);
      }
    });
  }

  // load trips of at tracker
  $scope.showTrip = function(t, index) {
      t.path = t.trips.data[index].messages;
      t.trips.active = index;
      $scope.$broadcast('PathUpdate', t, true);
  };

  $scope.loadTrips = function (tracker, init) {
    var BATCH = 3; // load trip counts per time

    if (tracker.trips && tracker.trips.error !== '') {
      return false;
    }

    // prevent load more if user secondly click trip tab
    if (init && tracker.trips &&
        tracker.trips.data.length !==0) {
      $scope.showTrip(tracker, 0);
      return;
    }

    tracker.trips ? null: tracker.trips = { data:[], destCnt: 0, error: '' };
    tracker.trips.destCnt = tracker.trips.data.length + BATCH;
    tracker.trips.loading = true;

    _getOneTrip(tracker);
  };

  // get one trip from server
  function _getOneTrip(tracker) {
    var trips = tracker.trips;
    var before = '';
    var url, filtered;

    if (trips.data.length > 0) {
      before = '&_id=before$' + trips.data[trips.data.length - 1]._id;
    }

    url = '/trips?sortBy=_id$desc&limit=1&trackerId=' + tracker._id + before;
    console.log(url);

    $http.get(url).success(function (documents) {
      if (documents.items.length > 0) {
        filtered = _simplifyTripMsg(documents.items[0]);
        if (filtered.messages.length === 0) {
          tracker.trips.loading = false;
          tracker.trips.error = 'trips data invalid, retry later';
          $timeout(function() { tracker.trips.error = ''; }, 10 * 1000);
          return;
        }
        trips.data.push(filtered);
        // get next trip
        if (trips.data.length < tracker.trips.destCnt) {
          _getOneTrip(tracker);
        } else {
          tracker.trips.loading = false;
        }
        tracker.trips.error = '';
      } else {
        tracker.trips.loading = false;
        tracker.trips.error = 'no more trips, retry later';
        $timeout(function() { tracker.trips.error = ''; }, 10 * 1000);
      }
    }).error(function(data) {
      tracker.trips.error = data;
      tracker.trips.loading = false;
      $timeout(function() { tracker.trips.error = ''; }, 5000);
    });
  };

  function _simplifyTripMsg(trip) {
    // cut useless info in trip messages
    trip.messages = _filterValidMsg(trip.messages);

    return trip;
  }

  function _filterValidMsg(data) {
    var validMsg = [];

    // filter out useless messages
    for (var i = 0; i < data.length; i++) {
      if (data[i].message && data[i].message.location
          && data[i].message.location.latitude) {
        if (!data[i].message.location.timestamp) {
          data[i].message.location.timestamp =
            data[i].receivedTime || data[i].message.receiveTime;
        }
        validMsg.push(data[i].message.location);
      }
    }

    return validMsg;
  }

}]);

<|endoftext|>


<commit_before>import {
    action,
    intercept,
    observable,
    reaction,
    toJS,
    when }               from 'mobx';
import { isEmptyObject } from '_common/utility';
import Validator         from 'Utils/Validator';
import { isProduction }  from '../../config';

/**
 * BaseStore class is the base class for all defined stores in the application. It handles some stuff such as:
 *  1. Creating snapshot object from the store.
 *  2. Saving the store's snapshot in local/session storage and keeping them in sync.
 */
export default class BaseStore {

    /**
     * An enum object to define LOCAL_STORAGE and SESSION_STORAGE
     */
    static STORAGES = Object.freeze({
        LOCAL_STORAGE  : Symbol('LOCAL_STORAGE'),
        SESSION_STORAGE: Symbol('SESSION_STORAGE'),
    });

    @observable
    validation_errors = {};

    @observable
    validation_rules = {};

    switchAccountDisposer = null;
    switch_account_listener = null;

    /**
     * Constructor of the base class that gets properties' name of child which should be saved in storages
     *
     * @param {Object} options - An object that contains the following properties:
     *     @property {Object}   root_store - An object that contains the root store of the app.
     *     @property {String[]} local_storage_properties - A list of properties' names that should be kept in localStorage.
     *     @property {String[]} session_storage_properties - A list of properties' names that should be kept in sessionStorage.
     *     @property {Object}   validation_rules - An object that contains the validation rules for each property of the store.
     *     @property {String}   store_name - Explicit store name for browser application storage (to bypass minification)
     */
    constructor(options = {}) {
        const {
            root_store,
            local_storage_properties,
            session_storage_properties,
            validation_rules,
            store_name,
        } = options;

        Object.defineProperty(this, 'root_store', {
            enumerable: false,
            writable  : true,
        });
        Object.defineProperty(this, 'local_storage_properties', {
            enumerable: false,
            writable  : true,
        });
        Object.defineProperty(this, 'session_storage_properties', {
            enumerable: false,
            writable  : true,
        });
        Object.defineProperty(this, 'store_name', {
            value     : store_name,
            enumerable: false,
            writable  : false,
        });

        this.root_store                 = root_store;
        this.local_storage_properties   = local_storage_properties || [];
        this.session_storage_properties = session_storage_properties || [];
        this.setValidationRules(validation_rules);

        this.setupReactionForLocalStorage();
        this.setupReactionForSessionStorage();
        this.retrieveFromStorage();
    }

    /**
     * Returns an snapshot of the current store
     *
     * @param {String[]} properties - A list of properties' names that should be in the snapshot.
     *
     * @return {Object} Returns a cloned object of the store.
     */
    getSnapshot(properties) {
        let snapshot = toJS(this);

        if (!isEmptyObject(this.root_store)) {
            snapshot.root_store = this.root_store;
        }

        if (properties && properties.length) {
            snapshot = properties.reduce(
                (result, p) => Object.assign(result, { [p]: snapshot[p] }),
                {},
            );
        }

        return snapshot;
    }

    /**
     * Sets up a reaction on properties which are mentioned in `local_storage_properties`
     *  and invokes `saveToStorage` when there are any changes on them.
     *
     */
    setupReactionForLocalStorage() {
        if (this.local_storage_properties.length) {
            reaction(
                () => this.local_storage_properties.map(i => this[i]),
                () => this.saveToStorage(this.local_storage_properties, BaseStore.STORAGES.LOCAL_STORAGE),
            );
        }
    }

    /**
     * Sets up a reaction on properties which are mentioned in `session_storage_properties`
     *  and invokes `saveToStorage` when there are any changes on them.
     *
     */
    setupReactionForSessionStorage() {
        if (this.session_storage_properties.length) {
            reaction(
                () => this.session_storage_properties.map(i => this[i]),
                () => this.saveToStorage(this.session_storage_properties, BaseStore.STORAGES.SESSION_STORAGE),
            );
        }
    }

    /**
     * Removes properties that are not passed from the snapshot of the store and saves it to the passed storage
     *
     * @param {String[]} properties - A list of the store's properties' names which should be saved in the storage.
     * @param {Symbol}   storage    - A symbol object that defines the storage which the snapshot should be stored in it.
     *
     */
    saveToStorage(properties, storage) {
        const snapshot = JSON.stringify(this.getSnapshot(properties), (key, value) => {
            if (value !== null) return value;
            return undefined;
        });

        if (storage === BaseStore.STORAGES.LOCAL_STORAGE) {
            localStorage.setItem(this.store_name ? this.store_name : this.constructor.name, snapshot);
        } else if (storage === BaseStore.STORAGES.SESSION_STORAGE) {
            sessionStorage.setItem(this.store_name ? this.store_name : this.constructor.name, snapshot);
        }
    }

    /**
     * Retrieves saved snapshot of the store and assigns to the current instance.
     *
     */
    @action
    retrieveFromStorage() {
        const local_storage_snapshot   = JSON.parse(localStorage.getItem(this.constructor.name, {}));
        const session_storage_snapshot = JSON.parse(sessionStorage.getItem(this.constructor.name, {}));

        const snapshot = { ...local_storage_snapshot, ...session_storage_snapshot };

        Object.keys(snapshot).forEach((k) => this[k] = snapshot[k]);
    }

    /**
     * Sets validation error messages for an observable property of the store
     *
     * @param {String} propertyName - The observable property's name
     * @param {String} messages - An array of strings that contains validation error messages for the particular property.
     *
     */
    @action
    setValidationErrorMessages(propertyName, messages) {
        this.validation_errors[propertyName] = messages;
    }

    /**
     * Sets validation rules
     *
     * @param {object} rules
     *
     */
    @action
    setValidationRules(rules = {}) {
        Object.keys(rules).forEach(key => {
            this.addRule(key, rules[key]);
        });
    }

    /**
     * Adds rules to the particular property
     *
     * @param {String} property
     * @param {String} rules
     *
     */
    @action
    addRule(property, rules) {
        this.validation_rules[property] = rules;

        intercept(this, property, change => {
            this.validateProperty(property, change.newValue);
            return change;
        });
    }

    /**
     * Validates a particular property of the store
     *
     * @param {String} property - The name of the property in the store
     * @param {object} value    - The value of the property, it can be undefined.
     *
     */
    @action
    validateProperty(property, value) {
        const trigger          = this.validation_rules[property].trigger;
        const inputs           = { [property]: value !== undefined ? value : this[property] };
        const validation_rules = { [property]: (this.validation_rules[property].rules || []) };

        if (!!trigger && Object.hasOwnProperty.call(this, trigger)) {
            inputs[trigger]           = this[trigger];
            validation_rules[trigger] = this.validation_rules[trigger].rules || [];
        }

        const validator = new Validator(
            inputs,
            validation_rules,
            this,
        );

        validator.isPassed();

        Object.keys(inputs).forEach(key => {
            this.setValidationErrorMessages(key, validator.errors.get(key));
        });
    }

    /**
     * Validates all properties which validation rule has been set for.
     *
     */
    @action
    validateAllProperties() {
        this.validation_errors = {};
        Object.keys(this.validation_rules).forEach(p => {
            this.validateProperty(p, this[p]);
        });
    }

    @action.bound
    onSwitchAccount(listener) {
        this.switchAccountDisposer = when(
            () => this.root_store.client.switch_broadcast,
            async () => {
                try {
                    const result = this.switch_account_listener();
                    if (result && result.then && typeof result.then === 'function') {
                        result.then(() => {
                            this.root_store.client.switchEndSignal();
                            this.onSwitchAccount(this.switch_account_listener);
                        });
                    } else {
                        throw new Error('Switching account listeners are required to return a promise.');
                    }
                } catch (error) {
                    // there is no listener currently active. so we can just ignore the error raised from treating
                    // a null object as a function. Although, in development mode, we throw a console error.
                    if (!isProduction()) {
                        console.error(error); // eslint-disable-line
                    }
                }
            },
        );
        this.switch_account_listener = listener;
    }

    @action.bound
    disposeSwitchAccount() {
        if (typeof this.switchAccountDisposer === 'function') {
            this.switchAccountDisposer();
        }
        this.switch_account_listener = null;
    }

    @action.bound
    onUnmount() {
        this.disposeSwitchAccount();
    }
}
<commit_msg>Add store_name to get<commit_after>import {
    action,
    intercept,
    observable,
    reaction,
    toJS,
    when }               from 'mobx';
import { isEmptyObject } from '_common/utility';
import Validator         from 'Utils/Validator';
import { isProduction }  from '../../config';

/**
 * BaseStore class is the base class for all defined stores in the application. It handles some stuff such as:
 *  1. Creating snapshot object from the store.
 *  2. Saving the store's snapshot in local/session storage and keeping them in sync.
 */
export default class BaseStore {

    /**
     * An enum object to define LOCAL_STORAGE and SESSION_STORAGE
     */
    static STORAGES = Object.freeze({
        LOCAL_STORAGE  : Symbol('LOCAL_STORAGE'),
        SESSION_STORAGE: Symbol('SESSION_STORAGE'),
    });

    @observable
    validation_errors = {};

    @observable
    validation_rules = {};

    switchAccountDisposer = null;
    switch_account_listener = null;

    /**
     * Constructor of the base class that gets properties' name of child which should be saved in storages
     *
     * @param {Object} options - An object that contains the following properties:
     *     @property {Object}   root_store - An object that contains the root store of the app.
     *     @property {String[]} local_storage_properties - A list of properties' names that should be kept in localStorage.
     *     @property {String[]} session_storage_properties - A list of properties' names that should be kept in sessionStorage.
     *     @property {Object}   validation_rules - An object that contains the validation rules for each property of the store.
     *     @property {String}   store_name - Explicit store name for browser application storage (to bypass minification)
     */
    constructor(options = {}) {
        const {
            root_store,
            local_storage_properties,
            session_storage_properties,
            validation_rules,
            store_name,
        } = options;

        Object.defineProperty(this, 'root_store', {
            enumerable: false,
            writable  : true,
        });
        Object.defineProperty(this, 'local_storage_properties', {
            enumerable: false,
            writable  : true,
        });
        Object.defineProperty(this, 'session_storage_properties', {
            enumerable: false,
            writable  : true,
        });
        Object.defineProperty(this, 'store_name', {
            value     : store_name,
            enumerable: false,
            writable  : false,
        });

        this.root_store                 = root_store;
        this.local_storage_properties   = local_storage_properties || [];
        this.session_storage_properties = session_storage_properties || [];
        this.setValidationRules(validation_rules);

        this.setupReactionForLocalStorage();
        this.setupReactionForSessionStorage();
        this.retrieveFromStorage();
    }

    /**
     * Returns an snapshot of the current store
     *
     * @param {String[]} properties - A list of properties' names that should be in the snapshot.
     *
     * @return {Object} Returns a cloned object of the store.
     */
    getSnapshot(properties) {
        let snapshot = toJS(this);

        if (!isEmptyObject(this.root_store)) {
            snapshot.root_store = this.root_store;
        }

        if (properties && properties.length) {
            snapshot = properties.reduce(
                (result, p) => Object.assign(result, { [p]: snapshot[p] }),
                {},
            );
        }

        return snapshot;
    }

    /**
     * Sets up a reaction on properties which are mentioned in `local_storage_properties`
     *  and invokes `saveToStorage` when there are any changes on them.
     *
     */
    setupReactionForLocalStorage() {
        if (this.local_storage_properties.length) {
            reaction(
                () => this.local_storage_properties.map(i => this[i]),
                () => this.saveToStorage(this.local_storage_properties, BaseStore.STORAGES.LOCAL_STORAGE),
            );
        }
    }

    /**
     * Sets up a reaction on properties which are mentioned in `session_storage_properties`
     *  and invokes `saveToStorage` when there are any changes on them.
     *
     */
    setupReactionForSessionStorage() {
        if (this.session_storage_properties.length) {
            reaction(
                () => this.session_storage_properties.map(i => this[i]),
                () => this.saveToStorage(this.session_storage_properties, BaseStore.STORAGES.SESSION_STORAGE),
            );
        }
    }

    /**
     * Removes properties that are not passed from the snapshot of the store and saves it to the passed storage
     *
     * @param {String[]} properties - A list of the store's properties' names which should be saved in the storage.
     * @param {Symbol}   storage    - A symbol object that defines the storage which the snapshot should be stored in it.
     *
     */
    saveToStorage(properties, storage) {
        const snapshot = JSON.stringify(this.getSnapshot(properties), (key, value) => {
            if (value !== null) return value;
            return undefined;
        });

        if (storage === BaseStore.STORAGES.LOCAL_STORAGE) {
            localStorage.setItem(this.store_name ? this.store_name : this.constructor.name, snapshot);
        } else if (storage === BaseStore.STORAGES.SESSION_STORAGE) {
            sessionStorage.setItem(this.store_name ? this.store_name : this.constructor.name, snapshot);
        }
    }

    /**
     * Retrieves saved snapshot of the store and assigns to the current instance.
     *
     */
    @action
    retrieveFromStorage() {
        const local_storage_snapshot   = JSON.parse(localStorage.getItem(this.store_name ?
            this.store_name : this.constructor.name, {}));
        const session_storage_snapshot = JSON.parse(sessionStorage.getItem(this.store_name ?
            this.store_name : this.constructor.name, {}));

        const snapshot = { ...local_storage_snapshot, ...session_storage_snapshot };

        Object.keys(snapshot).forEach((k) => this[k] = snapshot[k]);
    }

    /**
     * Sets validation error messages for an observable property of the store
     *
     * @param {String} propertyName - The observable property's name
     * @param {String} messages - An array of strings that contains validation error messages for the particular property.
     *
     */
    @action
    setValidationErrorMessages(propertyName, messages) {
        this.validation_errors[propertyName] = messages;
    }

    /**
     * Sets validation rules
     *
     * @param {object} rules
     *
     */
    @action
    setValidationRules(rules = {}) {
        Object.keys(rules).forEach(key => {
            this.addRule(key, rules[key]);
        });
    }

    /**
     * Adds rules to the particular property
     *
     * @param {String} property
     * @param {String} rules
     *
     */
    @action
    addRule(property, rules) {
        this.validation_rules[property] = rules;

        intercept(this, property, change => {
            this.validateProperty(property, change.newValue);
            return change;
        });
    }

    /**
     * Validates a particular property of the store
     *
     * @param {String} property - The name of the property in the store
     * @param {object} value    - The value of the property, it can be undefined.
     *
     */
    @action
    validateProperty(property, value) {
        const trigger          = this.validation_rules[property].trigger;
        const inputs           = { [property]: value !== undefined ? value : this[property] };
        const validation_rules = { [property]: (this.validation_rules[property].rules || []) };

        if (!!trigger && Object.hasOwnProperty.call(this, trigger)) {
            inputs[trigger]           = this[trigger];
            validation_rules[trigger] = this.validation_rules[trigger].rules || [];
        }

        const validator = new Validator(
            inputs,
            validation_rules,
            this,
        );

        validator.isPassed();

        Object.keys(inputs).forEach(key => {
            this.setValidationErrorMessages(key, validator.errors.get(key));
        });
    }

    /**
     * Validates all properties which validation rule has been set for.
     *
     */
    @action
    validateAllProperties() {
        this.validation_errors = {};
        Object.keys(this.validation_rules).forEach(p => {
            this.validateProperty(p, this[p]);
        });
    }

    @action.bound
    onSwitchAccount(listener) {
        this.switchAccountDisposer = when(
            () => this.root_store.client.switch_broadcast,
            async () => {
                try {
                    const result = this.switch_account_listener();
                    if (result && result.then && typeof result.then === 'function') {
                        result.then(() => {
                            this.root_store.client.switchEndSignal();
                            this.onSwitchAccount(this.switch_account_listener);
                        });
                    } else {
                        throw new Error('Switching account listeners are required to return a promise.');
                    }
                } catch (error) {
                    // there is no listener currently active. so we can just ignore the error raised from treating
                    // a null object as a function. Although, in development mode, we throw a console error.
                    if (!isProduction()) {
                        console.error(error); // eslint-disable-line
                    }
                }
            },
        );
        this.switch_account_listener = listener;
    }

    @action.bound
    disposeSwitchAccount() {
        if (typeof this.switchAccountDisposer === 'function') {
            this.switchAccountDisposer();
        }
        this.switch_account_listener = null;
    }

    @action.bound
    onUnmount() {
        this.disposeSwitchAccount();
    }
}
<|endoftext|>


<commit_before>VISH.Editor.IMSQTI = (function(V,$,undefined){
	var init = function(){
	};

var generatePresentationQuiz = function(imgs,pdfexId){
		var elements = [];
		var imgL = imgs.length;
		for(var i=0; i<imgL; i++){
			elements.push({"body": imgs[i], "type": V.Constant.QUIZ});
		}
		var options = {
			template : "t2",
			pdfexId: pdfexId
		}
		return V.Editor.Presentation.generatePresentationScaffold(elements,options);
	}

 var isCompliantXMLFile = function(fileXML){
		xmlDoc = $.parseXML( fileXML ),
		$xml = $( xmlDoc );

		return !$.isEmptyObject($.find('assessmentItem'));
 }

 var getJSONFromXMLFile = function(fileXML){
 	return null;
 }

 /*
 !$.isEmptyObject($.find('#id'))
 This will return true if the element exists and false if it doesn't.
 */

 /*TO DO
	First we have to check if there's a label called assessmentItem. If there's no one,
	we can assure it's not a QTI XML file.

	Doubt: in case there's a file with assessmentItem, do we have to check if that's the correct format?

 */

	return {
		init 		: init,
		generatePresentationQuiz	: generatePresentationQuiz,
		isCompliantXMLFile			:  isCompliantXMLFile
	};

}) (VISH, jQuery);
<commit_msg>Modified isCompliantXMLFile<commit_after>VISH.Editor.IMSQTI = (function(V,$,undefined){
	var init = function(){
	};

var generatePresentationQuiz = function(imgs,pdfexId){
		var elements = [];
		var imgL = imgs.length;
		for(var i=0; i<imgL; i++){
			elements.push({"body": imgs[i], "type": V.Constant.QUIZ});
		}
		var options = {
			template : "t2",
			pdfexId: pdfexId
		}
		return V.Editor.Presentation.generatePresentationScaffold(elements,options);
	}

 var isCompliantXMLFile = function(fileXML){
		xmlDoc = $.parseXML( fileXML ),
		$xml = $( xmlDoc );

		return !$.isEmptyObject($(xml).find('assessmentItem'));
 }

 var getJSONFromXMLFile = function(fileXML){
 	return null;
 }

 /*
 !$.isEmptyObject($.find('#id'))
 This will return true if the element exists and false if it doesn't.
 */

 /*TO DO
	First we have to check if there's a label called assessmentItem. If there's no one,
	we can assure it's not a QTI XML file.

	Doubt: in case there's a file with assessmentItem, do we have to check if that's the correct format?

 */

	return {
		init 		: init,
		generatePresentationQuiz	: generatePresentationQuiz,
		isCompliantXMLFile			:  isCompliantXMLFile
	};

}) (VISH, jQuery);
<|endoftext|>


<commit_before>ngapp.directive('contextMenu', function($timeout) {
    return {
        restrict: 'E',
        templateUrl: 'directives/contextMenu.html',
        scope: {
            items: '=',
            offset: '=?'
        },
        controller: 'contextMenuController',
        link: function(scope, element) {
            let e = element[0];
            if (scope.offset) {
                e.style.top = '0';
                e.style.left = '0';
                e.style.visibility = 'hidden';
                $timeout(function() {
                    if (scope.offset.top + e.offsetHeight > window.innerHeight) {
                        scope.offset.top = window.innerHeight - e.offsetHeight - 16;
                    }
                    if (scope.offset.left + e.offsetWidth > window.innerWidth) {
                        scope.offset.left = window.innerWidth - e.offsetWidth - 6;
                    }
                    e.style.top = scope.offset.top + 'px';
                    e.style.left = scope.offset.left + 'px';
                    e.style.visibility = 'visible';
                });
            } else {
                // TODO
            }
        }
    }
});

ngapp.controller('contextMenuController', function($scope, $element) {
    $scope.selectItem = function(item) {
        if (item.selected) return;
        item.selected = true;
    };

    $scope.deselectItem = function(e, item) {
        let container = $element[0],
            src = e.srcElement,
            depth = 0;
        while (src !== container) {
            src = src.parentElement;
            depth++;
        }
        if (depth > 4) return;
        item.selected = false;
    };

    $scope.clickItem = function(e, item) {
        e.stopImmediatePropagation();
        item.callback && item.callback();
        $scope.$emit('closeContextMenu')
    };
});<commit_msg>added basic positioning for nested menus<commit_after>ngapp.directive('contextMenu', function($timeout) {
    return {
        restrict: 'E',
        templateUrl: 'directives/contextMenu.html',
        scope: {
            items: '=',
            offset: '=?'
        },
        controller: 'contextMenuController',
        link: function(scope, element) {
            let e = element[0];
            if (scope.offset) {
                e.style.top = '0';
                e.style.left = '0';
                e.style.visibility = 'hidden';
                $timeout(function() {
                    if (scope.offset.top + e.offsetHeight > window.innerHeight) {
                        scope.offset.top = window.innerHeight - e.offsetHeight - 16;
                    }
                    if (scope.offset.left + e.offsetWidth > window.innerWidth) {
                        scope.offset.left = window.innerWidth - e.offsetWidth - 6;
                    }
                    e.style.top = scope.offset.top + 'px';
                    e.style.left = scope.offset.left + 'px';
                    e.style.visibility = 'visible';
                });
            } else {
                e.style.top = '0';
                e.style.left = '0';
                e.style.visibility = 'hidden';
                scope.offset = { top: -2, left: 16 };
                $timeout(function() {
                    // TODO: move up/down as necessary
                    // TODO: move to left side of parent if necessary
                    // TODO: enable scrolling if necessary
                    e.style.top = scope.offset.top + 'px';
                    e.style.left = scope.offset.left + 'px';
                    e.style.visibility = 'visible';
                });
            }
        }
    }
});

ngapp.controller('contextMenuController', function($scope, $element) {
    $scope.selectItem = function(item) {
        if (item.selected) return;
        item.selected = true;
    };

    $scope.deselectItem = function(e, item) {
        let container = $element[0],
            src = e.srcElement,
            depth = 0;
        while (src !== container) {
            src = src.parentElement;
            depth++;
        }
        if (depth > 4) return;
        item.selected = false;
    };

    $scope.clickItem = function(e, item) {
        e.stopImmediatePropagation();
        item.callback && item.callback();
        $scope.$emit('closeContextMenu')
    };
});<|endoftext|>


<commit_before>VISH.Status.Device = (function(V,$,undefined){

	var init = function(callback){
		V.Status.Device.Browser.init();
		V.Status.Device.Features.init();
		_fillDevice(callback);
	};

	var _fillDevice = function(callback){
		//Look device in the cache
		var storedDevice = V.Storage.get(V.Constant.Storage.Device);
		if(typeof storedDevice !== "undefined"){
			device = storedDevice;
			_loadViewportForDevice(device,function(){
				_fillScreen(device); //Update screen
				if(typeof callback === "function"){
					callback(device);
				}
			});
			return;
		}

		//No device in the cache
		//Categorize it
		var device = {};
		device.browser = {};
		device.features = {};

		device.browser = V.Status.Device.Browser.fillBrowser();
		_fillUserAgentBeforeViewport(device);
		_loadViewportForDevice(device,function(){
			//On viewport loaded
			_fillUserAgentAfterViewport(device);
			_fillScreen(device);
			device.features = V.Status.Device.Features.fillFeatures();

			//Store device
			V.Storage.add(VISH.Constant.Storage.Device,device,false);

			//Fix for Android Browsers
			if((device.android)&&(device.browser.name===VISH.Constant.ANDROID_BROWSER)){
				if(device.hasTestingViewport===true){
					//We need to reload the page with the real viewport
					_reloadOnAndroidTestingViewport(callback,device);
					return;
				}
			}

			if(typeof callback === "function"){
				callback(device);
			}
		});
	}

	var _reloadOnAndroidTestingViewport = function(callback,device){
		var attempts = 0;
		var maxAttempts = 3;

		//Ensure that the device has been stored
		var initialDevice = V.Storage.get(V.Constant.Storage.Device);
		if(typeof storedDevice !== "undefined"){
			location.reload(true);
			return;
		}

		//Wait with a timer
		var waitTimer = setInterval(function(){
			var storedDevice = V.Storage.get(V.Constant.Storage.Device);
			if(typeof storedDevice !== "undefined"){
				clearInterval(waitTimer);
				location.reload(true);
			} else {
				attempts++;
				if(attempts>=maxAttempts){
					clearInterval(waitTimer);
					callback(device);
				}
			}
		},1000);
	}

	var _fillUserAgentBeforeViewport = function(device){
		device.pixelRatio = window.devicePixelRatio || 1;

		//Apple devices
		device.iPhone = /iPhone/i.test(navigator.userAgent);
		device.iPhone4 = (device.iPhone && device.pixelRatio == 2);
		device.iPad = /iPad/i.test(navigator.userAgent);
		device.iOS = device.iPhone || device.iPad;
		device.applePhone = device.iPhone || device.iPhone4;
		device.appleTablet = device.iPad;

		//Android devices
		device.android = /android/i.test(navigator.userAgent);
	}

	var _loadViewportForDevice = function(device,callback){
		alert(device.desktop);
		if((device.iOS)&&(device.browser.name===V.Constant.SAFARI)){
			_setViewportForIphone(callback);
		} else if(device.android){
			if(device.browser.name===V.Constant.CHROME){
				_setViewportForChromeForAndroid(callback);
			} else if(device.browser.name===V.Constant.ANDROID_BROWSER){
				/*
				 * Some magic to fix android browser bug
				 * 1. If no device information, load "testing" viewport to get and store it
				 * Then, page will be reloaded with device information stored.
				 * 2. If device information exists, load "real"/"functional" viewport.
				 *
				 * We need to do this, because with "real" viewport we can't access to screen dimensions
				 * in a reliable way in order to differenciate between android smartphones and tablets.
				 * Also, "testing" viewport is not valid because Android browser has a crucial bug related to iframe loading.
				 * Changing the viewport "on fly" not work in all devices. So, we need to reload the page.
				 * Since LocalStorage is supported in all Android Browsers, this is a cross-device solution.
				 * http://caniuse.com/namevalue-storage
				 */

				 //device.desktop===undefined means that isn't a stored device
				if(typeof device.desktop === "undefined"){
					device.hasTestingViewport = true; //Indicate that the viewport is only for testing
					_setTestingViewportForAndroidBrowser(callback);
				} else {
					_setViewportForAndroidBrowser(callback);
				}
			}
		} else {
			if(typeof callback === "function"){
				callback();
			}
		}
	}

	//////////////
	// VIEWPORT
	//////////////

	var WAITING_TIME_FOR_VIEWPORT_LOAD = 1250;

	/*
	 * Take a look at meta viewport browser compatibility
	 * http://www.quirksmode.org/mobile/tableViewport.html#metaviewport
	 *
	 * Totally remove viewport is not supported in Safari or Android browsers
	 * Change its content "on fly" fails in some devices.
	 * So, its preferable to load an initial suitable viewport according to the specific device
	 */
	var _setViewport = function(viewportContent,callback){
		var viewport = $("head>meta[name='viewport']");
		if(viewport.length===0){
			//Insert viewport
			$("head").prepend('<meta name="viewport" content="'+viewportContent+'"/>');
		} else {
			//Change viewport
			$(viewport).attr("content",viewportContent);
		}
		setTimeout(function(){
			if(typeof callback === "function"){
				callback();
			}
		},WAITING_TIME_FOR_VIEWPORT_LOAD);
	}

	var _setViewportForAndroidBrowser = function(callback){
		//We cant specify width=device-width due to a iframe loading bug.
		//It can't be solved
		_setViewport("user-scalable=yes",callback);
	}

	var _setTestingViewportForAndroidBrowser = function(callback){
		_setViewport("width=device-width,height=device-height,user-scalable=yes",callback);
	}

	var _setViewportForChromeForAndroid = function(callback){
		_setViewport("width=device-width,height=device-height,user-scalable=yes",callback);
	}

	var _setViewportForIphone = function(callback){
		_setViewport("user-scalable=yes",callback);
	}

	var _fillUserAgentAfterViewport = function(device){
		//Android devices
		//Differentiate between phones and tablets
		device.androidPhone = false;
		device.androidTablet = false;

		if(device.android){
			// There are phones that don't include "mobile" in the UA string, and tablets that do.
			// However, if UA contains tablet, it's a tablet.

			//First intent: Look for Tablet in userAgent
			if(/tablet/i.test(navigator.userAgent)){
				device.androidTablet = true;
			} else {
				//Second intent: Check screen size

				//We will consider android tablets devices with a screen that are at least 960dp x 720dp (in landscape)
				//Taken from: http://developer.android.com/guide/practices/screens_support.html
				var maxWidth = 960 * device.pixelRatio;
				var maxHeight = 720 * device.pixelRatio;

				// Reliable detection (just for Chrome for Android)
				var landscape = window.screen.availWidth > window.screen.availHeight;
				if(landscape){
					if((window.screen.availWidth>=maxWidth)&&(window.screen.availHeight)>=maxHeight){
						device.androidTablet = true;
					} else {
						device.androidPhone = true;
					}
				} else {
					if((window.screen.availWidth>=maxHeight)&&(window.screen.availHeight)>=maxWidth){
						device.androidTablet = true;
					} else {
						device.androidPhone = true;
					}
				}

			}
		}

		//Phones and Tablets
		device.mobile = device.applePhone || device.androidPhone;
		device.tablet = device.appleTablet || device.androidTablet;

		if((!device.mobile)&&(!device.tablet)){
			device.desktop = true;
		} else {
			device.desktop = false;
		}

		 // Force mobile or tablet
		 // device.desktop = false;
		 // device.mobile = true;
		 // device.tablet = false;

		 // if(device.mobile){
		 // 	alert("mobile");
		 // } else if(device.tablet){
		 // 	alert("tablet");
		 // } else if(device.desktop){
		 // 	alert("desktop");
		 // }
	};

	/*
	 * Must be called after viewport is loaded
	 */
	var _fillScreen = function(device){
		device.viewport = {
			width: window.innerWidth,
			height: window.innerHeight
		};
		device.screen = {
			rWidth: window.screen.availWidth * device.pixelRatio,
			rHeight: window.screen.availHeight * device.pixelRatio,
			width: window.screen.availWidth,
			height: window.screen.availHeight
		};
	}

	return {
		init            : init
	};

}) (VISH, jQuery);
<commit_msg>Remove alert<commit_after>VISH.Status.Device = (function(V,$,undefined){

	var init = function(callback){
		V.Status.Device.Browser.init();
		V.Status.Device.Features.init();
		_fillDevice(callback);
	};

	var _fillDevice = function(callback){
		//Look device in the cache
		var storedDevice = V.Storage.get(V.Constant.Storage.Device);
		if(typeof storedDevice !== "undefined"){
			device = storedDevice;
			_loadViewportForDevice(device,function(){
				_fillScreen(device); //Update screen
				if(typeof callback === "function"){
					callback(device);
				}
			});
			return;
		}

		//No device in the cache
		//Categorize it
		var device = {};
		device.browser = {};
		device.features = {};

		device.browser = V.Status.Device.Browser.fillBrowser();
		_fillUserAgentBeforeViewport(device);
		_loadViewportForDevice(device,function(){
			//On viewport loaded
			_fillUserAgentAfterViewport(device);
			_fillScreen(device);
			device.features = V.Status.Device.Features.fillFeatures();

			//Store device
			V.Storage.add(VISH.Constant.Storage.Device,device,false);

			//Fix for Android Browsers
			if((device.android)&&(device.browser.name===VISH.Constant.ANDROID_BROWSER)){
				if(device.hasTestingViewport===true){
					//We need to reload the page with the real viewport
					_reloadOnAndroidTestingViewport(callback,device);
					return;
				}
			}

			if(typeof callback === "function"){
				callback(device);
			}
		});
	}

	var _reloadOnAndroidTestingViewport = function(callback,device){
		var attempts = 0;
		var maxAttempts = 3;

		//Ensure that the device has been stored
		var initialDevice = V.Storage.get(V.Constant.Storage.Device);
		if(typeof storedDevice !== "undefined"){
			location.reload(true);
			return;
		}

		//Wait with a timer
		var waitTimer = setInterval(function(){
			var storedDevice = V.Storage.get(V.Constant.Storage.Device);
			if(typeof storedDevice !== "undefined"){
				clearInterval(waitTimer);
				location.reload(true);
			} else {
				attempts++;
				if(attempts>=maxAttempts){
					clearInterval(waitTimer);
					callback(device);
				}
			}
		},1000);
	}

	var _fillUserAgentBeforeViewport = function(device){
		device.pixelRatio = window.devicePixelRatio || 1;

		//Apple devices
		device.iPhone = /iPhone/i.test(navigator.userAgent);
		device.iPhone4 = (device.iPhone && device.pixelRatio == 2);
		device.iPad = /iPad/i.test(navigator.userAgent);
		device.iOS = device.iPhone || device.iPad;
		device.applePhone = device.iPhone || device.iPhone4;
		device.appleTablet = device.iPad;

		//Android devices
		device.android = /android/i.test(navigator.userAgent);
	}

	var _loadViewportForDevice = function(device,callback){
		if((device.iOS)&&(device.browser.name===V.Constant.SAFARI)){
			_setViewportForIphone(callback);
		} else if(device.android){
			if(device.browser.name===V.Constant.CHROME){
				_setViewportForChromeForAndroid(callback);
			} else if(device.browser.name===V.Constant.ANDROID_BROWSER){
				/*
				 * Some magic to fix android browser bug
				 * 1. If no device information, load "testing" viewport to get and store it
				 * Then, page will be reloaded with device information stored.
				 * 2. If device information exists, load "real"/"functional" viewport.
				 *
				 * We need to do this, because with "real" viewport we can't access to screen dimensions
				 * in a reliable way in order to differenciate between android smartphones and tablets.
				 * Also, "testing" viewport is not valid because Android browser has a crucial bug related to iframe loading.
				 * Changing the viewport "on fly" not work in all devices. So, we need to reload the page.
				 * Since LocalStorage is supported in all Android Browsers, this is a cross-device solution.
				 * http://caniuse.com/namevalue-storage
				 */

				 //device.desktop===undefined means that isn't a stored device
				if(typeof device.desktop === "undefined"){
					device.hasTestingViewport = true; //Indicate that the viewport is only for testing
					_setTestingViewportForAndroidBrowser(callback);
				} else {
					_setViewportForAndroidBrowser(callback);
				}
			}
		} else {
			if(typeof callback === "function"){
				callback();
			}
		}
	}

	//////////////
	// VIEWPORT
	//////////////

	var WAITING_TIME_FOR_VIEWPORT_LOAD = 1250;

	/*
	 * Take a look at meta viewport browser compatibility
	 * http://www.quirksmode.org/mobile/tableViewport.html#metaviewport
	 *
	 * Totally remove viewport is not supported in Safari or Android browsers
	 * Change its content "on fly" fails in some devices.
	 * So, its preferable to load an initial suitable viewport according to the specific device
	 */
	var _setViewport = function(viewportContent,callback){
		var viewport = $("head>meta[name='viewport']");
		if(viewport.length===0){
			//Insert viewport
			$("head").prepend('<meta name="viewport" content="'+viewportContent+'"/>');
		} else {
			//Change viewport
			$(viewport).attr("content",viewportContent);
		}
		setTimeout(function(){
			if(typeof callback === "function"){
				callback();
			}
		},WAITING_TIME_FOR_VIEWPORT_LOAD);
	}

	var _setViewportForAndroidBrowser = function(callback){
		//We cant specify width=device-width due to a iframe loading bug.
		//It can't be solved
		_setViewport("user-scalable=yes",callback);
	}

	var _setTestingViewportForAndroidBrowser = function(callback){
		_setViewport("width=device-width,height=device-height,user-scalable=yes",callback);
	}

	var _setViewportForChromeForAndroid = function(callback){
		_setViewport("width=device-width,height=device-height,user-scalable=yes",callback);
	}

	var _setViewportForIphone = function(callback){
		_setViewport("user-scalable=yes",callback);
	}

	var _fillUserAgentAfterViewport = function(device){
		//Android devices
		//Differentiate between phones and tablets
		device.androidPhone = false;
		device.androidTablet = false;

		if(device.android){
			// There are phones that don't include "mobile" in the UA string, and tablets that do.
			// However, if UA contains tablet, it's a tablet.

			//First intent: Look for Tablet in userAgent
			if(/tablet/i.test(navigator.userAgent)){
				device.androidTablet = true;
			} else {
				//Second intent: Check screen size

				//We will consider android tablets devices with a screen that are at least 960dp x 720dp (in landscape)
				//Taken from: http://developer.android.com/guide/practices/screens_support.html
				var maxWidth = 960 * device.pixelRatio;
				var maxHeight = 720 * device.pixelRatio;

				// Reliable detection (just for Chrome for Android)
				var landscape = window.screen.availWidth > window.screen.availHeight;
				if(landscape){
					if((window.screen.availWidth>=maxWidth)&&(window.screen.availHeight)>=maxHeight){
						device.androidTablet = true;
					} else {
						device.androidPhone = true;
					}
				} else {
					if((window.screen.availWidth>=maxHeight)&&(window.screen.availHeight)>=maxWidth){
						device.androidTablet = true;
					} else {
						device.androidPhone = true;
					}
				}

			}
		}

		//Phones and Tablets
		device.mobile = device.applePhone || device.androidPhone;
		device.tablet = device.appleTablet || device.androidTablet;

		if((!device.mobile)&&(!device.tablet)){
			device.desktop = true;
		} else {
			device.desktop = false;
		}

		 // Force mobile or tablet
		 // device.desktop = false;
		 // device.mobile = true;
		 // device.tablet = false;

		 // if(device.mobile){
		 // 	alert("mobile");
		 // } else if(device.tablet){
		 // 	alert("tablet");
		 // } else if(device.desktop){
		 // 	alert("desktop");
		 // }
	};

	/*
	 * Must be called after viewport is loaded
	 */
	var _fillScreen = function(device){
		device.viewport = {
			width: window.innerWidth,
			height: window.innerHeight
		};
		device.screen = {
			rWidth: window.screen.availWidth * device.pixelRatio,
			rHeight: window.screen.availHeight * device.pixelRatio,
			width: window.screen.availWidth,
			height: window.screen.availHeight
		};
	}

	return {
		init            : init
	};

}) (VISH, jQuery);
<|endoftext|>


<commit_before>/**
 * Libs
 */
var React = require('react');
var TupiqTools = require('../utils/TupiqTools');
var Analytics = require('../utils/Analytics');

/**
 * Stores
 */
var TupiqStore = require('../stores/TupiqStore');

/**
 * Actions
 */
var TupiqActions = require('../actions/TupiqActions');

/**
 * Pure component
 */
var Tupiq = require('./Tupiq.react');

/**
 * Private
 */
var padding = 10;

function getStateFromStores() {
  return {
    isDragging: TupiqStore.getDragging(),
    isMinimised: TupiqStore.getMinimised(),
    coordinates: TupiqStore.getCoordinates(),
    dragOriginData: TupiqStore.getDragOriginData()
  }
}

/**
 * TupiqContainer
 */
var TupiqContainer = React.createClass({
  getInitialState: function() {
    return getStateFromStores();
  },

  componentDidMount: function() {
    TupiqStore.addChangeListener(this._onChange);

    // Listen to window resize so we can reposition if necessary
    window.addEventListener('resize', this.onWindowResize);

    // Handcrafted DOM resize handler
    // http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/
    var object = document.createElement('object');
    object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
    object.onload = function() {
    	object.contentDocument.defaultView.addEventListener('resize', this.onDomNodeResize.bind(this));
    }.bind(this);
    object.type = 'text/html';
    object.data = 'about:blank';

    var domNode = this.getDOMNode();
    domNode.appendChild(object);

    // This will be fired from the context menu background script.
    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  		if ('minimisePanel' in request) {
				TupiqActions.minimise();

				Analytics.trackEvent('Button', 'Click', 'Minimise Panel');
  		}
  	}.bind(this));
  },

  componentWillUnmount: function() {
    TupiqStore.removeChangeListener(this._onChange);

    window.removeEventListener('resize', this.onWindowResize);
  },

  _onChange: function() {
    this.setState(getStateFromStores());
  },

  onDomNodeResize: function(event) {
  	this.onWindowResize();
  },

  onWindowResize: function(event) {
  	// If we're minimised, or it's the initial x position of 50%
    if (this.state.isMinimised === true) {
    	return;
    }

    TupiqActions.reposition({
      x: this.state.coordinates.x,
      y: this.state.coordinates.y
    });
  },

  onMouseDown: function(event) {
    // Dodgy as.
    if (this.state.isMinimised === true || event.target.className === 'tupiq__header__logo-container' || event.target.className === 'tupiq__header__logo-container__tupiq-logo') {
    	event.preventDefault();

    	return;
    }

    if (event.button === 0) {
      event.stopPropagation();

      this.addEvents();

      var pageOffset = this.getDOMNode().getBoundingClientRect();

      TupiqActions.startDrag({
        scrollOriginX: event.pageX,
        scrollOriginY: event.pageY,
        elementOriginX: pageOffset.left,
        elementOriginY: pageOffset.top
      });

      Analytics.trackEvent('Tupiq', 'Drag');
    }
  },

  onMouseMove: function(event) {
  	var element = this.getDOMNode(),
  			elementWidth = element.offsetWidth,
				elementHeight = element.offsetHeight,
    		elementWidthCenter = elementWidth / 2,
				elementHeightCenter = elementHeight / 2,
				windowWidthCenter = window.innerWidth / 2,
				windowHeightCenter = window.innerHeight / 2,
				deltaX = event.pageX - this.state.dragOriginData.scrollOriginX,
				deltaY = event.pageY - this.state.dragOriginData.scrollOriginY,
				targetX = this.state.dragOriginData.elementOriginX + deltaX + elementWidthCenter,
				targetY = this.state.dragOriginData.elementOriginY + deltaY + elementHeightCenter,
				snapBuffer = 15;

    if (this.state.isDragging) {
      var targetXRel = (targetX - elementWidthCenter - padding) / (window.innerWidth - elementWidth - (padding * 2));
      var targetYRel = (targetY - elementHeightCenter - padding) / (window.innerHeight - elementHeight - (padding * 2));

      targetXRel = Math.max(targetXRel, 0);
      targetXRel = Math.min(targetXRel, 1);

      targetYRel = Math.max(targetYRel, 0);
      targetYRel = Math.min(targetYRel, 1);

      if (TupiqTools.isNumberBetween(targetX, windowWidthCenter - snapBuffer, windowWidthCenter + snapBuffer) && TupiqTools.isNumberBetween(targetY, windowHeightCenter - snapBuffer, windowHeightCenter + snapBuffer)) {
      	targetXRel = .5;
      	targetYRel = .5;
      }

      TupiqActions.reposition({
        x: targetXRel,
        y: targetYRel
      });
    }
  },

  onMouseUp: function(event) {
    this.removeEvents();

    TupiqActions.stopDrag();
  },

  addEvents: function() {
    document.addEventListener('mousemove', this.onMouseMove);
    document.addEventListener('mouseup', this.onMouseUp);
  },

  removeEvents: function() {
    document.removeEventListener('mousemove', this.onMouseMove);
    document.removeEventListener('mouseup', this.onMouseUp);
  },

  render: function(){
    return (
      <Tupiq
      	isMinimised={this.state.isMinimised}
        isDragging={this.state.isDragging}
        coordinates={this.state.coordinates}
        onMouseDown={this.onMouseDown} />
    )
  }
});

module.exports = TupiqContainer;
<commit_msg>comments on window resize<commit_after>/**
 * Libs
 */
var React = require('react');
var TupiqTools = require('../utils/TupiqTools');
var Analytics = require('../utils/Analytics');

/**
 * Stores
 */
var TupiqStore = require('../stores/TupiqStore');

/**
 * Actions
 */
var TupiqActions = require('../actions/TupiqActions');

/**
 * Pure component
 */
var Tupiq = require('./Tupiq.react');

/**
 * Private
 */
var padding = 10;

function getStateFromStores() {
  return {
    isDragging: TupiqStore.getDragging(),
    isMinimised: TupiqStore.getMinimised(),
    coordinates: TupiqStore.getCoordinates(),
    dragOriginData: TupiqStore.getDragOriginData()
  }
}

/**
 * TupiqContainer
 */
var TupiqContainer = React.createClass({
  getInitialState: function() {
    return getStateFromStores();
  },

  componentDidMount: function() {
    TupiqStore.addChangeListener(this._onChange);

    // Listen to window resize so we can reposition if necessary
    window.addEventListener('resize', this.onWindowResize);

    // Handcrafted DOM resize handler
    // http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/
    var object = document.createElement('object');
    object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
    object.onload = function() {
    	object.contentDocument.defaultView.addEventListener('resize', this.onDomNodeResize.bind(this));
    }.bind(this);
    object.type = 'text/html';
    object.data = 'about:blank';

    var domNode = this.getDOMNode();
    domNode.appendChild(object);

    // This will be fired from the context menu background script.
    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  		if ('minimisePanel' in request) {
				TupiqActions.minimise();

				Analytics.trackEvent('Button', 'Click', 'Minimise Panel');
  		}
  	}.bind(this));
  },

  componentWillUnmount: function() {
    TupiqStore.removeChangeListener(this._onChange);

    window.removeEventListener('resize', this.onWindowResize);
  },

  _onChange: function() {
    this.setState(getStateFromStores());
  },

  onDomNodeResize: function(event) {
  	this.onWindowResize();
  },

  onWindowResize: function(event) {
  	// If we're minimised, or it's the initial x position of 50%
    if (this.state.isMinimised === true) {
    	return;
    }

    // We could call forceUpdate here, but nested Tupiq component is "pure"
    // and will ignore a forceUpdate. Better yet set window dimensions
    // into state?
    TupiqActions.reposition({
      x: this.state.coordinates.x,
      y: this.state.coordinates.y
    });
  },

  onMouseDown: function(event) {
    // Dodgy as.
    if (this.state.isMinimised === true || event.target.className === 'tupiq__header__logo-container' || event.target.className === 'tupiq__header__logo-container__tupiq-logo') {
    	event.preventDefault();

    	return;
    }

    if (event.button === 0) {
      event.stopPropagation();

      this.addEvents();

      var pageOffset = this.getDOMNode().getBoundingClientRect();

      TupiqActions.startDrag({
        scrollOriginX: event.pageX,
        scrollOriginY: event.pageY,
        elementOriginX: pageOffset.left,
        elementOriginY: pageOffset.top
      });

      Analytics.trackEvent('Tupiq', 'Drag');
    }
  },

  onMouseMove: function(event) {
  	var element = this.getDOMNode(),
  			elementWidth = element.offsetWidth,
				elementHeight = element.offsetHeight,
    		elementWidthCenter = elementWidth / 2,
				elementHeightCenter = elementHeight / 2,
				windowWidthCenter = window.innerWidth / 2,
				windowHeightCenter = window.innerHeight / 2,
				deltaX = event.pageX - this.state.dragOriginData.scrollOriginX,
				deltaY = event.pageY - this.state.dragOriginData.scrollOriginY,
				targetX = this.state.dragOriginData.elementOriginX + deltaX + elementWidthCenter,
				targetY = this.state.dragOriginData.elementOriginY + deltaY + elementHeightCenter,
				snapBuffer = 15;

    if (this.state.isDragging) {
      var targetXRel = (targetX - elementWidthCenter - padding) / (window.innerWidth - elementWidth - (padding * 2));
      var targetYRel = (targetY - elementHeightCenter - padding) / (window.innerHeight - elementHeight - (padding * 2));

      targetXRel = Math.max(targetXRel, 0);
      targetXRel = Math.min(targetXRel, 1);

      targetYRel = Math.max(targetYRel, 0);
      targetYRel = Math.min(targetYRel, 1);

      if (TupiqTools.isNumberBetween(targetX, windowWidthCenter - snapBuffer, windowWidthCenter + snapBuffer) && TupiqTools.isNumberBetween(targetY, windowHeightCenter - snapBuffer, windowHeightCenter + snapBuffer)) {
      	targetXRel = .5;
      	targetYRel = .5;
      }

      TupiqActions.reposition({
        x: targetXRel,
        y: targetYRel
      });
    }
  },

  onMouseUp: function(event) {
    this.removeEvents();

    TupiqActions.stopDrag();
  },

  addEvents: function() {
    document.addEventListener('mousemove', this.onMouseMove);
    document.addEventListener('mouseup', this.onMouseUp);
  },

  removeEvents: function() {
    document.removeEventListener('mousemove', this.onMouseMove);
    document.removeEventListener('mouseup', this.onMouseUp);
  },

  render: function(){
    return (
      <Tupiq
      	isMinimised={this.state.isMinimised}
        isDragging={this.state.isDragging}
        coordinates={this.state.coordinates}
        onMouseDown={this.onMouseDown} />
    )
  }
});

module.exports = TupiqContainer;
<|endoftext|>


<commit_before>'use strict'

angular.module('github-directives')
    .directive('ghRepositories', function () {
        return {
            restrict: 'E',
            scope: {
                'user': '@',
                'hideOpenIssues': '=',
                'hideForks': '=',
                'limit': '='
            },
            template:
                '
' +
                    '
Repositories: {{ user }}
' +
                    '
' +
                        '
' +
                            '
' +
                                '' +
                                    '{{ repo.name }}' +
                                    '{{ repo.forks }}' +
                                    '{{ repo.open_issues }}' +
                                '' +
                            '
' +
                        '
' +
                    '
' +
                '
',
            controller: ['$scope', '$http', 'apiUrl', function($scope, $http, apiUrl) {
                $scope.repos = [];

                $scope.$watch('user', function () {
                    if ($scope.user == undefined) {
                        return;
                    }
                    $http({method: 'GET', url: apiUrl + '/users/' + $scope.user + '/repos'})
                        .success(function (data) {
                            if ($scope.limit == undefined) {
                                $scope.repos = data;
                            } else {
                                $scope.repos = data.splice(0,$scope.limit);
                            }
                        });
                });
            }]
        };
    });

<commit_msg>Repositories directive renamed and finished. Language added and template with bootstrap optimized.<commit_after>'use strict'

angular.module('github-directives')
    .directive('ghRepositories', function () {
        return {
            restrict: 'E',
            scope: {
                'user': '@',
                'hideOpenIssues': '=',
                'hideForks': '=',
                'hideLanguage': '=',
                'limit': '='
            },
            template:
                '
' +
                    '
Repositories for {{ user }}
' +
                    '
' +
                        '' +
                            '{{ repo.name }}' +
                            '{{ repo.forks }}' +
                            '{{ repo.open_issues }}' +
                            '{{ repo.language }}' +
                        '' +
                    '
' +
                '
',
            controller: ['$scope', '$http', 'apiUrl', function($scope, $http, apiUrl) {
                $scope.repos = [];

                $scope.$watch('user', function () {
                    if ($scope.user == undefined) {
                        return;
                    }
                    $http({method: 'GET', url: apiUrl + '/users/' + $scope.user + '/repos'})
                        .success(function (data) {
                            if ($scope.limit == undefined) {
                                $scope.repos = data;
                            } else {
                                $scope.repos = data.splice(0,$scope.limit);
                            }
                        });
                });
            }]
        };
    });

<|endoftext|>


<commit_before>/*
 * Taglist autocomplete wrapper for the Mine API, Richard Marr
 *
 * Based on jQuery Autocomplete plugin 1.1 by Jörn Zaefferer (c) 2009 Jörn Zaefferer
 * http://docs.jquery.com/Plugins/Autocomplete
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 */

/**
 * TODO:
 * - bug: handle errors on ajax
 *  - like "your are not logged in"
 * - feature: delete tags
 * - feature: let the mine api do the filtering instead of the js
 * - feature: add new tag
 * - feature: show implied tags in tagcloud DONE
 * - feature: hide already selected tags in tagpicker
 *  - should also load implied tags for the existing tags
 */
;(function($) {

	// wrap the generic autocomplete functionality (which can be reused) with tag-list specific code
	// replace a input element with a nice tagpicker, the input element becomes a hidden one, which can just be posted
	var tagLister = new(function() {
		var that = this;
		var input;
		var hiddenInput;
		var ul;
		var tags;
		var tagRetreiver;

		$.extend(this, {
			init : function() {
				// "this" in this context is the element on which the tagLister is working
				var el = this;

				tags = that.extractExistingTags(el);
				that.replaceHTML(el);
				tagRetreiver = new that.TagRetreiver(that);
				that.addAutoCompleteBehaviour(input);
				return hiddenInput;
			},
			replaceHTML : function(el) {
				var attrs = {
					id      : el.attr("id"),
					name    : el.attr("name"),
					value   : tags.join(" "),
					"class" : el.attr("class"),
				}

				var i=1;
				while ($("#taglist"+i).length) {i++}
				var widgetId = "taglist"+i;

				el.replaceWith('
<input type="hidden" id="'+attrs.id+'" name="'+attrs.name+'" value="'+attrs.value+'" />
');
				hiddenInput = $("#"+widgetId+" input[type=hidden]");

				hiddenInput.after('<input type="text" class="'+attrs.class+'"/>');
				hiddenInput.before('

');

				input = $("#"+widgetId+" input[type=text]");
				ul = $("#"+widgetId+" ul");

				for (var i=0; i<tags.length; i++) {
					that.insertTagLi(tags[i]);
				}
			},
			extractExistingTags : function(el) {
				return that.splitTagString(el.attr("value"));
			},
			parseApiResult : function(data) {
				// opening the envelope
				// mapping data and value properties
				var result = [];
				for (var i=0; i<data.result.length; i++) {
					result[i] = data.result[i].tag;
					result[i].data = result[i].tagName;
					result[i].value = that.splitTagString(result[i].tagImplies)
				}
				return result;
			},
			addAutoCompleteBehaviour : function(input) {
				input.autocomplete(
					tagRetreiver.getTags,
					{
						minChars: 1,
						autoFill: false,
						mustMatch: false,
						cacheLength: 0,
						matchContains: true,
						scrollHeight: 220,
						formatItem: function(data, i, total, term) {
							return ""+data.tagName+" "+
								""+data.tagCloud.replace(data.tagName,'')+"";
						},
						parse : this.parseApiResult
					}
				).result(function(event, tag, implied) {
					// this is all kinda hacky; wheels will fall off this wagon
					if ($.inArray(tag, tags)==-1) {
						tags.push(tag);
						hiddenInput.attr("value", tags.join(" "));
						that.insertTagLi(tag, implied);
					}
					input.attr("value", "");
				});
			},
			insertTagLi : function(value, implied) {
				console.log(implied)
				if (implied && implied.length) {
					ul.append('
'+value+' < '+implied.join(' ')+'
')
				} else {
					ul.append('
'+value+'
')
				}
			},
			filter : function() {
				return input.attr("value");
			},
			splitTagString : function(string) {
				return $.grep((string||"").split(/\s+/), function(s){return s!=""});
			},
			TagRetreiver : function(tagLister) {
					var completeResult = null;

					// assuming we're on the same domain, which is true for now
					var baseUrl = document.location.href.match(/[^:]+:\/\/[^\/]+/)[0];

					$.getJSON(
						baseUrl+"/api/tag.json",
						function(data) {
							completeResult = tagLister.parseApiResult(data);
						}
					);

					this.getTags = function() {
						if (!completeResult) {
							return [];
						}
						var filter = tagLister.filter();
						return $.grep(completeResult, function(entry) {
							return entry.tagName.indexOf(filter)==0;
						});
					}
				}
			}
		);
	})();

	$.fn.extend({taglist: tagLister.init});

})(jQuery);<commit_msg>tagwidget: only show direct implied tags in dropdown<commit_after>/*
 * Taglist autocomplete wrapper for the Mine API, Richard Marr
 *
 * Based on jQuery Autocomplete plugin 1.1 by Jörn Zaefferer (c) 2009 Jörn Zaefferer
 * http://docs.jquery.com/Plugins/Autocomplete
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 */

/**
 * TODO:
 * - bug: handle errors on ajax
 *  - like "your are not logged in"
 * - feature: delete tags
 * - feature: let the mine api do the filtering instead of the js
 * - feature: add new tag
 * - feature: show implied tags in tagcloud DONE
 * - feature: hide already selected tags in tagpicker
 *  - should also load implied tags for the existing tags
 */
;(function($) {

	// wrap the generic autocomplete functionality (which can be reused) with tag-list specific code
	// replace a input element with a nice tagpicker, the input element becomes a hidden one, which can just be posted
	var tagLister = new(function() {
		var that = this;
		var input;
		var hiddenInput;
		var ul;
		var tags;
		var tagRetreiver;

		$.extend(this, {
			init : function() {
				// "this" in this context is the element on which the tagLister is working
				var el = this;

				tags = that.extractExistingTags(el);
				that.replaceHTML(el);
				tagRetreiver = new that.TagRetreiver(that);
				that.addAutoCompleteBehaviour(input);
				return hiddenInput;
			},
			replaceHTML : function(el) {
				var attrs = {
					id      : el.attr("id"),
					name    : el.attr("name"),
					value   : tags.join(" "),
					"class" : el.attr("class"),
				}

				var i=1;
				while ($("#taglist"+i).length) {i++}
				var widgetId = "taglist"+i;

				el.replaceWith('
<input type="hidden" id="'+attrs.id+'" name="'+attrs.name+'" value="'+attrs.value+'" />
');
				hiddenInput = $("#"+widgetId+" input[type=hidden]");

				hiddenInput.after('<input type="text" class="'+attrs.class+'"/>');
				hiddenInput.before('

');

				input = $("#"+widgetId+" input[type=text]");
				ul = $("#"+widgetId+" ul");

				for (var i=0; i<tags.length; i++) {
					that.insertTagLi(tags[i]);
				}
			},
			extractExistingTags : function(el) {
				return that.splitTagString(el.attr("value"));
			},
			parseApiResult : function(data) {
				// opening the envelope
				// mapping data and value properties
				var result = [];
				for (var i=0; i<data.result.length; i++) {
					result[i] = data.result[i].tag;
					result[i].data = result[i].tagName;
					result[i].value = that.splitTagString(result[i].tagImplies)
				}
				return result;
			},
			addAutoCompleteBehaviour : function(input) {
				input.autocomplete(
					tagRetreiver.getTags,
					{
						minChars: 1,
						autoFill: false,
						mustMatch: false,
						cacheLength: 0,
						matchContains: true,
						scrollHeight: 220,
						formatItem: function(data, i, total, term) {
							return ""+data.tagName+" "+
								""+(data.tagImplies||"")+"";
						},
						parse : this.parseApiResult
					}
				).result(function(event, tag, implied) {
					// this is all kinda hacky; wheels will fall off this wagon
					if ($.inArray(tag, tags)==-1) {
						tags.push(tag);
						hiddenInput.attr("value", tags.join(" "));
						that.insertTagLi(tag, implied);
					}
					input.attr("value", "");
				});
			},
			insertTagLi : function(value, implied) {
				if (implied && implied.length) {
					ul.append('
'+value+' < '+implied.join(' ')+'
')
				} else {
					ul.append('
'+value+'
')
				}
			},
			filter : function() {
				return input.attr("value");
			},
			splitTagString : function(string) {
				return $.grep((string||"").split(/\s+/), function(s){return s!=""});
			},
			TagRetreiver : function(tagLister) {
					var completeResult = null;

					// assuming we're on the same domain, which is true for now
					var baseUrl = document.location.href.match(/[^:]+:\/\/[^\/]+/)[0];

					$.getJSON(
						baseUrl+"/api/tag.json",
						function(data) {
							completeResult = tagLister.parseApiResult(data);
						}
					);

					this.getTags = function() {
						if (!completeResult) {
							return [];
						}
						var filter = tagLister.filter();
						return $.grep(completeResult, function(entry) {
							return entry.tagName.indexOf(filter)==0;
						});
					}
				}
			}
		);
	})();

	$.fn.extend({taglist: tagLister.init});

})(jQuery);<|endoftext|>