<commit_before>//
// EvDa Events and Data v1.0
// https://github.com/kristopolous/EvDa
//
// Copyright 2011, Chris McKenzie
// Dual licensed under the MIT or GPL Version 2 licenses.
//
function EvDa (imported) {
  var
    BASE = '__base',
    slice = Array.prototype.slice,
    toString = Object.prototype.toString,
    isArray = [].isArray || function(obj) { return toString.call(obj) === '[object Array]' },
    isFunction = function(obj) { return !!(obj && obj.constructor && obj.call && obj.apply) },
    isString = function(obj) { return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)) },
    isNumber = function(obj) { return toString.call(obj) === '[object Number]' },
    isObject = function(obj) {
      if(isString(obj)) {
        return false;
      }

      return obj == null ?
        String( obj ) == 'object' :
        toString.call(obj) === '[object Object]' || true ;
    },

    toArray = function(obj) {
      return slice.call(obj);
    },

    each = [].forEach ?
      function (obj, cb) {
        if (isArray(obj) || obj.length) {
          toArray(obj).forEach(cb);
        } else {
          for( var key in obj ) {
            cb(key, obj[key]);
          }
        }
      } :

      function (obj, cb) {
        if (isArray(obj)) {
          for ( var i = 0, len = obj.length; i < len; i++ ) {
            cb(obj[i], i);
          }
        } else {
          for( var key in obj ) {
            cb(key, obj[key]);
          }
        }
      },

    last = function(obj) {
      return obj.length ? obj[obj.length - 1] : undefined;
    },

    keys = ({}).keys || function (obj) {
      if(isArray(obj)) {
        return obj;
      }
      var ret = [];

      for(var key in obj) {
        ret.push(key);
      }

      return ret;
    },

    without = function(collection, item) {
      var ret = [];
      each(collection, function(which) {
        if(which !== item) {
          ret.push(which);
        }
      });
      return ret;
    },

    uniq = function(obj) {
      var
        old,
        ret = [];

      each(keys(obj).sort(), function(which) {
        if(which != old) {
          old = which;
          ret.push(which);
        }
      });
      return ret;
    },

    select = function(obj, test) {
      var ret = [];
      each(obj, function(which) {
        if(test(which)) { ret.push (which); }
      });
      return ret;
    },

    size = function(obj) {
      return (obj && 'length' in obj) ? obj.length : 0;
    },

    map = [].map ?
      function(array, cb) {
        return array.map(cb)
      } :

      function(array, cb) {
        var ret = [];

        for ( var i = 0, len = obj.length; i < len; i++ ) {
          ret.push(cb(obj[i], i));
        }

        return ret;
      },

    clone = function(obj) {
      if(isArray(obj)) { return slice.call(obj); }
      if(isObject(obj)) { return extend(obj, {}); }
      return obj;
    },

    extend = function(obj) {
      each(slice.call(arguments, 1), function(source) {
        for (var prop in source) {
          if (source[prop] !== void 0) {
            obj[prop] = source[prop];
          }
        }
      });
      return obj;
    },

    // Constants
    ON = 'on',
    AFTER = 'after',

    // The one time callback gets a property to
    // the end of the object to notify our future-selfs
    // that we ought to remove the function.
    ONCE = {once: 1},

    // Internals
    data = imported || {},
    setterMap = {},
    eventMap = {};

  // This is the main invocation function. After
  // you declare an instance and call that instance
  // as a function, this is what gets run.
  function pub ( scope, value, meta ) {
    // If there was one argument, then this is
    // either a getter or the object style
    // invocation.
    if ( arguments.length == 1 ) {

      // The object style invocation will return
      // handles associated with all the keys that
      // went in. There *could* be a mix and match
      // of callbacks and setters, but that would
      // be fine I guess...
      if( isObject(scope) ) {
        var ret = {};

        // Object style should be executed as a transaction
        // to avoid ordinals of the keys making a substantial
        // difference in the existence of the values
        each( scope, function( _key, _value ) {
          ret[_key] = pub ( _key, _value, meta, 0, 1 );
        });

        // After the callbacks has been bypassed, then we
        // run all of them a second time, this time the
        // dependency graphs from the object style transactional
        // invocation should be satisfied
        each( ret, function( _key, _value ) {
          if(isFunction(ret[_key]) && !isFunction(scope[_key])) {
            scope[_key] = ret[_key]();
          }
        });

        return scope;
      } else if (isArray(scope)) {
        return map(scope, function(which) {
          return pub(which, value, meta);
        });
      }

      return data[ scope ];
    }

    // If there were two arguments and if one of them was a function, then
    // this needs to be registered.  Otherwise, we are setting a value.
    return pub [ isFunction ( value ) ? ON : 'set' ].apply(this, arguments);
  }

  // Register callbacks for
  // test, on, and after.
  each ( [ON, AFTER, 'test'], function ( stage ) {

    // register the function
    pub[stage] = function ( key, callback, meta ) {
      if ( !callback ) {
        callback = key;
        key = BASE;
      }

      // This is the back-reference map to this callback
      // so that we can unregister it in the future.
      (callback.$ || (callback.$ = [])).push ( stage + key );

      (eventMap[stage + key] || (eventMap[stage + key] = [])).push ( callback );

      return extend(callback, meta);
    }
  });

  function del ( handle ) {
    each ( handle.$, function( stagekey ) {
      eventMap[ stagekey ] = without( eventMap[ stagekey ], handle );
    });
  }

  function isset ( key, callback ) {
    if( isObject(key) ) {

      each( key, function( _key, _value ) {
        key[_key] = isset( _key, _value );
      });

      return key;
    }

    // If I know how to set this key but
    // I just haven't done it yet, run through
    // those functions now.

    if( setterMap[key] ) {
      setterMap[key]();

      // This is functionally the same as a delete
      // for our purposes.  Also, this should not
      // grow enormous so it's an inexpensive
      // optimization.
      setterMap[key] = 0;
    }

    if ( callback ) {
      return key in data ?
        callback ( data[key] ) :
        pub ( key, callback, ONCE );
    }

    return key in data;
  };

  extend(pub, {
    // Exposing the internal variables so that
    // extensions can be made.
    list: {},
    db: data,
    events: eventMap,
    del: del,
    isset: isset,

    // Unlike much of the reset of the code,
    // setters have single functions.
    setter: function ( key, callback ) {
      setterMap[key] = callback;

      if (eventMap[ON + key]) {
        isset( key );
      }
    },

    incr: function ( key ) {
      // we can't use the same trick here because if we
      // hit 0, it will auto-increment to 1
      return pub.set ( key, isNumber(data[key]) ? (data[key] + 1) : 1 );
    },

    decr: function ( key ) {
      // if key isn't in data, it returns 0 and sets it
      // if key is in data but isn't a number, it returns NaN and sets it
      // if key is 1, then it gets reduced to 0, getting 0,
      // if key is any other number, than it gets set
      return pub.set ( key, data[key] - 1 || 0 );
    },

    // If we are pushing and popping a non-array then
    // it's better that the browser tosses the error
    // to the user than we try to be graceful and silent
    // Therein, we don't try to handle input validation
    // and just try it anyway
    push: function ( key, value ) {
      if (size(arguments) == 1) {
        value = key;
        key = BASE;
      }

      return pub.set ( key, [].concat(data[key] || [], [value]) );
    },

    pop: function ( key ) {
      return pub.set ( key || BASE, data[key].slice(0, -1) );
    },

    group: function ( list ) {
      var
        opts = toArray(arguments),
        list = opts.shift(),
        ret = pub.apply(0, opts);

      ( pub.list[list] || (pub.list[list] = []) );

      if(isFunction(ret)) {
        pub.list[list].push(ret);
      } else {
        each(ret, function(value, key) {
          pub.list[list].push(value);
        });
      }
      return function() {
        return pub.group.apply(0, [list].concat(toArray(arguments)));
      }
    },

    set: function (key, value, _meta, bypass, _noexecute) {
      var
        testKey = 'test' + key,
        times = size(eventMap[ testKey ]),
        failure,

        // Invoke will also get done
        // but it will have no semantic
        // meaning, so it's fine.
        meta = {
          meta: _meta || {},
          old: clone(data[key]),
          key: key,
          done: function ( ok ) {
            failure |= (ok === false);

            if ( ! --times ) {
              if ( ! failure ) {
                pub.set ( key, value, _meta, 1 );
              }
            }
          }
        };

      if (times && !bypass) {
        each ( eventMap[ testKey ], function ( callback ) {
          callback ( value, meta );
        });
      } else {
        // Set the key to the new value.
        // The old value is beind passed in
        // through the meta
        data[key] = value;

        var cback = function(){
          each(
            (eventMap[ON + key] || []).concat
            (eventMap[AFTER + key] || []),
            function(callback) {

              if(!callback.S) {
                callback ( value, meta );

                if ( callback.once ) {
                  del ( callback );
                }
              }
            });
          return value;
        }

        if(!_noexecute) {
          return cback();
        } else {
          return cback;
        }
      }

      return value;
    },

    once: function ( key, lambda ) {
      return pub ( key, lambda, { once: true } );
    },

    enable: function ( listName ) {
      each(pub.list[listName], function(callback) {
        if ( callback.S && callback.S[listName] ) {
          delete callback.S[listName];
        }

        if ( size(callback.S) == 0 ) {
          delete callback.S;
        }
      });
    },

    setadd: function ( key, value ) {
      return pub ( key, uniq(( data[key] || [] ).concat([value])) );
    },

    setdel: function ( key, value ) {
      return pub ( key, without(( data[key] || [] ), value) );
    },

    disable: function ( listName ) {
      each(pub.list[listName], function(callback) {
        ( callback.S || (callback.S = {}) ) [ listName ] = true;
      });
    },

    unset: function () {
      each(arguments, function(which) {
        delete data[which];
      });
    },

    find: function ( regex ) {
      return select( keys(data), function(toTest) {
        return toTest.match(regex);
      });
    },

    added: function(key, callback) {
      if( !callback ) {
        callback = key;
        key = BASE;
      }

      pub.on(key, function(value, meta) {
        var
          newlen = size(value),
          oldlen = size(meta.old);

        if(newlen - oldlen == 1) {
          callback(last(value));
        } else if (newlen > oldlen) {
          callback(toArray(value).slice(oldlen));
        }
      });
    },

    sniff: function () {
      pub.set_ = pub.set;
      var ignoreMap = {};

      pub.set = function() {
        var args = Array.prototype.slice.call(arguments);

        if(!ignoreMap[args[0]]) {
          console.log(+new Date(), args);
        }

        pub.set_.apply (this, args);
      }

      // neuter this function but don't populate
      // the users keyspace.
      pub.sniff = function(key) {
        if(key) {
          ignoreMap[key] = !ignoreMap[key];
          return "[Un]ignoring " + key;
        } else {
          console.log(keys(ignoreMap));
        }
      }
    }
  });

  pub.change = pub.on;
  pub.add = pub.push;

  return pub;
}
<commit_msg>map issues<commit_after>//
// EvDa Events and Data v1.0
// https://github.com/kristopolous/EvDa
//
// Copyright 2011, Chris McKenzie
// Dual licensed under the MIT or GPL Version 2 licenses.
//
function EvDa (imported) {
  var
    BASE = '__base',
    slice = Array.prototype.slice,
    toString = Object.prototype.toString,
    isArray = [].isArray || function(obj) { return toString.call(obj) === '[object Array]' },
    isFunction = function(obj) { return !!(obj && obj.constructor && obj.call && obj.apply) },
    isString = function(obj) { return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)) },
    isNumber = function(obj) { return toString.call(obj) === '[object Number]' },
    isObject = function(obj) {
      if(isString(obj)) {
        return false;
      }

      return obj == null ?
        String( obj ) == 'object' :
        toString.call(obj) === '[object Object]' || true ;
    },

    toArray = function(obj) {
      return slice.call(obj);
    },

    each = [].forEach ?
      function (obj, cb) {
        if (isArray(obj) || obj.length) {
          toArray(obj).forEach(cb);
        } else {
          for( var key in obj ) {
            cb(key, obj[key]);
          }
        }
      } :

      function (obj, cb) {
        if (isArray(obj)) {
          for ( var i = 0, len = obj.length; i < len; i++ ) {
            cb(obj[i], i);
          }
        } else {
          for( var key in obj ) {
            cb(key, obj[key]);
          }
        }
      },

    last = function(obj) {
      return obj.length ? obj[obj.length - 1] : undefined;
    },

    keys = ({}).keys || function (obj) {
      if(isArray(obj)) {
        return obj;
      }
      var ret = [];

      for(var key in obj) {
        ret.push(key);
      }

      return ret;
    },

    without = function(collection, item) {
      var ret = [];
      each(collection, function(which) {
        if(which !== item) {
          ret.push(which);
        }
      });
      return ret;
    },

    uniq = function(obj) {
      var
        old,
        ret = [];

      each(keys(obj).sort(), function(which) {
        if(which != old) {
          old = which;
          ret.push(which);
        }
      });
      return ret;
    },

    select = function(obj, test) {
      var ret = [];
      each(obj, function(which) {
        if(test(which)) { ret.push (which); }
      });
      return ret;
    },

    size = function(obj) {
      return (obj && 'length' in obj) ? obj.length : 0;
    },

    map = [].map ?
      function(array, cb) {
        return array.map(cb)
      } :

      function(array, cb) {
        var ret = [];

        for ( var i = 0, len = array.length; i < len; i++ ) {
          ret.push(cb(array[i], i));
        }

        return ret;
      },

    clone = function(obj) {
      if(isArray(obj)) { return slice.call(obj); }
      if(isObject(obj)) { return extend(obj, {}); }
      return obj;
    },

    extend = function(obj) {
      each(slice.call(arguments, 1), function(source) {
        for (var prop in source) {
          if (source[prop] !== void 0) {
            obj[prop] = source[prop];
          }
        }
      });
      return obj;
    },

    // Constants
    ON = 'on',
    AFTER = 'after',

    // The one time callback gets a property to
    // the end of the object to notify our future-selfs
    // that we ought to remove the function.
    ONCE = {once: 1},

    // Internals
    data = imported || {},
    setterMap = {},
    eventMap = {};

  // This is the main invocation function. After
  // you declare an instance and call that instance
  // as a function, this is what gets run.
  function pub ( scope, value, meta ) {
    // If there was one argument, then this is
    // either a getter or the object style
    // invocation.
    if ( arguments.length == 1 ) {

      // The object style invocation will return
      // handles associated with all the keys that
      // went in. There *could* be a mix and match
      // of callbacks and setters, but that would
      // be fine I guess...
      if( isObject(scope) ) {
        var ret = {};

        // Object style should be executed as a transaction
        // to avoid ordinals of the keys making a substantial
        // difference in the existence of the values
        each( scope, function( _key, _value ) {
          ret[_key] = pub ( _key, _value, meta, 0, 1 );
        });

        // After the callbacks has been bypassed, then we
        // run all of them a second time, this time the
        // dependency graphs from the object style transactional
        // invocation should be satisfied
        each( ret, function( _key, _value ) {
          if(isFunction(ret[_key]) && !isFunction(scope[_key])) {
            scope[_key] = ret[_key]();
          }
        });

        return scope;
      } else if (isArray(scope)) {
        return map(scope, function(which) {
          return pub(which, value, meta);
        });
      }

      return data[ scope ];
    }

    // If there were two arguments and if one of them was a function, then
    // this needs to be registered.  Otherwise, we are setting a value.
    return pub [ isFunction ( value ) ? ON : 'set' ].apply(this, arguments);
  }

  // Register callbacks for
  // test, on, and after.
  each ( [ON, AFTER, 'test'], function ( stage ) {

    // register the function
    pub[stage] = function ( key, callback, meta ) {
      if ( !callback ) {
        callback = key;
        key = BASE;
      }

      // This is the back-reference map to this callback
      // so that we can unregister it in the future.
      (callback.$ || (callback.$ = [])).push ( stage + key );

      (eventMap[stage + key] || (eventMap[stage + key] = [])).push ( callback );

      return extend(callback, meta);
    }
  });

  function del ( handle ) {
    each ( handle.$, function( stagekey ) {
      eventMap[ stagekey ] = without( eventMap[ stagekey ], handle );
    });
  }

  function isset ( key, callback ) {
    if( isObject(key) ) {

      each( key, function( _key, _value ) {
        key[_key] = isset( _key, _value );
      });

      return key;
    }

    // If I know how to set this key but
    // I just haven't done it yet, run through
    // those functions now.

    if( setterMap[key] ) {
      setterMap[key]();

      // This is functionally the same as a delete
      // for our purposes.  Also, this should not
      // grow enormous so it's an inexpensive
      // optimization.
      setterMap[key] = 0;
    }

    if ( callback ) {
      return key in data ?
        callback ( data[key] ) :
        pub ( key, callback, ONCE );
    }

    return key in data;
  };

  extend(pub, {
    // Exposing the internal variables so that
    // extensions can be made.
    list: {},
    db: data,
    events: eventMap,
    del: del,
    isset: isset,

    // Unlike much of the reset of the code,
    // setters have single functions.
    setter: function ( key, callback ) {
      setterMap[key] = callback;

      if (eventMap[ON + key]) {
        isset( key );
      }
    },

    incr: function ( key ) {
      // we can't use the same trick here because if we
      // hit 0, it will auto-increment to 1
      return pub.set ( key, isNumber(data[key]) ? (data[key] + 1) : 1 );
    },

    decr: function ( key ) {
      // if key isn't in data, it returns 0 and sets it
      // if key is in data but isn't a number, it returns NaN and sets it
      // if key is 1, then it gets reduced to 0, getting 0,
      // if key is any other number, than it gets set
      return pub.set ( key, data[key] - 1 || 0 );
    },

    // If we are pushing and popping a non-array then
    // it's better that the browser tosses the error
    // to the user than we try to be graceful and silent
    // Therein, we don't try to handle input validation
    // and just try it anyway
    push: function ( key, value ) {
      if (size(arguments) == 1) {
        value = key;
        key = BASE;
      }

      return pub.set ( key, [].concat(data[key] || [], [value]) );
    },

    pop: function ( key ) {
      return pub.set ( key || BASE, data[key].slice(0, -1) );
    },

    group: function ( list ) {
      var
        opts = toArray(arguments),
        list = opts.shift(),
        ret = pub.apply(0, opts);

      ( pub.list[list] || (pub.list[list] = []) );

      if(isFunction(ret)) {
        pub.list[list].push(ret);
      } else {
        each(ret, function(value, key) {
          pub.list[list].push(value);
        });
      }
      return function() {
        return pub.group.apply(0, [list].concat(toArray(arguments)));
      }
    },

    set: function (key, value, _meta, bypass, _noexecute) {
      var
        testKey = 'test' + key,
        times = size(eventMap[ testKey ]),
        failure,

        // Invoke will also get done
        // but it will have no semantic
        // meaning, so it's fine.
        meta = {
          meta: _meta || {},
          old: clone(data[key]),
          key: key,
          done: function ( ok ) {
            failure |= (ok === false);

            if ( ! --times ) {
              if ( ! failure ) {
                pub.set ( key, value, _meta, 1 );
              }
            }
          }
        };

      if (times && !bypass) {
        each ( eventMap[ testKey ], function ( callback ) {
          callback ( value, meta );
        });
      } else {
        // Set the key to the new value.
        // The old value is beind passed in
        // through the meta
        data[key] = value;

        var cback = function(){
          each(
            (eventMap[ON + key] || []).concat
            (eventMap[AFTER + key] || []),
            function(callback) {

              if(!callback.S) {
                callback ( value, meta );

                if ( callback.once ) {
                  del ( callback );
                }
              }
            });
          return value;
        }

        if(!_noexecute) {
          return cback();
        } else {
          return cback;
        }
      }

      return value;
    },

    once: function ( key, lambda ) {
      return pub ( key, lambda, { once: true } );
    },

    enable: function ( listName ) {
      each(pub.list[listName], function(callback) {
        if ( callback.S && callback.S[listName] ) {
          delete callback.S[listName];
        }

        if ( size(callback.S) == 0 ) {
          delete callback.S;
        }
      });
    },

    setadd: function ( key, value ) {
      return pub ( key, uniq(( data[key] || [] ).concat([value])) );
    },

    setdel: function ( key, value ) {
      return pub ( key, without(( data[key] || [] ), value) );
    },

    disable: function ( listName ) {
      each(pub.list[listName], function(callback) {
        ( callback.S || (callback.S = {}) ) [ listName ] = true;
      });
    },

    unset: function () {
      each(arguments, function(which) {
        delete data[which];
      });
    },

    find: function ( regex ) {
      return select( keys(data), function(toTest) {
        return toTest.match(regex);
      });
    },

    added: function(key, callback) {
      if( !callback ) {
        callback = key;
        key = BASE;
      }

      pub.on(key, function(value, meta) {
        var
          newlen = size(value),
          oldlen = size(meta.old);

        if(newlen - oldlen == 1) {
          callback(last(value));
        } else if (newlen > oldlen) {
          callback(toArray(value).slice(oldlen));
        }
      });
    },

    sniff: function () {
      pub.set_ = pub.set;
      var ignoreMap = {};

      pub.set = function() {
        var args = Array.prototype.slice.call(arguments);

        if(!ignoreMap[args[0]]) {
          console.log(+new Date(), args);
        }

        pub.set_.apply (this, args);
      }

      // neuter this function but don't populate
      // the users keyspace.
      pub.sniff = function(key) {
        if(key) {
          ignoreMap[key] = !ignoreMap[key];
          return "[Un]ignoring " + key;
        } else {
          console.log(keys(ignoreMap));
        }
      }
    }
  });

  pub.change = pub.on;
  pub.add = pub.push;

  return pub;
}
<|endoftext|>


<commit_before>import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react'
import logger from 'electron-timber'
import { makeStyles } from '@material-ui/core'
import { remote } from 'electron'
import { useIntl, defineMessages } from 'react-intl'
import path from 'path'

import api from '../../new-api'
import Searching from './Searching'
import SyncAppBar from './SyncAppBar'
import SyncTarget from './SyncTarget'
import SyncGrid from './SyncGrid'

export const peerStatus = {
  READY: 'ready',
  PROGRESS: 'progress',
  ERROR: 'error',
  COMPLETE: 'complete'
}

const m = defineMessages({
  openSyncFileDialog: 'Select a database to syncronize',
  createSyncFileDialog: 'Create a new database to syncronize'
})

const fileDialogFilters = [
  {
    name: 'Mapeo Data (*.mapeodata)',
    extensions: ['mapeodata', 'mapeo-jungle', 'sync', 'zip']
  }
]

const SyncView = ({ focusState }) => {
  const cx = useStyles()
  const listenForSyncPeers = focusState === 'focused'
  const [peers, syncPeer] = usePeers(listenForSyncPeers)
  const { formatMessage: t } = useIntl()
  logger.log('render peers', peers)

  const handleClickSelectSyncfile = () => {
    remote.dialog.showOpenDialog(
      {
        title: t(m.openSyncFileDialog),
        properties: ['openFile'],
        filters: fileDialogFilters
      },
      filenames => {
        if (typeof filenames === 'undefined' || filenames.length !== 1) return
        syncPeer(filenames[0], { file: true })
      }
    )
  }

  const handleClickNewSyncfile = () => {
    remote.dialog.showSaveDialog(
      {
        title: t(m.openSyncFileDialog),
        defaultPath: 'database.mapeodata',
        filters: fileDialogFilters
      },
      filename => {
        if (!filename) return
        syncPeer(filename, { file: true })
      }
    )
  }

  return (

      <SyncAppBar
        onClickSelectSyncfile={handleClickSelectSyncfile}
        onClickNewSyncfile={handleClickNewSyncfile}
      />
      {peers.length === 0 ? (
        <Searching />
      ) : (
        <SyncGrid>
          {peers.map(peer => (
            <SyncTarget
              key={peer.id}
              {...peer}
              onClick={() => syncPeer(peer.id)}
            />
          ))}
        </SyncGrid>
      )}

  )
}

export default SyncView

function usePeers (listen) {
  const lastClosed = useRef(Date.now())
  const [serverPeers, setServerPeers] = useState([])
  const [syncErrors, setSyncErrors] = useState(new Map())
  const [syncRequests, setSyncRequests] = useState(new Map())

  // Keep a ref of the last time this view was closed (used to maintain peer
  // "completed" state in the UI)
  useEffect(
    () => {
      if (!listen) lastClosed.current = Date.now()
    },
    [listen]
  )

  useEffect(
    () => {
      // Only start listening if `listen` is true
      if (!listen) return

      const updatePeers = (updatedServerPeers = []) => {
        logger.log('Received peer update', updatedServerPeers)
        setServerPeers(updatedServerPeers)
        // NB: use callback version of setState because the new error state
        // depends on the previous error state
        setSyncErrors(syncErrors => {
          const newErrors = new Map(syncErrors)
          updatedServerPeers.forEach(peer => {
            if (peer.state && peer.state.topic === 'replication-error') {
              newErrors.set(peer.id, peer.state.message)
            }
          })
          return newErrors
        })
        // Argh, this is hacky. This is making up for us not being able to rely
        // on server state for rendering the UI
        setSyncRequests(syncRequests => {
          const newSyncRequests = new Map(syncRequests)
          updatedServerPeers.forEach(peer => {
            if (!peer.state) return
            if (
              peer.state.topic === 'replication-error' ||
              peer.state.topic === 'replication-complete'
            ) {
              newSyncRequests.delete(peer.id)
            }
          })
          return newSyncRequests
        })
      }

      // Whenever the sync view becomes focused, announce for sync and start
      // listening for updates to peer status
      api.syncJoin()
      const peerListener = api.addPeerListener(updatePeers)

      // When the listen changes or component unmounts, cleanup listeners
      return () => {
        api.syncLeave()
        if (peerListener) peerListener.remove()
      }
    },
    [listen]
  )

  const peers = useMemo(
    () =>
      getPeersStatus({
        syncRequests,
        serverPeers,
        syncErrors,
        since: lastClosed.current
      }),
    [serverPeers, syncErrors, syncRequests]
  )

  const syncPeer = useCallback(
    (peerId, opts) => {
      logger.log('Request sync start', peerId, serverPeers)
      if (opts && opts.file) return api.syncStart({ filename: peerId })
      const peer = serverPeers.find(peer => peer.id === peerId)
      // Peer could have vanished in the moment the button was pressed
      if (peer) {
        // The server does always respond immediately with progress, especially
        // if the two devices are already up to sync. We store the request state
        // so the user can see the UI update when they click the button
        setSyncRequests(syncRequests => {
          const newSyncRequests = new Map(syncRequests)
          newSyncRequests.set(peerId, true)
          return newSyncRequests
        })
        api.syncStart(peer)
      }
    },
    [serverPeers]
  )

  return [peers, syncPeer]
}

/**
 * The peer status from Mapeo Core does not 'remember' the completion of a sync.
 * If the user is not looking at the screen when sync completes, they might miss
 * it. This function derives a peer status from the server state and any errors
 */
function getPeersStatus ({ serverPeers = [], syncErrors, syncRequests, since }) {
  return serverPeers.map(serverPeer => {
    let status = peerStatus.READY
    let complete
    const state = serverPeer.state || {}
    if (
      state.topic === 'replication-progress' ||
      state.topic === 'replication-started' ||
      syncRequests.has(serverPeer.id)
    ) {
      status = peerStatus.PROGRESS
    } else if (
      syncErrors.has(serverPeer.id) ||
      state.topic === 'replication-error'
    ) {
      status = peerStatus.ERROR
    } else if (
      (state.lastCompletedDate || 0) > since ||
      state.topic === 'replication-complete'
    ) {
      status = peerStatus.COMPLETE
      complete = state.message
    }
    return {
      id: serverPeer.id,
      name: serverPeer.filename
        ? path.basename(serverPeer.name)
        : serverPeer.name,
      status: status,
      lastCompleted: complete || state.lastCompletedDate,
      error: syncErrors.get(serverPeer.id),
      progress: getPeerProgress(serverPeer.state),
      deviceType: serverPeer.filename ? 'file' : serverPeer.deviceType
    }
  })
}

// We combine media and database items in progress. In order to show roughtly
// accurate progress, this weighting is how much more progress a media item
// counts vs. a database item
const MEDIA_WEIGHTING = 50
function getPeerProgress (peerState) {
  if (
    !peerState ||
    peerState.topic !== 'replication-progress' ||
    !peerState.message ||
    !peerState.message.db ||
    !peerState.message.media
  ) {
    return
  }
  const total =
    (peerState.message.db.total || 0) +
    (peerState.message.media.total || 0) * MEDIA_WEIGHTING
  const sofar =
    (peerState.message.db.sofar || 0) +
    (peerState.message.media.sofar || 0) * MEDIA_WEIGHTING
  const progress = total > 0 ? sofar / total : 0
  // Round progress to 2-decimal places. PeerItem is memoized, so it will not
  // update if progress does not change. Without rounding PeerItem updates
  // unnecessarily on every progress change, when we are only showing the user a
  // rounded percentage progress. Increase this to 3-decimal places if you want
  // to show more detail to the user.
  return {
    percent: Math.round(progress * 100) / 100,
    mediaSofar: peerState.message.media.sofar || 0,
    mediaTotal: peerState.message.media.total || 0,
    dbSofar: peerState.message.db.sofar || 0,
    dbTotal: peerState.message.db.total || 0
  }
}

const useStyles = makeStyles(theme => ({
  root: {
    height: '100%',
    width: '100%',
    position: 'relative',
    display: 'flex',
    flexDirection: 'column',
    backgroundColor: '#F5F5F5'
  }
}))
<commit_msg>fix(perf): Don't show searching animation when sync tab is hidden<commit_after>import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react'
import logger from 'electron-timber'
import { makeStyles } from '@material-ui/core'
import { remote } from 'electron'
import { useIntl, defineMessages } from 'react-intl'
import path from 'path'

import api from '../../new-api'
import Searching from './Searching'
import SyncAppBar from './SyncAppBar'
import SyncTarget from './SyncTarget'
import SyncGrid from './SyncGrid'

export const peerStatus = {
  READY: 'ready',
  PROGRESS: 'progress',
  ERROR: 'error',
  COMPLETE: 'complete'
}

const m = defineMessages({
  openSyncFileDialog: 'Select a database to syncronize',
  createSyncFileDialog: 'Create a new database to syncronize'
})

const fileDialogFilters = [
  {
    name: 'Mapeo Data (*.mapeodata)',
    extensions: ['mapeodata', 'mapeo-jungle', 'sync', 'zip']
  }
]

const SyncView = ({ focusState }) => {
  const cx = useStyles()
  const listenForSyncPeers = focusState === 'focused'
  const [peers, syncPeer] = usePeers(listenForSyncPeers)
  const { formatMessage: t } = useIntl()
  logger.log('render peers', peers)

  const handleClickSelectSyncfile = () => {
    remote.dialog.showOpenDialog(
      {
        title: t(m.openSyncFileDialog),
        properties: ['openFile'],
        filters: fileDialogFilters
      },
      filenames => {
        if (typeof filenames === 'undefined' || filenames.length !== 1) return
        syncPeer(filenames[0], { file: true })
      }
    )
  }

  const handleClickNewSyncfile = () => {
    remote.dialog.showSaveDialog(
      {
        title: t(m.openSyncFileDialog),
        defaultPath: 'database.mapeodata',
        filters: fileDialogFilters
      },
      filename => {
        if (!filename) return
        syncPeer(filename, { file: true })
      }
    )
  }

  return (

      <SyncAppBar
        onClickSelectSyncfile={handleClickSelectSyncfile}
        onClickNewSyncfile={handleClickNewSyncfile}
      />
      {peers.length === 0 && focusState === 'focused' ? (
        <Searching />
      ) : (
        <SyncGrid>
          {peers.map(peer => (
            <SyncTarget
              key={peer.id}
              {...peer}
              onClick={() => syncPeer(peer.id)}
            />
          ))}
        </SyncGrid>
      )}

  )
}

export default SyncView

function usePeers (listen) {
  const lastClosed = useRef(Date.now())
  const [serverPeers, setServerPeers] = useState([])
  const [syncErrors, setSyncErrors] = useState(new Map())
  const [syncRequests, setSyncRequests] = useState(new Map())

  // Keep a ref of the last time this view was closed (used to maintain peer
  // "completed" state in the UI)
  useEffect(
    () => {
      if (!listen) lastClosed.current = Date.now()
    },
    [listen]
  )

  useEffect(
    () => {
      // Only start listening if `listen` is true
      if (!listen) return

      const updatePeers = (updatedServerPeers = []) => {
        logger.log('Received peer update', updatedServerPeers)
        setServerPeers(updatedServerPeers)
        // NB: use callback version of setState because the new error state
        // depends on the previous error state
        setSyncErrors(syncErrors => {
          const newErrors = new Map(syncErrors)
          updatedServerPeers.forEach(peer => {
            if (peer.state && peer.state.topic === 'replication-error') {
              newErrors.set(peer.id, peer.state.message)
            }
          })
          return newErrors
        })
        // Argh, this is hacky. This is making up for us not being able to rely
        // on server state for rendering the UI
        setSyncRequests(syncRequests => {
          const newSyncRequests = new Map(syncRequests)
          updatedServerPeers.forEach(peer => {
            if (!peer.state) return
            if (
              peer.state.topic === 'replication-error' ||
              peer.state.topic === 'replication-complete'
            ) {
              newSyncRequests.delete(peer.id)
            }
          })
          return newSyncRequests
        })
      }

      // Whenever the sync view becomes focused, announce for sync and start
      // listening for updates to peer status
      api.syncJoin()
      const peerListener = api.addPeerListener(updatePeers)

      // When the listen changes or component unmounts, cleanup listeners
      return () => {
        api.syncLeave()
        if (peerListener) peerListener.remove()
      }
    },
    [listen]
  )

  const peers = useMemo(
    () =>
      getPeersStatus({
        syncRequests,
        serverPeers,
        syncErrors,
        since: lastClosed.current
      }),
    [serverPeers, syncErrors, syncRequests]
  )

  const syncPeer = useCallback(
    (peerId, opts) => {
      logger.log('Request sync start', peerId, serverPeers)
      if (opts && opts.file) return api.syncStart({ filename: peerId })
      const peer = serverPeers.find(peer => peer.id === peerId)
      // Peer could have vanished in the moment the button was pressed
      if (peer) {
        // The server does always respond immediately with progress, especially
        // if the two devices are already up to sync. We store the request state
        // so the user can see the UI update when they click the button
        setSyncRequests(syncRequests => {
          const newSyncRequests = new Map(syncRequests)
          newSyncRequests.set(peerId, true)
          return newSyncRequests
        })
        api.syncStart(peer)
      }
    },
    [serverPeers]
  )

  return [peers, syncPeer]
}

/**
 * The peer status from Mapeo Core does not 'remember' the completion of a sync.
 * If the user is not looking at the screen when sync completes, they might miss
 * it. This function derives a peer status from the server state and any errors
 */
function getPeersStatus ({ serverPeers = [], syncErrors, syncRequests, since }) {
  return serverPeers.map(serverPeer => {
    let status = peerStatus.READY
    let complete
    const state = serverPeer.state || {}
    if (
      state.topic === 'replication-progress' ||
      state.topic === 'replication-started' ||
      syncRequests.has(serverPeer.id)
    ) {
      status = peerStatus.PROGRESS
    } else if (
      syncErrors.has(serverPeer.id) ||
      state.topic === 'replication-error'
    ) {
      status = peerStatus.ERROR
    } else if (
      (state.lastCompletedDate || 0) > since ||
      state.topic === 'replication-complete'
    ) {
      status = peerStatus.COMPLETE
      complete = state.message
    }
    return {
      id: serverPeer.id,
      name: serverPeer.filename
        ? path.basename(serverPeer.name)
        : serverPeer.name,
      status: status,
      lastCompleted: complete || state.lastCompletedDate,
      error: syncErrors.get(serverPeer.id),
      progress: getPeerProgress(serverPeer.state),
      deviceType: serverPeer.filename ? 'file' : serverPeer.deviceType
    }
  })
}

// We combine media and database items in progress. In order to show roughtly
// accurate progress, this weighting is how much more progress a media item
// counts vs. a database item
const MEDIA_WEIGHTING = 50
function getPeerProgress (peerState) {
  if (
    !peerState ||
    peerState.topic !== 'replication-progress' ||
    !peerState.message ||
    !peerState.message.db ||
    !peerState.message.media
  ) {
    return
  }
  const total =
    (peerState.message.db.total || 0) +
    (peerState.message.media.total || 0) * MEDIA_WEIGHTING
  const sofar =
    (peerState.message.db.sofar || 0) +
    (peerState.message.media.sofar || 0) * MEDIA_WEIGHTING
  const progress = total > 0 ? sofar / total : 0
  // Round progress to 2-decimal places. PeerItem is memoized, so it will not
  // update if progress does not change. Without rounding PeerItem updates
  // unnecessarily on every progress change, when we are only showing the user a
  // rounded percentage progress. Increase this to 3-decimal places if you want
  // to show more detail to the user.
  return {
    percent: Math.round(progress * 100) / 100,
    mediaSofar: peerState.message.media.sofar || 0,
    mediaTotal: peerState.message.media.total || 0,
    dbSofar: peerState.message.db.sofar || 0,
    dbTotal: peerState.message.db.total || 0
  }
}

const useStyles = makeStyles(theme => ({
  root: {
    height: '100%',
    width: '100%',
    position: 'relative',
    display: 'flex',
    flexDirection: 'column',
    backgroundColor: '#F5F5F5'
  }
}))
<|endoftext|>


<commit_before>function WebGLBindingStates( gl, extensions, attributes, capabilities ) {

	const maxVertexAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS );

	const extension = capabilities.isWebGL2 ? null : extensions.get( 'OES_vertex_array_object' );
	const vaoAvailable = capabilities.isWebGL2 || extension !== null;

	const bindingStates = {};

	const defaultState = createBindingState( null );
	let currentState = defaultState;

	function setup( object, material, program, geometry, index ) {

		let updateBuffers = false;

		if ( vaoAvailable ) {

			const state = getBindingState( geometry, program, material );

			if ( currentState !== state ) {

				currentState = state;
				bindVertexArrayObject( currentState.object );

			}

			updateBuffers = needsUpdate( geometry );

			if ( updateBuffers ) saveCache( geometry );

		} else {

			const wireframe = ( material.wireframe === true );

			if ( currentState.geometry !== geometry.id ||
				currentState.program !== program.id ||
				currentState.wireframe !== wireframe ) {

				currentState.geometry = geometry.id;
				currentState.program = program.id;
				currentState.wireframe = wireframe;

				updateBuffers = true;

			}

		}

		if ( object.isInstancedMesh === true ) {

			updateBuffers = true;

		}

		if ( index !== null ) {

			attributes.update( index, gl.ELEMENT_ARRAY_BUFFER );

		}

		if ( updateBuffers ) {

			setupVertexAttributes( object, material, program, geometry );

			if ( index !== null ) {

				gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, attributes.get( index ).buffer );

			}

		}

	}

	function createVertexArrayObject() {

		if ( capabilities.isWebGL2 ) return gl.createVertexArray();

		return extension.createVertexArrayOES();

	}

	function bindVertexArrayObject( vao ) {

		if ( capabilities.isWebGL2 ) return gl.bindVertexArray( vao );

		return extension.bindVertexArrayOES( vao );

	}

	function deleteVertexArrayObject( vao ) {

		if ( capabilities.isWebGL2 ) return gl.deleteVertexArray( vao );

		return extension.deleteVertexArrayOES( vao );

	}

	function getBindingState( geometry, program, material ) {

		const wireframe = ( material.wireframe === true );

		let programMap = bindingStates[ geometry.id ];

		if ( programMap === undefined ) {

			programMap = {};
			bindingStates[ geometry.id ] = programMap;

		}

		let stateMap = programMap[ program.id ];

		if ( stateMap === undefined ) {

			stateMap = {};
			programMap[ program.id ] = stateMap;

		}

		let state = stateMap[ wireframe ];

		if ( state === undefined ) {

			state = createBindingState( createVertexArrayObject() );
			stateMap[ wireframe ] = state;

		}

		return state;

	}

	function createBindingState( vao ) {

		const newAttributes = [];
		const enabledAttributes = [];
		const attributeDivisors = [];

		for ( let i = 0; i < maxVertexAttributes; i ++ ) {

			newAttributes[ i ] = 0;
			enabledAttributes[ i ] = 0;
			attributeDivisors[ i ] = 0;

		}

		return {

			// for backward compatibility on non-VAO support browser
			geometry: null,
			program: null,
			wireframe: false,

			newAttributes: newAttributes,
			enabledAttributes: enabledAttributes,
			attributeDivisors: attributeDivisors,
			object: vao,
			attributes: {}

		};

	}

	function needsUpdate( geometry ) {

		const cachedAttributes = currentState.attributes;
		const geometryAttributes = geometry.attributes;

		if ( Object.keys( cachedAttributes ).length !== Object.keys( geometryAttributes ).length ) return true;

		for ( const key in geometryAttributes ) {

			const cachedAttribute = cachedAttributes[ key ];
			const geometryAttribute = geometryAttributes[ key ];

			if ( cachedAttribute.attribute !== geometryAttribute ) return true;

			if ( cachedAttribute.data !== geometryAttribute.data ) return true;

		}

		return false;

	}

	function saveCache( geometry ) {

		const cache = {};
		const attributes = geometry.attributes;

		for ( const key in attributes ) {

			const attribute = attributes[ key ];

			const data = {};
			data.attribute = attribute;

			if ( attribute.data ) {

				data.data = attribute.data;

			}

			cache[ key ] = data;

		}

		currentState.attributes = cache;

	}

	function initAttributes() {

		const newAttributes = currentState.newAttributes;

		for ( let i = 0, il = newAttributes.length; i < il; i ++ ) {

			newAttributes[ i ] = 0;

		}

	}

	function enableAttribute( attribute ) {

		enableAttributeAndDivisor( attribute, 0 );

	}

	function enableAttributeAndDivisor( attribute, meshPerAttribute ) {

		const newAttributes = currentState.newAttributes;
		const enabledAttributes = currentState.enabledAttributes;
		const attributeDivisors = currentState.attributeDivisors;

		newAttributes[ attribute ] = 1;

		if ( enabledAttributes[ attribute ] === 0 ) {

			gl.enableVertexAttribArray( attribute );
			enabledAttributes[ attribute ] = 1;

		}

		if ( attributeDivisors[ attribute ] !== meshPerAttribute ) {

			const extension = capabilities.isWebGL2 ? gl : extensions.get( 'ANGLE_instanced_arrays' );

			extension[ capabilities.isWebGL2 ? 'vertexAttribDivisor' : 'vertexAttribDivisorANGLE' ]( attribute, meshPerAttribute );
			attributeDivisors[ attribute ] = meshPerAttribute;

		}

	}

	function disableUnusedAttributes() {

		const newAttributes = currentState.newAttributes;
		const enabledAttributes = currentState.enabledAttributes;

		for ( let i = 0, il = enabledAttributes.length; i < il; i ++ ) {

			if ( enabledAttributes[ i ] !== newAttributes[ i ] ) {

				gl.disableVertexAttribArray( i );
				enabledAttributes[ i ] = 0;

			}

		}

	}

	function vertexAttribPointer( index, size, type, normalized, stride, offset ) {

		if ( capabilities.isWebGL2 === true && ( type === gl.INT || type === gl.UNSIGNED_INT ) ) {

			gl.vertexAttribIPointer( index, size, type, stride, offset );

		} else {

			gl.vertexAttribPointer( index, size, type, normalized, stride, offset );

		}

	}

	function setupVertexAttributes( object, material, program, geometry ) {

		if ( capabilities.isWebGL2 === false && ( object.isInstancedMesh || geometry.isInstancedBufferGeometry ) ) {

			if ( extensions.get( 'ANGLE_instanced_arrays' ) === null ) return;

		}

		initAttributes();

		const geometryAttributes = geometry.attributes;

		const programAttributes = program.getAttributes();

		const materialDefaultAttributeValues = material.defaultAttributeValues;

		for ( const name in programAttributes ) {

			const programAttribute = programAttributes[ name ];

			if ( programAttribute >= 0 ) {

				const geometryAttribute = geometryAttributes[ name ];

				if ( geometryAttribute !== undefined ) {

					const normalized = geometryAttribute.normalized;
					const size = geometryAttribute.itemSize;

					const attribute = attributes.get( geometryAttribute );

					// TODO Attribute may not be available on context restore

					if ( attribute === undefined ) continue;

					const buffer = attribute.buffer;
					const type = attribute.type;
					const bytesPerElement = attribute.bytesPerElement;

					if ( geometryAttribute.isInterleavedBufferAttribute ) {

						const data = geometryAttribute.data;
						const stride = data.stride;
						const offset = geometryAttribute.offset;

						if ( data && data.isInstancedInterleavedBuffer ) {

							enableAttributeAndDivisor( programAttribute, data.meshPerAttribute );

							if ( geometry._maxInstanceCount === undefined ) {

								geometry._maxInstanceCount = data.meshPerAttribute * data.count;

							}

						} else {

							enableAttribute( programAttribute );

						}

						gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
						vertexAttribPointer( programAttribute, size, type, normalized, stride * bytesPerElement, offset * bytesPerElement );

					} else {

						if ( geometryAttribute.isInstancedBufferAttribute ) {

							enableAttributeAndDivisor( programAttribute, geometryAttribute.meshPerAttribute );

							if ( geometry._maxInstanceCount === undefined ) {

								geometry._maxInstanceCount = geometryAttribute.meshPerAttribute * geometryAttribute.count;

							}

						} else {

							enableAttribute( programAttribute );

						}

						gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
						vertexAttribPointer( programAttribute, size, type, normalized, 0, 0 );

					}

				} else if ( name === 'instanceMatrix' ) {

					const attribute = attributes.get( object.instanceMatrix );

					// TODO Attribute may not be available on context restore

					if ( attribute === undefined ) continue;

					const buffer = attribute.buffer;
					const type = attribute.type;

					enableAttributeAndDivisor( programAttribute + 0, 1 );
					enableAttributeAndDivisor( programAttribute + 1, 1 );
					enableAttributeAndDivisor( programAttribute + 2, 1 );
					enableAttributeAndDivisor( programAttribute + 3, 1 );

					gl.bindBuffer( gl.ARRAY_BUFFER, buffer );

					gl.vertexAttribPointer( programAttribute + 0, 4, type, false, 64, 0 );
					gl.vertexAttribPointer( programAttribute + 1, 4, type, false, 64, 16 );
					gl.vertexAttribPointer( programAttribute + 2, 4, type, false, 64, 32 );
					gl.vertexAttribPointer( programAttribute + 3, 4, type, false, 64, 48 );

				} else if ( name === 'instanceColor' ) {

					const attribute = attributes.get( object.instanceColor );

					// TODO Attribute may not be available on context restore

					if ( attribute === undefined ) continue;

					const buffer = attribute.buffer;
					const type = attribute.type;

					enableAttributeAndDivisor( programAttribute, 1 );

					gl.bindBuffer( gl.ARRAY_BUFFER, buffer );

					gl.vertexAttribPointer( programAttribute, 3, type, false, 12, 0 );

				} else if ( materialDefaultAttributeValues !== undefined ) {

					const value = materialDefaultAttributeValues[ name ];

					if ( value !== undefined ) {

						switch ( value.length ) {

							case 2:
								gl.vertexAttrib2fv( programAttribute, value );
								break;

							case 3:
								gl.vertexAttrib3fv( programAttribute, value );
								break;

							case 4:
								gl.vertexAttrib4fv( programAttribute, value );
								break;

							default:
								gl.vertexAttrib1fv( programAttribute, value );

						}

					}

				}

			}

		}

		disableUnusedAttributes();

	}

	function dispose() {

		reset();

		for ( const geometryId in bindingStates ) {

			const programMap = bindingStates[ geometryId ];

			for ( const programId in programMap ) {

				const stateMap = programMap[ programId ];

				for ( const wireframe in stateMap ) {

					deleteVertexArrayObject( stateMap[ wireframe ].object );

					delete stateMap[ wireframe ];

				}

				delete programMap[ programId ];

			}

			delete bindingStates[ geometryId ];

		}

	}

	function releaseStatesOfGeometry( geometry ) {

		if ( bindingStates[ geometry.id ] === undefined ) return;

		const programMap = bindingStates[ geometry.id ];

		for ( const programId in programMap ) {

			const stateMap = programMap[ programId ];

			for ( const wireframe in stateMap ) {

				deleteVertexArrayObject( stateMap[ wireframe ].object );

				delete stateMap[ wireframe ];

			}

			delete programMap[ programId ];

		}

		delete bindingStates[ geometry.id ];

	}

	function releaseStatesOfProgram( program ) {

		for ( const geometryId in bindingStates ) {

			const programMap = bindingStates[ geometryId ];

			if ( programMap[ program.id ] === undefined ) continue;

			const stateMap = programMap[ program.id ];

			for ( const wireframe in stateMap ) {

				deleteVertexArrayObject( stateMap[ wireframe ].object );

				delete stateMap[ wireframe ];

			}

			delete programMap[ program.id ];

		}

	}

	function reset() {

		resetDefaultState();

		if ( currentState === defaultState ) return;

		currentState = defaultState;
		bindVertexArrayObject( currentState.object );

	}

	// for backward-compatilibity

	function resetDefaultState() {

		defaultState.geometry = null;
		defaultState.program = null;
		defaultState.wireframe = false;

	}

	return {

		setup: setup,
		reset: reset,
		resetDefaultState: resetDefaultState,
		dispose: dispose,
		releaseStatesOfGeometry: releaseStatesOfGeometry,
		releaseStatesOfProgram: releaseStatesOfProgram,

		initAttributes: initAttributes,
		enableAttribute: enableAttribute,
		disableUnusedAttributes: disableUnusedAttributes

	};

}

export { WebGLBindingStates };
<commit_msg>Add index cache for vao<commit_after>function WebGLBindingStates( gl, extensions, attributes, capabilities ) {

	const maxVertexAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS );

	const extension = capabilities.isWebGL2 ? null : extensions.get( 'OES_vertex_array_object' );
	const vaoAvailable = capabilities.isWebGL2 || extension !== null;

	const bindingStates = {};

	const defaultState = createBindingState( null );
	let currentState = defaultState;

	function setup( object, material, program, geometry, index ) {

		let updateBuffers = false;

		if ( vaoAvailable ) {

			const state = getBindingState( geometry, program, material );

			if ( currentState !== state ) {

				currentState = state;
				bindVertexArrayObject( currentState.object );

			}

			updateBuffers = needsUpdate( geometry, index );

			if ( updateBuffers ) saveCache( geometry, index );

		} else {

			const wireframe = ( material.wireframe === true );

			if ( currentState.geometry !== geometry.id ||
				currentState.program !== program.id ||
				currentState.wireframe !== wireframe ) {

				currentState.geometry = geometry.id;
				currentState.program = program.id;
				currentState.wireframe = wireframe;

				updateBuffers = true;

			}

		}

		if ( object.isInstancedMesh === true ) {

			updateBuffers = true;

		}

		if ( index !== null ) {

			attributes.update( index, gl.ELEMENT_ARRAY_BUFFER );

		}

		if ( updateBuffers ) {

			setupVertexAttributes( object, material, program, geometry );

			if ( index !== null ) {

				gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, attributes.get( index ).buffer );

			}

		}

	}

	function createVertexArrayObject() {

		if ( capabilities.isWebGL2 ) return gl.createVertexArray();

		return extension.createVertexArrayOES();

	}

	function bindVertexArrayObject( vao ) {

		if ( capabilities.isWebGL2 ) return gl.bindVertexArray( vao );

		return extension.bindVertexArrayOES( vao );

	}

	function deleteVertexArrayObject( vao ) {

		if ( capabilities.isWebGL2 ) return gl.deleteVertexArray( vao );

		return extension.deleteVertexArrayOES( vao );

	}

	function getBindingState( geometry, program, material ) {

		const wireframe = ( material.wireframe === true );

		let programMap = bindingStates[ geometry.id ];

		if ( programMap === undefined ) {

			programMap = {};
			bindingStates[ geometry.id ] = programMap;

		}

		let stateMap = programMap[ program.id ];

		if ( stateMap === undefined ) {

			stateMap = {};
			programMap[ program.id ] = stateMap;

		}

		let state = stateMap[ wireframe ];

		if ( state === undefined ) {

			state = createBindingState( createVertexArrayObject() );
			stateMap[ wireframe ] = state;

		}

		return state;

	}

	function createBindingState( vao ) {

		const newAttributes = [];
		const enabledAttributes = [];
		const attributeDivisors = [];

		for ( let i = 0; i < maxVertexAttributes; i ++ ) {

			newAttributes[ i ] = 0;
			enabledAttributes[ i ] = 0;
			attributeDivisors[ i ] = 0;

		}

		return {

			// for backward compatibility on non-VAO support browser
			geometry: null,
			program: null,
			wireframe: false,

			newAttributes: newAttributes,
			enabledAttributes: enabledAttributes,
			attributeDivisors: attributeDivisors,
			object: vao,
			attributes: {},
			index: null

		};

	}

	function needsUpdate( geometry, index ) {

		const cachedAttributes = currentState.attributes;
		const geometryAttributes = geometry.attributes;

		if ( Object.keys( cachedAttributes ).length !== Object.keys( geometryAttributes ).length ) return true;

		for ( const key in geometryAttributes ) {

			const cachedAttribute = cachedAttributes[ key ];
			const geometryAttribute = geometryAttributes[ key ];

			if ( cachedAttribute.attribute !== geometryAttribute ) return true;

			if ( cachedAttribute.data !== geometryAttribute.data ) return true;

		}

		if ( currentState.index !== index ) return true;

		return false;

	}

	function saveCache( geometry, index ) {

		const cache = {};
		const attributes = geometry.attributes;

		for ( const key in attributes ) {

			const attribute = attributes[ key ];

			const data = {};
			data.attribute = attribute;

			if ( attribute.data ) {

				data.data = attribute.data;

			}

			cache[ key ] = data;

		}

		currentState.attributes = cache;

		currentState.index = index;

	}

	function initAttributes() {

		const newAttributes = currentState.newAttributes;

		for ( let i = 0, il = newAttributes.length; i < il; i ++ ) {

			newAttributes[ i ] = 0;

		}

	}

	function enableAttribute( attribute ) {

		enableAttributeAndDivisor( attribute, 0 );

	}

	function enableAttributeAndDivisor( attribute, meshPerAttribute ) {

		const newAttributes = currentState.newAttributes;
		const enabledAttributes = currentState.enabledAttributes;
		const attributeDivisors = currentState.attributeDivisors;

		newAttributes[ attribute ] = 1;

		if ( enabledAttributes[ attribute ] === 0 ) {

			gl.enableVertexAttribArray( attribute );
			enabledAttributes[ attribute ] = 1;

		}

		if ( attributeDivisors[ attribute ] !== meshPerAttribute ) {

			const extension = capabilities.isWebGL2 ? gl : extensions.get( 'ANGLE_instanced_arrays' );

			extension[ capabilities.isWebGL2 ? 'vertexAttribDivisor' : 'vertexAttribDivisorANGLE' ]( attribute, meshPerAttribute );
			attributeDivisors[ attribute ] = meshPerAttribute;

		}

	}

	function disableUnusedAttributes() {

		const newAttributes = currentState.newAttributes;
		const enabledAttributes = currentState.enabledAttributes;

		for ( let i = 0, il = enabledAttributes.length; i < il; i ++ ) {

			if ( enabledAttributes[ i ] !== newAttributes[ i ] ) {

				gl.disableVertexAttribArray( i );
				enabledAttributes[ i ] = 0;

			}

		}

	}

	function vertexAttribPointer( index, size, type, normalized, stride, offset ) {

		if ( capabilities.isWebGL2 === true && ( type === gl.INT || type === gl.UNSIGNED_INT ) ) {

			gl.vertexAttribIPointer( index, size, type, stride, offset );

		} else {

			gl.vertexAttribPointer( index, size, type, normalized, stride, offset );

		}

	}

	function setupVertexAttributes( object, material, program, geometry ) {

		if ( capabilities.isWebGL2 === false && ( object.isInstancedMesh || geometry.isInstancedBufferGeometry ) ) {

			if ( extensions.get( 'ANGLE_instanced_arrays' ) === null ) return;

		}

		initAttributes();

		const geometryAttributes = geometry.attributes;

		const programAttributes = program.getAttributes();

		const materialDefaultAttributeValues = material.defaultAttributeValues;

		for ( const name in programAttributes ) {

			const programAttribute = programAttributes[ name ];

			if ( programAttribute >= 0 ) {

				const geometryAttribute = geometryAttributes[ name ];

				if ( geometryAttribute !== undefined ) {

					const normalized = geometryAttribute.normalized;
					const size = geometryAttribute.itemSize;

					const attribute = attributes.get( geometryAttribute );

					// TODO Attribute may not be available on context restore

					if ( attribute === undefined ) continue;

					const buffer = attribute.buffer;
					const type = attribute.type;
					const bytesPerElement = attribute.bytesPerElement;

					if ( geometryAttribute.isInterleavedBufferAttribute ) {

						const data = geometryAttribute.data;
						const stride = data.stride;
						const offset = geometryAttribute.offset;

						if ( data && data.isInstancedInterleavedBuffer ) {

							enableAttributeAndDivisor( programAttribute, data.meshPerAttribute );

							if ( geometry._maxInstanceCount === undefined ) {

								geometry._maxInstanceCount = data.meshPerAttribute * data.count;

							}

						} else {

							enableAttribute( programAttribute );

						}

						gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
						vertexAttribPointer( programAttribute, size, type, normalized, stride * bytesPerElement, offset * bytesPerElement );

					} else {

						if ( geometryAttribute.isInstancedBufferAttribute ) {

							enableAttributeAndDivisor( programAttribute, geometryAttribute.meshPerAttribute );

							if ( geometry._maxInstanceCount === undefined ) {

								geometry._maxInstanceCount = geometryAttribute.meshPerAttribute * geometryAttribute.count;

							}

						} else {

							enableAttribute( programAttribute );

						}

						gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
						vertexAttribPointer( programAttribute, size, type, normalized, 0, 0 );

					}

				} else if ( name === 'instanceMatrix' ) {

					const attribute = attributes.get( object.instanceMatrix );

					// TODO Attribute may not be available on context restore

					if ( attribute === undefined ) continue;

					const buffer = attribute.buffer;
					const type = attribute.type;

					enableAttributeAndDivisor( programAttribute + 0, 1 );
					enableAttributeAndDivisor( programAttribute + 1, 1 );
					enableAttributeAndDivisor( programAttribute + 2, 1 );
					enableAttributeAndDivisor( programAttribute + 3, 1 );

					gl.bindBuffer( gl.ARRAY_BUFFER, buffer );

					gl.vertexAttribPointer( programAttribute + 0, 4, type, false, 64, 0 );
					gl.vertexAttribPointer( programAttribute + 1, 4, type, false, 64, 16 );
					gl.vertexAttribPointer( programAttribute + 2, 4, type, false, 64, 32 );
					gl.vertexAttribPointer( programAttribute + 3, 4, type, false, 64, 48 );

				} else if ( name === 'instanceColor' ) {

					const attribute = attributes.get( object.instanceColor );

					// TODO Attribute may not be available on context restore

					if ( attribute === undefined ) continue;

					const buffer = attribute.buffer;
					const type = attribute.type;

					enableAttributeAndDivisor( programAttribute, 1 );

					gl.bindBuffer( gl.ARRAY_BUFFER, buffer );

					gl.vertexAttribPointer( programAttribute, 3, type, false, 12, 0 );

				} else if ( materialDefaultAttributeValues !== undefined ) {

					const value = materialDefaultAttributeValues[ name ];

					if ( value !== undefined ) {

						switch ( value.length ) {

							case 2:
								gl.vertexAttrib2fv( programAttribute, value );
								break;

							case 3:
								gl.vertexAttrib3fv( programAttribute, value );
								break;

							case 4:
								gl.vertexAttrib4fv( programAttribute, value );
								break;

							default:
								gl.vertexAttrib1fv( programAttribute, value );

						}

					}

				}

			}

		}

		disableUnusedAttributes();

	}

	function dispose() {

		reset();

		for ( const geometryId in bindingStates ) {

			const programMap = bindingStates[ geometryId ];

			for ( const programId in programMap ) {

				const stateMap = programMap[ programId ];

				for ( const wireframe in stateMap ) {

					deleteVertexArrayObject( stateMap[ wireframe ].object );

					delete stateMap[ wireframe ];

				}

				delete programMap[ programId ];

			}

			delete bindingStates[ geometryId ];

		}

	}

	function releaseStatesOfGeometry( geometry ) {

		if ( bindingStates[ geometry.id ] === undefined ) return;

		const programMap = bindingStates[ geometry.id ];

		for ( const programId in programMap ) {

			const stateMap = programMap[ programId ];

			for ( const wireframe in stateMap ) {

				deleteVertexArrayObject( stateMap[ wireframe ].object );

				delete stateMap[ wireframe ];

			}

			delete programMap[ programId ];

		}

		delete bindingStates[ geometry.id ];

	}

	function releaseStatesOfProgram( program ) {

		for ( const geometryId in bindingStates ) {

			const programMap = bindingStates[ geometryId ];

			if ( programMap[ program.id ] === undefined ) continue;

			const stateMap = programMap[ program.id ];

			for ( const wireframe in stateMap ) {

				deleteVertexArrayObject( stateMap[ wireframe ].object );

				delete stateMap[ wireframe ];

			}

			delete programMap[ program.id ];

		}

	}

	function reset() {

		resetDefaultState();

		if ( currentState === defaultState ) return;

		currentState = defaultState;
		bindVertexArrayObject( currentState.object );

	}

	// for backward-compatilibity

	function resetDefaultState() {

		defaultState.geometry = null;
		defaultState.program = null;
		defaultState.wireframe = false;

	}

	return {

		setup: setup,
		reset: reset,
		resetDefaultState: resetDefaultState,
		dispose: dispose,
		releaseStatesOfGeometry: releaseStatesOfGeometry,
		releaseStatesOfProgram: releaseStatesOfProgram,

		initAttributes: initAttributes,
		enableAttribute: enableAttribute,
		disableUnusedAttributes: disableUnusedAttributes

	};

}

export { WebGLBindingStates };
<|endoftext|>


<commit_before>define([
	'text!templates/search-box.tpl',
	'collections/locations'
], function(
	templateHtml,
	LocationsCollection
) {

	// TODO: Way to let user click "current location"

	// TODO: Store field values and restore them on render

	function initDatePickers(view) {

		// TODO: Fix display of calendar popups on mobile

		// get checkin/checkout fields
		$checkinField = $('input[name="checkin"]', view.$el);
		$checkoutField = $('input[name="checkout"]', view.$el);

		// initialize both fields the same way
		_.each([$checkinField, $checkoutField], function($field) {
			$field.datepicker({
				minDate: 0,
				numberOfMonths: 2,
				slideshowAnim: "slideDown",
				dateFormat: "yy-mm-dd"
			});
		});

		// checking field specific settings
		$checkinField.datepicker("option", "onClose", function(selectedDate) {
			$checkoutField.datepicker( "option", "minDate", selectedDate );
		});
		$checkinField.datepicker('setDate', 'now');

		// checkout field specific settings
		$checkoutField.datepicker("option", "onClose", function(selectedDate) {
			$checkinField.datepicker( "option", "maxDate", selectedDate );
		});
		$checkoutField.datepicker('setDate', "+1d");

		// TODO: Hide date fields and show as formatted date text when not focused. Click text/icon to change.

	}

	// initialize location search field autocomplete
	function initLocationAutoComplete(view) {

		// TODO: ignore punctuation in matching and consider both abbreviated and full state names

		// create autocomplete instance on search field
		view.$search.autocomplete({
			minLength: 3,
			select: function(evt, ui) {
				var itm = ui.item;
				$('input[name="lat"]', view.$el).val(itm.lat);
				$('input[name="lng"]', view.$el).val(itm.lng);
				view.currLocationLabel = itm.label;
				view.$form.submit();
			},
			source: []
		});
	}

	// initialize keyup event handler on location field
	function initLocationKeyup(view) {

		var keyUpDelay = 500;	// millisecond delay to perform location search after key up

		// when the user finishes entering a keystroke in the location search
		view.$search.keyup(function(evt) {

			// TODO: Fix buggy application of results based on current location search (check timing)

			// make sure key pressed was a word character
			if((evt.which != 8 && evt.which != 846) && !String.fromCharCode(evt.which).match(/\w/)) {
				return;
			}

			// TODO: cache location search results (maybe)

			// if the search string is greater than 2 characters
			if(view.$search.val().length > 2) {
				// perform the actual search after the last keystroke was at least keyUpDelay ago
				view.$search.addClass('loading');
				com.jeromedane.Utils.delay(function(){
					searchLocations(view);
				}, keyUpDelay);
			}
		});
	}

	// initialize location search field
	function initLocationSearchField(view) {

		view.$search = $('.location input', view.$el);

		// remember original text to be able to conditionally style input
		view._origSearchPrompt = view.$search.val();

		// restore location text if previously set
		if(view.currLocationLabel) {
			view.$search.val(view.currLocationLabel);
		}

		// handle search field gain focus
		view.$search.focus(function() {
			// remove original search prompt on focus
			if(this.value === view._origSearchPrompt) {
				this.value = "";
				styleLocationSearchInput(view);
			}

			// TODO: Fix having to tap on search field twice on iPhones (and other mobiles?)

		});

		// handle search field lose focus
		view.$search.blur(function() {

			// restore any current location to search field on blur
			view.$search.val(view.currLocationLabel);

			// restore original prompt if field is empty
			if(this.value.replace(/\s/g, '') === "") {
				this.value = view._origSearchPrompt;
			}
			styleLocationSearchInput(view);

		});

		initLocationAutoComplete(view);
		initLocationKeyup(view);

		styleLocationSearchInput(view);

	}

	// initialize maximum distance input
	function initMaxDist(view) {

		var $slider = $('.distSlider');
		var initialDist = 10;

		function updateSliderDisplay(val) {
			if(val < 5) return false;
			var $label = $('.search-field.dist label');
			$label.text($label.text().replace(/MAXDIST|\d+/, val));
			$('input[name="dist"]').val(val);
			return true;
		}

		$slider.slider({
			range: 'min',
			min: 0,
			max: 50,
			step: 5,
			value: initialDist,
			slide: function(evt, ui) {
				return updateSliderDisplay(ui.value);
			}
		});

		updateSliderDisplay(initialDist);
	}

	function initSubmit(view) {
		view.$form = $('form', view.$el);
		view.$form.submit(function() {
			searchHotels(view);
			return false;
		});

		$('input[type="submit"]', view.$form).button();

		// TODO: show location not found message as appropriate
	}

	// map locations collection to autocomplete data and refresh options
	function parseLocationResults() {
		this.$search.autocomplete('option', 'source', this.locations.map(function(loc) {
			return {
				label: loc.get('city') + ', ' + l(loc.get('state')) + ' ' + loc.get('postal'),
				city: loc.get('city'),
				state: loc.get('state'),
				zip: loc.get('postal'),
				lat: loc.get('latitude'),
				lng: loc.get('longitude')
			};
		}));
		this.$search.removeClass('loading');
		this.$search.keydown();					// force open autocomplete options
	}

	// perform hotel search
	function searchHotels(view) {

		// TODO: Apply search parameters to URL in order to allow bookmarking and page refresh

		// TODO: Display error on missing or bad lat/long parameters

		view.hotels.setParams(view.$form.serialize());
		view.hotels.fetch({
			complete: function() {
				// tell other views to re-render as necessary
				view.hotels.trigger('update');
			},
			error: function(e) {
				// TODO: error handling and UI message
				console.log('error', e);
			}
		});
	}

	// apply search filter and get locations matching that filter
	function searchLocations(view) {
		view.locations.setFilter(view.$search.val());
		view.locations.fetch({
			complete: function() {
				// tell other views to re-render as necessary
				view.locations.trigger('update');
			},
			error: function(e) {
				// TODO: error handling and UI message
				console.log('error', e);
			}
		});
	}

	// style location search input based on whether original text is present or not
	function styleLocationSearchInput(view) {
		if(view.$search.val() === view._origSearchPrompt) {
			view.$search.addClass("prompt");
		} else {
			view.$search.removeClass("prompt");
		}
	}

	var view = {

		template: _.template(templateHtml),

		initialize: function(options) {

			if(!options || !options.hotels) throw "Search box view must be initialized with a hotels collection";

			this.hotels = options.hotels;

			this.locations = new LocationsCollection();
			this.locations.on('update', parseLocationResults, this);

			// preload cville hotels for easy preview during development
			this.hotels.setParams('lat=38.035466&lng=-78.46289&dist=10&checkin=2015-07-03&checkout=2015-07-04');
			this.hotels.fetch();

		},

		render: function() {

			this.$el.html(this.template());

			initDatePickers(this);
			initLocationSearchField(this);
			initMaxDist(this);
			initSubmit(this);

		}
	};

	return Backbone.View.extend(view);

});<commit_msg>Don't submit location search on enter pressed within input fields<commit_after>define([
	'text!templates/search-box.tpl',
	'collections/locations'
], function(
	templateHtml,
	LocationsCollection
) {

	// TODO: Way to let user click "current location"

	// TODO: Store field values and restore them on render

	function initDatePickers(view) {

		// TODO: Fix display of calendar popups on mobile

		// get checkin/checkout fields
		$checkinField = $('input[name="checkin"]', view.$el);
		$checkoutField = $('input[name="checkout"]', view.$el);

		// initialize both fields the same way
		_.each([$checkinField, $checkoutField], function($field) {
			$field.datepicker({
				minDate: 0,
				numberOfMonths: 2,
				slideshowAnim: "slideDown",
				dateFormat: "yy-mm-dd"
			});
		});

		// checking field specific settings
		$checkinField.datepicker("option", "onClose", function(selectedDate) {
			$checkoutField.datepicker( "option", "minDate", selectedDate );
		});
		$checkinField.datepicker('setDate', 'now');

		// checkout field specific settings
		$checkoutField.datepicker("option", "onClose", function(selectedDate) {
			$checkinField.datepicker( "option", "maxDate", selectedDate );
		});
		$checkoutField.datepicker('setDate', "+1d");

		// TODO: Hide date fields and show as formatted date text when not focused. Click text/icon to change.

	}

	// initialize location search field autocomplete
	function initLocationAutoComplete(view) {

		// TODO: ignore punctuation in matching and consider both abbreviated and full state names

		// create autocomplete instance on search field
		view.$search.autocomplete({
			minLength: 3,
			select: function(evt, ui) {
				var itm = ui.item;
				$('input[name="lat"]', view.$el).val(itm.lat);
				$('input[name="lng"]', view.$el).val(itm.lng);
				view.currLocationLabel = itm.label;
				view.$form.submit();
			},
			source: []
		});
	}

	// initialize keyup event handler on location field
	function initLocationKeyup(view) {

		var keyUpDelay = 500;	// millisecond delay to perform location search after key up

		// when the user finishes entering a keystroke in the location search
		view.$search.keyup(function(evt) {

			// TODO: Fix buggy application of results based on current location search (check timing)

			// make sure key pressed was a word character
			if((evt.which != 8 && evt.which != 846) && !String.fromCharCode(evt.which).match(/\w/)) {
				return;
			}

			// TODO: cache location search results (maybe)

			// if the search string is greater than 2 characters
			if(view.$search.val().length > 2) {
				// perform the actual search after the last keystroke was at least keyUpDelay ago
				view.$search.addClass('loading');
				com.jeromedane.Utils.delay(function(){
					searchLocations(view);
				}, keyUpDelay);
			}
		});
	}

	// initialize location search field
	function initLocationSearchField(view) {

		view.$search = $('.location input', view.$el);

		// remember original text to be able to conditionally style input
		view._origSearchPrompt = view.$search.val();

		// restore location text if previously set
		if(view.currLocationLabel) {
			view.$search.val(view.currLocationLabel);
		}

		// handle search field gain focus
		view.$search.focus(function() {
			// remove original search prompt on focus
			if(this.value === view._origSearchPrompt) {
				this.value = "";
				styleLocationSearchInput(view);
			}

			// TODO: Fix having to tap on search field twice on iPhones (and other mobiles?)

		});

		// handle search field lose focus
		view.$search.blur(function() {

			// restore any current location to search field on blur
			view.$search.val(view.currLocationLabel);

			// restore original prompt if field is empty
			if(this.value.replace(/\s/g, '') === "") {
				this.value = view._origSearchPrompt;
			}
			styleLocationSearchInput(view);

		});

		initLocationAutoComplete(view);
		initLocationKeyup(view);

		styleLocationSearchInput(view);

	}

	// initialize maximum distance input
	function initMaxDist(view) {

		var $slider = $('.distSlider');
		var initialDist = 10;

		function updateSliderDisplay(val) {
			if(val < 5) return false;
			var $label = $('.search-field.dist label');
			$label.text($label.text().replace(/MAXDIST|\d+/, val));
			$('input[name="dist"]').val(val);
			return true;
		}

		$slider.slider({
			range: 'min',
			min: 0,
			max: 50,
			step: 5,
			value: initialDist,
			slide: function(evt, ui) {
				return updateSliderDisplay(ui.value);
			}
		});

		updateSliderDisplay(initialDist);
	}

	function initSubmit(view) {
		view.$form = $('form', view.$el);
		view.$form.submit(function() {
			searchHotels(view);
			return false;
		});

		// don't submit form on enter pressed
		$('input', view.$form).keydown(function(event){
			if(event.keyCode === 13) {
			  event.preventDefault();
			  return false;
			}
		});

		$('input[type="submit"]', view.$form).button();

		// TODO: show location not found message as appropriate
	}

	// map locations collection to autocomplete data and refresh options
	function parseLocationResults() {
		this.$search.autocomplete('option', 'source', this.locations.map(function(loc) {
			return {
				label: loc.get('city') + ', ' + l(loc.get('state')) + ' ' + loc.get('postal'),
				city: loc.get('city'),
				state: loc.get('state'),
				zip: loc.get('postal'),
				lat: loc.get('latitude'),
				lng: loc.get('longitude')
			};
		}));
		this.$search.removeClass('loading');
		this.$search.keydown();					// force open autocomplete options
	}

	// perform hotel search
	function searchHotels(view) {

		// TODO: Apply search parameters to URL in order to allow bookmarking and page refresh

		// TODO: Display error on missing or bad lat/long parameters

		view.hotels.setParams(view.$form.serialize());
		view.hotels.fetch({
			complete: function() {
				// tell other views to re-render as necessary
				view.hotels.trigger('update');
			},
			error: function(e) {
				// TODO: error handling and UI message
				console.log('error', e);
			}
		});
	}

	// apply search filter and get locations matching that filter
	function searchLocations(view) {
		view.locations.setFilter(view.$search.val());
		view.locations.fetch({
			complete: function() {
				// tell other views to re-render as necessary
				view.locations.trigger('update');
			},
			error: function(e) {
				// TODO: error handling and UI message
				console.log('error', e);
			}
		});
	}

	// style location search input based on whether original text is present or not
	function styleLocationSearchInput(view) {
		if(view.$search.val() === view._origSearchPrompt) {
			view.$search.addClass("prompt");
		} else {
			view.$search.removeClass("prompt");
		}
	}

	var view = {

		template: _.template(templateHtml),

		initialize: function(options) {

			if(!options || !options.hotels) throw "Search box view must be initialized with a hotels collection";

			this.hotels = options.hotels;

			this.locations = new LocationsCollection();
			this.locations.on('update', parseLocationResults, this);

			// preload cville hotels for easy preview during development
			this.hotels.setParams('lat=38.035466&lng=-78.46289&dist=10&checkin=2015-07-03&checkout=2015-07-04');
			this.hotels.fetch();

		},

		render: function() {

			this.$el.html(this.template());

			initDatePickers(this);
			initLocationSearchField(this);
			initMaxDist(this);
			initSubmit(this);

		}
	};

	return Backbone.View.extend(view);

});<|endoftext|>


<commit_before>(function ($) {
  function SlickColumnPicker(columns, grid, options) {
    var $menu;
    var columnCheckboxes;
    var onUpdateColumns = new Slick.Event();

    var columnsLookup = {};

    var defaults = {
      fadeSpeed: 250,
      forceFitColumnsText: "Force fit columns",
      syncColumnCellResizeText: "Synchronous resize"
    };

    function updateColumnLookupTable(list) {
      for (var i = 0; i < list.length; i++) {
        columnsLookup[ list[i].id ] = list[i];
      }
    }

    function init() {
      options = $.extend({}, defaults, options);
      updateColumnLookupTable(columns);
      grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);
      grid.onColumnsReordered.subscribe(updateColumnOrder);

      $menu = $("").appendTo(document.body);

      $menu.bind("mouseleave", function (e) {
        $(this).fadeOut(options.fadeSpeed);
      });
      $menu.bind("click", updateColumn);
    }

    function destroy() {
      grid.onHeaderContextMenu.unsubscribe(handleHeaderContextMenu);
      grid.onColumnsReordered.unsubscribe(updateColumnOrder);
      $menu.remove();
    }

    function handleHeaderContextMenu(e, args) {
      e.preventDefault();
      $menu.empty();
      updateColumnOrder();
      columnCheckboxes = [];

      var $li, $input;
      for (var i = 0; i < columns.length; i++) {
        // Do not show columns (a.k.a. 'protected columns') in the pick list which either:
        // - have an empty 'name' field in their column definition (they would show up as a checkbox for 'nothing' anyway)
        // - are marked as hidden by having an ID which starts with an underscore, e.g. "_checkbox_selector"
        var colName = columns[i].name;
        if (typeof colName !== 'string' && !colName) continue;
        if (colName === '') continue;
        if (/^_./.test(columns[i].id)) continue;

        $li = $("
").appendTo($menu);
        $input = $("<input type='checkbox' />").data("column-id", columns[i].id);
        columnCheckboxes.push($input);

        if (grid.getColumnIndex(columns[i].id) != null) {
          $input.attr("checked", "checked");
        }

        $("")
            .text(columns[i].name)
            .prepend($input)
            .appendTo($li);
      }

      $("
").appendTo($menu);
      $li = $("
").appendTo($menu);
      $input = $("<input type='checkbox' />").data("option", "autoresize");
      $("")
          .text(options.forceFitColumnsText)
          .prepend($input)
          .appendTo($li);
      if (grid.getOptions().forceFitColumns) {
        $input.attr("checked", "checked");
      }

      $li = $("
").appendTo($menu);
      $input = $("<input type='checkbox' />").data("option", "syncresize");
      $("")
          .text(options.syncColumnCellResizeText)
          .prepend($input)
          .appendTo($li);
      if (grid.getOptions().syncColumnCellResize) {
        $input.attr("checked", "checked");
      }

      $menu
          .css("top", Math.min(e.pageY, $(window).height() - $menu.height()) - 10)
          .css("left", Math.min(e.pageX, $(window).width() - $menu.width()) - 10)
          .fadeIn(options.fadeSpeed);
    }

    function updateColumnOrder() {
      // Because columns can be reordered, we have to update the `columns`
      // to reflect the new order, however we can't just take `grid.getColumns()`,
      // as it does not include columns currently hidden by the picker.
      // We create a new `columns` structure by leaving currently-hidden
      // columns in their original ordinal position and interleaving the results
      // of the current column sort.
      var current = grid.getColumns().slice(0);
      var ordered = new Array(columns.length);
      for (var i = 0; i < ordered.length; i++) {
        if (grid.getColumnIndex(columns[i].id) === undefined) {
          // If the column doesn't return a value from getColumnIndex,
          // it is hidden. Leave it in this position.
          ordered[i] = columns[i];
        } else {
          // Otherwise, grab the next visible column.
          ordered[i] = current.shift();
        }
      }
      // Now we MAY arrive at a situation where other (userland) code has added columns
      // while we weren't looking: if there are any, then those will linger in the
      // `current` array right now and we can simply append them.
      columns = ordered.concat(current);
      updateColumnLookupTable(columns);
    }

    function updateColumn(e) {
      if ($(e.target).data("option") === "autoresize") {
        if (e.target.checked) {
          grid.setOptions({forceFitColumns: true});
          grid.autosizeColumns();
        } else {
          grid.setOptions({forceFitColumns: false});
        }
        return;
      }

      if ($(e.target).data("option") === "syncresize") {
        if (e.target.checked) {
          grid.setOptions({syncColumnCellResize: true});
        } else {
          grid.setOptions({syncColumnCellResize: false});
        }
        return;
      }

      if ($(e.target).is(":checkbox")) {
        var visibleColumns = [];
        var invisibleColumns = [];

        $.each(columnCheckboxes, function (i, e) {
          var columnID = $(e).data('column-id');
          if (columnID != null && columnsLookup[columnID]) {
            if ($(this).is(":checked")) {
              visibleColumns.push( columnsLookup[columnID] );
            } else {
              invisibleColumns.push( columnsLookup[columnID] );
            }
          }
        });

        // Always keep at least one column visible:
        if (!visibleColumns.length) {
          $(e.target).attr("checked", "checked");
          return;
        }

        // Now create the proper columns ordered list for SlickGrid by interleaving the 'protected' columns
        // with the new visible set: hat we do is discard the *in*visible set, because that's the equivalent
        // operation.
        var ordered = columns.slice(0);
        var newColumnSet = ordered.filter(function (col) {
          return (invisibleColumns.indexOf(col) === -1);
        });

        grid.setColumns(newColumnSet);
        var evd = new Slick.EventData();
        evd.hiddenColumns = invisibleColumns;
        evd.visibleColumns = visibleColumns;
        onUpdateColumns.notify(visibleColumns, evd);
      }
    }

    function getAllColumns() {
      return columns;
    }

    init();

    return {
      "getAllColumns": getAllColumns,
      "onUpdateColumns": onUpdateColumns,
      "destroy": destroy
    };
  }
  // Slick.Controls.ColumnPicker
  $.extend(true, window, {
    Slick: {
      Controls: {
        ColumnPicker: SlickColumnPicker
      }
    }
  });
})(jQuery);
<commit_msg>treat all getColumnIndex() checks exactly the same throughout the code: always check using `!= null` or `== null`: treat NULL and UNDEFINED as if identifal.<commit_after>(function ($) {
  function SlickColumnPicker(columns, grid, options) {
    var $menu;
    var columnCheckboxes;
    var onUpdateColumns = new Slick.Event();

    var columnsLookup = {};

    var defaults = {
      fadeSpeed: 250,
      forceFitColumnsText: "Force fit columns",
      syncColumnCellResizeText: "Synchronous resize"
    };

    function updateColumnLookupTable(list) {
      for (var i = 0; i < list.length; i++) {
        columnsLookup[ list[i].id ] = list[i];
      }
    }

    function init() {
      options = $.extend({}, defaults, options);
      updateColumnLookupTable(columns);
      grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);
      grid.onColumnsReordered.subscribe(updateColumnOrder);

      $menu = $("").appendTo(document.body);

      $menu.bind("mouseleave", function (e) {
        $(this).fadeOut(options.fadeSpeed);
      });
      $menu.bind("click", updateColumn);
    }

    function destroy() {
      grid.onHeaderContextMenu.unsubscribe(handleHeaderContextMenu);
      grid.onColumnsReordered.unsubscribe(updateColumnOrder);
      $menu.remove();
    }

    function handleHeaderContextMenu(e, args) {
      e.preventDefault();
      $menu.empty();
      updateColumnOrder();
      columnCheckboxes = [];

      var $li, $input;
      for (var i = 0; i < columns.length; i++) {
        // Do not show columns (a.k.a. 'protected columns') in the pick list which either:
        // - have an empty 'name' field in their column definition (they would show up as a checkbox for 'nothing' anyway)
        // - are marked as hidden by having an ID which starts with an underscore, e.g. "_checkbox_selector"
        var colName = columns[i].name;
        if (typeof colName !== 'string' && !colName) continue;
        if (colName === '') continue;
        if (/^_./.test(columns[i].id)) continue;

        $li = $("
").appendTo($menu);
        $input = $("<input type='checkbox' />").data("column-id", columns[i].id);
        columnCheckboxes.push($input);

        if (grid.getColumnIndex(columns[i].id) != null) {
          $input.attr("checked", "checked");
        }

        $("")
            .text(columns[i].name)
            .prepend($input)
            .appendTo($li);
      }

      $("
").appendTo($menu);
      $li = $("
").appendTo($menu);
      $input = $("<input type='checkbox' />").data("option", "autoresize");
      $("")
          .text(options.forceFitColumnsText)
          .prepend($input)
          .appendTo($li);
      if (grid.getOptions().forceFitColumns) {
        $input.attr("checked", "checked");
      }

      $li = $("
").appendTo($menu);
      $input = $("<input type='checkbox' />").data("option", "syncresize");
      $("")
          .text(options.syncColumnCellResizeText)
          .prepend($input)
          .appendTo($li);
      if (grid.getOptions().syncColumnCellResize) {
        $input.attr("checked", "checked");
      }

      $menu
          .css("top", Math.min(e.pageY, $(window).height() - $menu.height()) - 10)
          .css("left", Math.min(e.pageX, $(window).width() - $menu.width()) - 10)
          .fadeIn(options.fadeSpeed);
    }

    function updateColumnOrder() {
      // Because columns can be reordered, we have to update the `columns`
      // to reflect the new order, however we can't just take `grid.getColumns()`,
      // as it does not include columns currently hidden by the picker.
      // We create a new `columns` structure by leaving currently-hidden
      // columns in their original ordinal position and interleaving the results
      // of the current column sort.
      var current = grid.getColumns().slice(0);
      var ordered = new Array(columns.length);
      for (var i = 0; i < ordered.length; i++) {
        if (grid.getColumnIndex(columns[i].id) != null) {
          // If the column doesn't return a value from getColumnIndex,
          // it is hidden. Leave it in this position.
          ordered[i] = columns[i];
        } else {
          // Otherwise, grab the next visible column.
          ordered[i] = current.shift();
        }
      }
      // Now we MAY arrive at a situation where other (userland) code has added columns
      // while we weren't looking: if there are any, then those will linger in the
      // `current` array right now and we can simply append them.
      columns = ordered.concat(current);
      updateColumnLookupTable(columns);
    }

    function updateColumn(e) {
      if ($(e.target).data("option") === "autoresize") {
        if (e.target.checked) {
          grid.setOptions({forceFitColumns: true});
          grid.autosizeColumns();
        } else {
          grid.setOptions({forceFitColumns: false});
        }
        return;
      }

      if ($(e.target).data("option") === "syncresize") {
        if (e.target.checked) {
          grid.setOptions({syncColumnCellResize: true});
        } else {
          grid.setOptions({syncColumnCellResize: false});
        }
        return;
      }

      if ($(e.target).is(":checkbox")) {
        var visibleColumns = [];
        var invisibleColumns = [];

        $.each(columnCheckboxes, function (i, e) {
          var columnID = $(e).data('column-id');
          if (columnID != null && columnsLookup[columnID]) {
            if ($(this).is(":checked")) {
              visibleColumns.push( columnsLookup[columnID] );
            } else {
              invisibleColumns.push( columnsLookup[columnID] );
            }
          }
        });

        // Always keep at least one column visible:
        if (!visibleColumns.length) {
          $(e.target).attr("checked", "checked");
          return;
        }

        // Now create the proper columns ordered list for SlickGrid by interleaving the 'protected' columns
        // with the new visible set: hat we do is discard the *in*visible set, because that's the equivalent
        // operation.
        var ordered = columns.slice(0);
        var newColumnSet = ordered.filter(function (col) {
          return (invisibleColumns.indexOf(col) === -1);
        });

        grid.setColumns(newColumnSet);
        var evd = new Slick.EventData();
        evd.hiddenColumns = invisibleColumns;
        evd.visibleColumns = visibleColumns;
        onUpdateColumns.notify(visibleColumns, evd);
      }
    }

    function getAllColumns() {
      return columns;
    }

    init();

    return {
      "getAllColumns": getAllColumns,
      "onUpdateColumns": onUpdateColumns,
      "destroy": destroy
    };
  }
  // Slick.Controls.ColumnPicker
  $.extend(true, window, {
    Slick: {
      Controls: {
        ColumnPicker: SlickColumnPicker
      }
    }
  });
})(jQuery);
<|endoftext|>