<commit_before>window.stacks = window.stacks || {};
stacks.sync = stacks.sync || {};

stacks.sync.client_id = stacks.sync.client_id || "815060055713-or8bqdnlear2ua7d0s51toc71bllfn1t.apps.googleusercontent.com";

stacks.sync.auth2 = stacks.sync.auth2 || null;
stacks.sync.auth2listening = stacks.sync.auth2listening || false;
stacks.sync.gdriveid = null;
stacks.sync.initialFinished = false;

stacks.sync.preauth = stacks.sync.preauth || function() {
  if (!gapi || !gapi.signin2) {
    setTimeout(function() {stacks.sync.preauth();}, 100);
    return;
  }

  console.log("sync: pre-authentication");

  gapi.signin2.render("settings-sync-signin-hidden", {
    "scope": ""
  });

  stacks.sync.auth(false);
}

stacks.sync.auth = stacks.sync.auth || function(active) {
  if (!gapi.auth2 || !gapi.client) {
    setTimeout(function() {stacks.sync.auth(active);}, 100);
    return;
  }

  console.log("sync: authentication" + (active ? " (active)" : ""));

  if (!stacks.sync.auth2) {
    stacks.sync.auth2 = gapi.auth2.getAuthInstance();
  }

  if (!stacks.sync.auth2listening) {
    stacks.sync.auth2listening = true;
    stacks.sync.auth2.isSignedIn.listen(function(status) {if (status) {stacks.sync.onSignIn();} else {stacks.sync.onSignOut();}});
  }

  if (stacks.sync.auth2.isSignedIn.get()) {
    stacks.sync.onSignIn();
  } else if (active) {
    stacks.sync.signIn();
  }

  gapi.client.load("drive", "v2");
}

stacks.sync.signIn = stacks.sync.signIn || function() {
  console.log("sync: Signing in...");
  stacks.sync.auth2.signIn({"scope": "https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/drive"});
};

stacks.sync.signOut = stacks.sync.signOut || function() {
  console.log("sync: Signing out...");
  stacks.sync.auth2.currentUser.get().disconnect();
};

stacks.sync.onSignIn = stacks.sync.onSignIn || function() {
  console.log("sync: Signed in.");
  $("#settings-sync-signin").css("display", "none");
  $("#settings-sync-signout").css("display", "");

  stacks.sync.load(function() {stacks.sync.save(function() {stacks.sync.initialFinished = true;});});
};

stacks.sync.onSignOut = stacks.sync.onSignOut || function() {
  console.log("sync: Signed out.");
  $("#settings-sync-signout").css("display", "none");
  $("#settings-sync-signin").css("display", "");
};

stacks.sync.newGDriveFile_running = false;

stacks.sync.newGDriveFile = stacks.sync.newGDriveFile || function(cb) {
  cb = cb || function() {};

  if (stacks.sync.gdriveid) {
    cb(stacks.sync.gdriveid);
    return;
  }
  if (!gapi.client) {
    setTimeout(function() {stacks.sync.newGDriveFile(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.newGDriveFile(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }
  if (stacks.sync.newGDriveFile_running) {
    setTimeout(function() {stacks.sync.newGDriveFile(cb);}, 100);
    return;
  }

  console.log("sync: Creating a new Stacks Sync file in GDrive...");

  stacks.sync.newGDriveFile_running = true;
  gapi.client.drive.files.insert({"uploadType": "multipart", "title": "Stacks Sync", "mimeType": "application/json"}).execute(function(result) {
    stacks.sync.gdriveid = result.id;
    stacks.sync.newGDriveFile_running = false;
    console.log("sync: New Stacks Sync file created.");
    cb(result.id);
  });
};

stacks.sync.getGDriveFile = stacks.sync.getGDriveFile || function(cb) {
  cb = cb || function() {};

  if (stacks.sync.gdriveid) {
    cb(stacks.sync.gdriveid);
    return;
  }
  if (!gapi.client) {
    setTimeout(function() {stacks.sync.getGDriveFile(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.getGDriveFile(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }

  console.log("sync: Searching for the Stacks Sync file in GDrive...");

  gapi.client.drive.files.list({"q": "title = 'Stacks Sync'"}).execute(function(result) {
    if (!result.items) {
      console.log("sync: WARNING: Something went wrong with the getGDriveFile result.");
      stacks.sync.getGDriveFile(cb);
      return;
    }

    if (result.items.length == 0) {
      console.log("sync: WARNING: No file found with getGDriveFile.");
      stacks.sync.newGDriveFile(cb);
      return;
    }

    stacks.sync.gdriveid = result.items[0].id;
    console.log("sync: Stacks Sync file found in GDrive.");
    cb(result.items[0].id);
  });
};

stacks.sync.load = stacks.sync.load || function(cb) {
  cb = cb || function() {};

  if (!gapi.client) {
    setTimeout(function() {stacks.sync.load(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.load(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }

  console.log("sync: Loading data from GDrive...");

  stacks.sync.gdriveid = stacks.sync.gdriveid;
  if (!stacks.sync.gdriveid) {
    stacks.sync.getGDriveFile(function() {stacks.sync.load(cb);});
    return;
  }

  gapi.client.drive.files.get({"fileId": stacks.sync.gdriveid}).execute(function(file) {
    console.log("sync: File found; Downloading...");
    if (file.downloadUrl) {
      var auth = stacks.sync.auth2.currentUser.get().getAuthResponse();
      $.ajax({
        url: file.downloadUrl,
        cache: false,
        method: "GET",
        headers: {
          "Authorization": auth.token_type + " " + auth.access_token
        },
        success: function(data) {
          if (parseInt(data.timestamp) <= parseInt(localStorage.timestamp)) {
            console.log("sync: Downloaded data from GDrive; Changes will not be applied due to the timestamp.");
            console.log("sync: Online: " + data.timestamp + "; Offline: " + localStorage.timestamp);
            cb(data);
            return;
          }
          console.log("sync: Downloaded data from GDrive; Applying changes...");
          var pinnedOld = localStorage.pinned;
          var themeOld = localStorage.theme;
          var locationOld = localStorage.location;
          var alwaysListeningOld = localStorage["voicesearch:alwaysListening"];
          for (var key in (data || {})) {
            localStorage[key] = data[key];
          }
          localStorage.location = locationOld;
          if (localStorage.pinned != pinnedOld) {
            refreshCards();
          }
          if (localStorage.theme != themeOld) {
            themer.setTheme(localStorage.theme);
          }
          voicesearch.setAlwaysListening(alwaysListeningOld);
          settings.refreshHotwords();
          //stacks.refresh();//TODO create stacks.js
          cb(data);
        },
        error: function() {
          console.log("sync: WARNING: GDrive Stacks Sync file did not download properly.");
          cb(null);
        }
      });
    } else {
      console.log("sync: WARNING: GDrive Stacks Sync file cannot be downloaded.");
    }
  });
};

stacks.sync.save = stacks.sync.save || function(cb) {
  cb = cb || function() {};

  if (!gapi.client) {
    setTimeout(function() {stacks.sync.save(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.save(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }

  console.log("sync: Saving data to GDrive...");

  localStorage.timestamp = Date.now();

  stacks.sync.gdriveid = stacks.sync.gdriveid;
  if (!stacks.sync.gdriveid) {
    stacks.sync.getGDriveFile(function() {stacks.sync.save(cb);});
    return;
  }

  var auth = stacks.sync.auth2.currentUser.get().getAuthResponse();
  $.ajax({
    url: "https://www.googleapis.com/upload/drive/v2/files/" + stacks.sync.gdriveid + "?uploadType=media&newRevision=false",
    cache: false,
    method: "PUT",
    headers: {
      "Authorization": auth.token_type + " " + auth.access_token
    },
    data: JSON.stringify(localStorage),
    success: function(data) {
      console.log("sync: Stacks Sync file uploaded to GDrive.");
      cb(data);
    },
    error: function() {
      console.log("sync: WARNING: Stacks Sync file could not be uploaded to GDrive.");
      cb(null);
    }
  });
};

$(document).ready(function() {
  stacks.sync.preauth();
  $("#settings-sync-signin").click(stacks.sync.signIn);
  $("#settings-sync-signout").click(stacks.sync.signOut);
});

localStorage_setItem_ = localStorage.setItem;
localStorage.setItem = function() {
  localStorage_setItem_.apply(this, arguments);
  if (stacks.sync.initialFinished) {
    stacks.sync.save();
  }
};

localStorage_removeItem_ = localStorage.removeItem;
localStorage.removeItem = function() {
  localStorage_removeItem_.apply(this, arguments);
  if (stacks.sync.initialFinished) {
    stacks.sync.save();
  }
};
<commit_msg>Fix syncing hotwords and not syncing alwaysListening<commit_after>window.stacks = window.stacks || {};
stacks.sync = stacks.sync || {};

stacks.sync.client_id = stacks.sync.client_id || "815060055713-or8bqdnlear2ua7d0s51toc71bllfn1t.apps.googleusercontent.com";

stacks.sync.auth2 = stacks.sync.auth2 || null;
stacks.sync.auth2listening = stacks.sync.auth2listening || false;
stacks.sync.gdriveid = null;
stacks.sync.initialFinished = false;

stacks.sync.preauth = stacks.sync.preauth || function() {
  if (!gapi || !gapi.signin2) {
    setTimeout(function() {stacks.sync.preauth();}, 100);
    return;
  }

  console.log("sync: pre-authentication");

  gapi.signin2.render("settings-sync-signin-hidden", {
    "scope": ""
  });

  stacks.sync.auth(false);
}

stacks.sync.auth = stacks.sync.auth || function(active) {
  if (!gapi.auth2 || !gapi.client) {
    setTimeout(function() {stacks.sync.auth(active);}, 100);
    return;
  }

  console.log("sync: authentication" + (active ? " (active)" : ""));

  if (!stacks.sync.auth2) {
    stacks.sync.auth2 = gapi.auth2.getAuthInstance();
  }

  if (!stacks.sync.auth2listening) {
    stacks.sync.auth2listening = true;
    stacks.sync.auth2.isSignedIn.listen(function(status) {if (status) {stacks.sync.onSignIn();} else {stacks.sync.onSignOut();}});
  }

  if (stacks.sync.auth2.isSignedIn.get()) {
    stacks.sync.onSignIn();
  } else if (active) {
    stacks.sync.signIn();
  }

  gapi.client.load("drive", "v2");
}

stacks.sync.signIn = stacks.sync.signIn || function() {
  console.log("sync: Signing in...");
  stacks.sync.auth2.signIn({"scope": "https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/drive"});
};

stacks.sync.signOut = stacks.sync.signOut || function() {
  console.log("sync: Signing out...");
  stacks.sync.auth2.currentUser.get().disconnect();
};

stacks.sync.onSignIn = stacks.sync.onSignIn || function() {
  console.log("sync: Signed in.");
  $("#settings-sync-signin").css("display", "none");
  $("#settings-sync-signout").css("display", "");

  stacks.sync.load(function() {stacks.sync.save(function() {stacks.sync.initialFinished = true;});});
};

stacks.sync.onSignOut = stacks.sync.onSignOut || function() {
  console.log("sync: Signed out.");
  $("#settings-sync-signout").css("display", "none");
  $("#settings-sync-signin").css("display", "");
};

stacks.sync.newGDriveFile_running = false;

stacks.sync.newGDriveFile = stacks.sync.newGDriveFile || function(cb) {
  cb = cb || function() {};

  if (stacks.sync.gdriveid) {
    cb(stacks.sync.gdriveid);
    return;
  }
  if (!gapi.client) {
    setTimeout(function() {stacks.sync.newGDriveFile(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.newGDriveFile(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }
  if (stacks.sync.newGDriveFile_running) {
    setTimeout(function() {stacks.sync.newGDriveFile(cb);}, 100);
    return;
  }

  console.log("sync: Creating a new Stacks Sync file in GDrive...");

  stacks.sync.newGDriveFile_running = true;
  gapi.client.drive.files.insert({"uploadType": "multipart", "title": "Stacks Sync", "mimeType": "application/json"}).execute(function(result) {
    stacks.sync.gdriveid = result.id;
    stacks.sync.newGDriveFile_running = false;
    console.log("sync: New Stacks Sync file created.");
    cb(result.id);
  });
};

stacks.sync.getGDriveFile = stacks.sync.getGDriveFile || function(cb) {
  cb = cb || function() {};

  if (stacks.sync.gdriveid) {
    cb(stacks.sync.gdriveid);
    return;
  }
  if (!gapi.client) {
    setTimeout(function() {stacks.sync.getGDriveFile(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.getGDriveFile(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }

  console.log("sync: Searching for the Stacks Sync file in GDrive...");

  gapi.client.drive.files.list({"q": "title = 'Stacks Sync'"}).execute(function(result) {
    if (!result.items) {
      console.log("sync: WARNING: Something went wrong with the getGDriveFile result.");
      stacks.sync.getGDriveFile(cb);
      return;
    }

    if (result.items.length == 0) {
      console.log("sync: WARNING: No file found with getGDriveFile.");
      stacks.sync.newGDriveFile(cb);
      return;
    }

    stacks.sync.gdriveid = result.items[0].id;
    console.log("sync: Stacks Sync file found in GDrive.");
    cb(result.items[0].id);
  });
};

stacks.sync.load = stacks.sync.load || function(cb) {
  cb = cb || function() {};

  if (!gapi.client) {
    setTimeout(function() {stacks.sync.load(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.load(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }

  console.log("sync: Loading data from GDrive...");

  stacks.sync.gdriveid = stacks.sync.gdriveid;
  if (!stacks.sync.gdriveid) {
    stacks.sync.getGDriveFile(function() {stacks.sync.load(cb);});
    return;
  }

  gapi.client.drive.files.get({"fileId": stacks.sync.gdriveid}).execute(function(file) {
    console.log("sync: File found; Downloading...");
    if (file.downloadUrl) {
      var auth = stacks.sync.auth2.currentUser.get().getAuthResponse();
      $.ajax({
        url: file.downloadUrl,
        cache: false,
        method: "GET",
        headers: {
          "Authorization": auth.token_type + " " + auth.access_token
        },
        success: function(data) {
          if (parseInt(data.timestamp) <= parseInt(localStorage.timestamp)) {
            console.log("sync: Downloaded data from GDrive; Changes will not be applied due to the timestamp.");
            console.log("sync: Online: " + data.timestamp + "; Offline: " + localStorage.timestamp);
            cb(data);
            return;
          }
          console.log("sync: Downloaded data from GDrive; Applying changes...");
          var pinnedOld = localStorage.pinned;
          var themeOld = localStorage.theme;
          var locationOld = localStorage.location;
          var alwaysListeningOld = localStorage["voicesearch:alwaysListening"];
          for (var key in (data || {})) {
            localStorage[key] = data[key];
          }
          localStorage.location = locationOld;
          localStorage["voicesearch:alwaysListening"] = alwaysListeningOld;
          if (localStorage.pinned != pinnedOld) {
            refreshCards();
          }
          if (localStorage.theme != themeOld) {
            themer.setTheme(localStorage.theme);
          }
          voicesearch.hotwordsCustom = JSON.parse(localStorage["voicesearch:hotwords"]);
          settings.refreshHotwords();
          //stacks.refresh();//TODO create stacks.js
          cb(data);
        },
        error: function() {
          console.log("sync: WARNING: GDrive Stacks Sync file did not download properly.");
          cb(null);
        }
      });
    } else {
      console.log("sync: WARNING: GDrive Stacks Sync file cannot be downloaded.");
    }
  });
};

stacks.sync.save = stacks.sync.save || function(cb) {
  cb = cb || function() {};

  if (!gapi.client) {
    setTimeout(function() {stacks.sync.save(cb);}, 100);
    return;
  }
  if (!gapi.client.drive) {
    gapi.client.load("drive", "v2", function() {stacks.sync.save(cb);});
    return;
  }
  if (!stacks.sync.auth2.currentUser.get().hasGrantedScopes("https://www.googleapis.com/auth/drive")) {
    cb(null);
    return;
  }

  console.log("sync: Saving data to GDrive...");

  localStorage.timestamp = Date.now();

  stacks.sync.gdriveid = stacks.sync.gdriveid;
  if (!stacks.sync.gdriveid) {
    stacks.sync.getGDriveFile(function() {stacks.sync.save(cb);});
    return;
  }

  var auth = stacks.sync.auth2.currentUser.get().getAuthResponse();
  $.ajax({
    url: "https://www.googleapis.com/upload/drive/v2/files/" + stacks.sync.gdriveid + "?uploadType=media&newRevision=false",
    cache: false,
    method: "PUT",
    headers: {
      "Authorization": auth.token_type + " " + auth.access_token
    },
    data: JSON.stringify(localStorage),
    success: function(data) {
      console.log("sync: Stacks Sync file uploaded to GDrive.");
      cb(data);
    },
    error: function() {
      console.log("sync: WARNING: Stacks Sync file could not be uploaded to GDrive.");
      cb(null);
    }
  });
};

$(document).ready(function() {
  stacks.sync.preauth();
  $("#settings-sync-signin").click(stacks.sync.signIn);
  $("#settings-sync-signout").click(stacks.sync.signOut);
});

localStorage_setItem_ = localStorage.setItem;
localStorage.setItem = function() {
  localStorage_setItem_.apply(this, arguments);
  if (stacks.sync.initialFinished) {
    stacks.sync.save();
  }
};

localStorage_removeItem_ = localStorage.removeItem;
localStorage.removeItem = function() {
  localStorage_removeItem_.apply(this, arguments);
  if (stacks.sync.initialFinished) {
    stacks.sync.save();
  }
};
<|endoftext|>


<commit_before>/*!
 * Controllers.js
 * Copyright(c) 2014 Vinh Tran (vqtran)
 * MIT Licensed
 */

var express = require('express'),
    requireDir = require('require-dir'),
    methods = require('methods'),
    fs = require('fs'),
    Exception = require('./lib/exceptions.js'),
    ROOT = require('path').dirname(require.main.filename),
    DEFAULT_PATH = ROOT + "/app/controllers";

/* Controllers.js - Simply adding MVC-style controllers to Express.js
 *
 * For every file in the top level of controllers, attach the all paths
 * and respective handlers found in the controller to an instance of an
 * Express.Router to be used as middleware.
*/

var router;

/* Exposes constructor to initalize the Router object */
module.exports = function(options) {
    /* Check if old user from version 0. */
    if (options.set)
       throw "Controllers-js has changed, please read the version 1.x documentation.";
    router = express.Router();
    buildRouter(options);
    return router;
}

/* This method attaches all the controllers to the internal router */
function buildRouter(options) {
    var controllers, table;
    /* Handle options */
    if (options) {
        if (options.path)
            DEFAULT_PATH = ROOT + options.path;
        if (options.table)
            table = require(options.table);
    }
    /* Load controllers data */
    controllers = requireDir(DEFAULT_PATH);
    /* If routing table is provided, load based on the order defined */
    if (options.table)
        tableLoad(controllers, table);
    /* Otherwise, simple load (alphabetic) */
    else defaultLoad(controllers);
}

/* Routing table ordered load and attachment of the controllers */
function tableLoad(controllers, table) {
    table.forEach(function(route) {
        /* Check for correct number of params */
        if (route.length != 4)
            Exception.RoutingTableParameters(route);
        /* Parse the route parameters */
        var verb = route[0].toLowerCase();
        var path = route[1];
        var controllerName = route[2];
        var controllerFunction = route[3];
        /* Check for errors and attach route */
        if (isValidHttpVerb(verb)) {
            if (!controllers[controllerName])
               Exception.ControllerNotFound(controllerName);
            if (!controllers[controllerName][controllerFunction])
               Exception.RouteNotFound(controllerName, controllerFunction);
            var handler = controllers[controllerName][controllerFunction];
            router[verb](path, handler);
        }
        else Exception.InvalidHttpVerb(controllerName, controllerFunction, verb);
    });
}

/* Default-order load and attachment of the controllers */
function defaultLoad(controllers) {
    for (var c in controllers) {
        attachController(controllers[c], {controller_name: c});
    }
}

/* Attaches all routes associated with a controller to
 * the router, like router.get() or router.post()
 */
function attachController(controller, info) {
    for (var route in controller.routes) {
        /* action and path of a route */
        var config = controller.routes[route];
        /* handler function for the route */
        var handler = controller[route];
        /* If the controller has a handler associated with it
         * attach that route/handler to the app */
        if (controller[route]) {
            info.route = route;
            attachRoute(config, handler, info);
        }
        else Exception.RouteNotFound(info.controller_name, route);
    }
}

/* Attaches a specific route and its handler to the router*/
function attachRoute(config, handler, info) {
    /* Only if it is a support http verb */
    if (isValidHttpVerb(config.action)) {
        /* attaches to the router*/
        router[config.action.toLowerCase()](config.path, handler);
    }
    else Exception.InvalidHttpVerb(info.controller_name, info.route, config.action);
}

/* Given a string, returns true if it is a valid HTTP verb, false otherwise */
function isValidHttpVerb(verb) {
    return methods.indexOf(verb) > -1;
}
<commit_msg>Fixed bug when no options<commit_after>/*!
 * Controllers.js
 * Copyright(c) 2014 Vinh Tran (vqtran)
 * MIT Licensed
 */

var express = require('express'),
    requireDir = require('require-dir'),
    methods = require('methods'),
    fs = require('fs'),
    Exception = require('./lib/exceptions.js'),
    ROOT = require('path').dirname(require.main.filename),
    DEFAULT_PATH = ROOT + "/app/controllers";

/* Controllers.js - Simply adding MVC-style controllers to Express.js
 *
 * For every file in the top level of controllers, attach the all paths
 * and respective handlers found in the controller to an instance of an
 * Express.Router to be used as middleware.
*/

var router;

/* Exposes constructor to initalize the Router object */
module.exports = function(options) {
    /* Check if old user from version 0. */
    if (option && options.set)
       throw "Controllers-js has changed, please read the version 1.x documentation.";
    router = express.Router();
    buildRouter(options);
    return router;
}

/* This method attaches all the controllers to the internal router */
function buildRouter(options) {
    var controllers, table;
    /* Handle options */
    if (options) {
        if (options.path)
            DEFAULT_PATH = ROOT + options.path;
        if (options.table)
            table = require(options.table);
    }
    /* Load controllers data */
    controllers = requireDir(DEFAULT_PATH);
    /* If routing table is provided, load based on the order defined */
    if (options.table)
        tableLoad(controllers, table);
    /* Otherwise, simple load (alphabetic) */
    else defaultLoad(controllers);
}

/* Routing table ordered load and attachment of the controllers */
function tableLoad(controllers, table) {
    table.forEach(function(route) {
        /* Check for correct number of params */
        if (route.length != 4)
            Exception.RoutingTableParameters(route);
        /* Parse the route parameters */
        var verb = route[0].toLowerCase();
        var path = route[1];
        var controllerName = route[2];
        var controllerFunction = route[3];
        /* Check for errors and attach route */
        if (isValidHttpVerb(verb)) {
            if (!controllers[controllerName])
               Exception.ControllerNotFound(controllerName);
            if (!controllers[controllerName][controllerFunction])
               Exception.RouteNotFound(controllerName, controllerFunction);
            var handler = controllers[controllerName][controllerFunction];
            router[verb](path, handler);
        }
        else Exception.InvalidHttpVerb(controllerName, controllerFunction, verb);
    });
}

/* Default-order load and attachment of the controllers */
function defaultLoad(controllers) {
    for (var c in controllers) {
        attachController(controllers[c], {controller_name: c});
    }
}

/* Attaches all routes associated with a controller to
 * the router, like router.get() or router.post()
 */
function attachController(controller, info) {
    for (var route in controller.routes) {
        /* action and path of a route */
        var config = controller.routes[route];
        /* handler function for the route */
        var handler = controller[route];
        /* If the controller has a handler associated with it
         * attach that route/handler to the app */
        if (controller[route]) {
            info.route = route;
            attachRoute(config, handler, info);
        }
        else Exception.RouteNotFound(info.controller_name, route);
    }
}

/* Attaches a specific route and its handler to the router*/
function attachRoute(config, handler, info) {
    /* Only if it is a support http verb */
    if (isValidHttpVerb(config.action)) {
        /* attaches to the router*/
        router[config.action.toLowerCase()](config.path, handler);
    }
    else Exception.InvalidHttpVerb(info.controller_name, info.route, config.action);
}

/* Given a string, returns true if it is a valid HTTP verb, false otherwise */
function isValidHttpVerb(verb) {
    return methods.indexOf(verb) > -1;
}
<|endoftext|>


<commit_before>class RowFormDateTimeFieldController extends RowFormFieldController {
    constructor(...args) {
        super(...args);
        this.view2 = null;
        this.defaultValue = 0;
        this.state.parseError2 = false;
        this.state.error2 = null;
    }
    getViewClass() {
        return RowFormDateTimeFieldView;
    }
    getValueForView() {
        return this.getValue();
    }
    getValueForTime() {
        // console.log('RowFormDateTimeFieldController.getValueForTime', this.model.getFullName(), this.defaultValue, TimeBox.getStringValue(this.defaultValue));
        const date = this.getValue();
        if (date) {
            const value = date.getHours()*60 + date.getMinutes();
            // console.log('value:', value);
            if (value !== this.defaultValue) {
                // console.log('not equal to default value', value, this.defaultValue);
                return value;
            }
        }
        return null;
    }
    setValueFromView(viewValue) {
        if (viewValue === null) {
            this.state.parseError2 = false;
            this.resetErrors2();
        } else {
            const [h, m] = TimeBox.splitTime(this.defaultValue);
            viewValue.setHours(h, m);
        }
        this.setValue(viewValue);
    }
    onView2Create = view2 => {
        // console.log('RowFormDateTimeFieldController.onView2Create', view2);
        this.view2 = view2;
    }
    _onChange(viewValue) {
        // console.log('RowFormDateTimeFieldController._onChange', this.view2);
        if (viewValue !== null) setTimeout(() => {
            const input = this.view2.getInput();
            input.focus();
            input.setSelectionRange(0, input.value.length);
        }, 0);
    }
    onChange2 = (viewValue, fireEvent = true) => {
        // console.log('RowFormDateTimeFieldController.onChange2', viewValue);
        this.resetErrors2();
        try {
            this.setValueFromView2(viewValue);
        } catch (err) {
            console.log(`${this.model.getFullName()}: cannot parse time: ${err.message}`);
            this.state.parseError2 = true;
        }
        /*
        this.validate2();
        if (this.isValid()) {
            this.copyValueToModel();
        }
        */
        this.refreshChanged();
        if (fireEvent) {
            try {
                this.emit('change', {value: viewValue});
            } catch (err) {
                console.error('unhandled change event error:', this.model.getFullName(), err);
            }
            this.parent.onFieldChange({source: this});
        }
    }
    onBlur2 = (viewValue, fireEvent = false) => {
        // console.log('RowFormDateTimeFieldController.onBlur2', viewValue);
        if (!this.isEditable()) return;
        this.validate2();
        if (this.isValid()) {
            this.copyValueToModel();
        }
        this.refreshChanged();
        if (fireEvent) {
            try {
                this.emit('change', {value: viewValue});
            } catch (err) {
                console.error('unhandled change event error:', this.model.getFullName(), err);
            }
        }
        this.parent.onFieldChange({source: this});
    }
    getPlaceholder2() {
        return TimeBox.getStringValue(this.defaultValue);
    }
    getDefaultValue() {
        return this.defaultValue;
    }
    setDefaultValue(defaultValue) {
        // console.log('RowFormDateTimeFieldController.setDefaultValue', this.view2 ? this.view2.getValue() : null);
        if (typeof defaultValue === 'string') {
            this.defaultValue = TimeBox.getIntegerValue(defaultValue);
        } else {
            if (defaultValue >= 24*60) throw new Error(`wrong default value: ${defaultValue}`);
            this.defaultValue = defaultValue;
        }
        if (this.view2 && this.view2.getValue() === null && this.state.value) {
            this.setValue2(null);
        }
    }
    setValueFromView2(viewValue) {
        if (isNaN(viewValue)) throw new Error('wrong time');
        this.setValue2(viewValue);
    }
    setValue2(viewValue) {
        const value = viewValue !== null ? viewValue : this.defaultValue;
        const [h, m] = TimeBox.splitTime(value);
        this.state.value.setHours(h, m);
    }
    validate2() {
        // console.log('RowFormFieldController.validate', this.model.getFullName());
        this.state.error2 = this.getError2();
    }
    getError2() {
        // console.log('RowFormFieldController.getError', this.model.getFullName());

        // parse validator
        if (this.view2) {
            try {
                const viewValue = this.view2.getValue();
            } catch (err) {
                return `can't parse time: ${err.message}`;
            }
        }

        return null;
    }
    isParseError() {
        return super.isParseError() || this.state.parseError2;
    }
    resetErrors2() {
        this.setError2(null);
        this.state.parseError2 = false;
    }
    setError2(error2) {
        this.state.error2 = error2;
    }
    refill() {
        // console.log('RowFormDateTimeFieldController.refill');
        super.refill();
        this.view2.setValue(this.getValueForTime());
        this.resetErrors2();
        this.refreshChanged();
    }
    isValid() {
        return super.isValid() && this.state.error2 === null;
    }
    getErrorMessage() {
        if (this.state.error === null && this.state.error2 === null) return null;
        return [
            ...(this.state.error  ? [this.state.error]  : []),
            ...(this.state.error2 ? [this.state.error2] : [])
        ].join(', ');
    }
}
<commit_msg>reset view2 to null<commit_after>class RowFormDateTimeFieldController extends RowFormFieldController {
    constructor(...args) {
        super(...args);
        this.view2 = null;
        this.defaultValue = 0;
        this.state.parseError2 = false;
        this.state.error2 = null;
    }
    getViewClass() {
        return RowFormDateTimeFieldView;
    }
    getValueForView() {
        return this.getValue();
    }
    getValueForTime() {
        // console.log('RowFormDateTimeFieldController.getValueForTime', this.model.getFullName(), this.defaultValue, TimeBox.getStringValue(this.defaultValue));
        const date = this.getValue();
        if (date) {
            const value = date.getHours()*60 + date.getMinutes();
            // console.log('value:', value);
            if (value !== this.defaultValue) {
                // console.log('not equal to default value', value, this.defaultValue);
                return value;
            }
        }
        return null;
    }
    setValueFromView(viewValue) {
        if (viewValue === null) {
            this.state.parseError2 = false;
            this.resetErrors2();
            if (this.view2) this.view2.setValue(null);
        } else {
            const [h, m] = TimeBox.splitTime(this.defaultValue);
            viewValue.setHours(h, m);
        }
        this.setValue(viewValue);
    }
    onView2Create = view2 => {
        // console.log('RowFormDateTimeFieldController.onView2Create', view2);
        this.view2 = view2;
    }
    _onChange(viewValue) {
        // console.log('RowFormDateTimeFieldController._onChange', this.view2);
        if (viewValue !== null) setTimeout(() => {
            const input = this.view2.getInput();
            input.focus();
            input.setSelectionRange(0, input.value.length);
        }, 0);
    }
    onChange2 = (viewValue, fireEvent = true) => {
        // console.log('RowFormDateTimeFieldController.onChange2', viewValue);
        this.resetErrors2();
        try {
            this.setValueFromView2(viewValue);
        } catch (err) {
            console.log(`${this.model.getFullName()}: cannot parse time: ${err.message}`);
            this.state.parseError2 = true;
        }
        /*
        this.validate2();
        if (this.isValid()) {
            this.copyValueToModel();
        }
        */
        this.refreshChanged();
        if (fireEvent) {
            try {
                this.emit('change', {value: viewValue});
            } catch (err) {
                console.error('unhandled change event error:', this.model.getFullName(), err);
            }
            this.parent.onFieldChange({source: this});
        }
    }
    onBlur2 = (viewValue, fireEvent = false) => {
        // console.log('RowFormDateTimeFieldController.onBlur2', viewValue);
        if (!this.isEditable()) return;
        this.validate2();
        if (this.isValid()) {
            this.copyValueToModel();
        }
        this.refreshChanged();
        if (fireEvent) {
            try {
                this.emit('change', {value: viewValue});
            } catch (err) {
                console.error('unhandled change event error:', this.model.getFullName(), err);
            }
        }
        this.parent.onFieldChange({source: this});
    }
    getPlaceholder2() {
        return TimeBox.getStringValue(this.defaultValue);
    }
    getDefaultValue() {
        return this.defaultValue;
    }
    setDefaultValue(defaultValue) {
        // console.log('RowFormDateTimeFieldController.setDefaultValue', this.view2 ? this.view2.getValue() : null);
        if (typeof defaultValue === 'string') {
            this.defaultValue = TimeBox.getIntegerValue(defaultValue);
        } else {
            if (defaultValue >= 24*60) throw new Error(`wrong default value: ${defaultValue}`);
            this.defaultValue = defaultValue;
        }
        if (this.view2 && this.view2.getValue() === null && this.state.value) {
            this.setValue2(null);
        }
    }
    setValueFromView2(viewValue) {
        if (isNaN(viewValue)) throw new Error('wrong time');
        this.setValue2(viewValue);
    }
    setValue2(viewValue) {
        const value = viewValue !== null ? viewValue : this.defaultValue;
        const [h, m] = TimeBox.splitTime(value);
        this.state.value.setHours(h, m);
    }
    validate2() {
        // console.log('RowFormFieldController.validate', this.model.getFullName());
        this.state.error2 = this.getError2();
    }
    getError2() {
        // console.log('RowFormFieldController.getError', this.model.getFullName());

        // parse validator
        if (this.view2) {
            try {
                const viewValue = this.view2.getValue();
            } catch (err) {
                return `can't parse time: ${err.message}`;
            }
        }

        return null;
    }
    isParseError() {
        return super.isParseError() || this.state.parseError2;
    }
    resetErrors2() {
        this.setError2(null);
        this.state.parseError2 = false;
    }
    setError2(error2) {
        this.state.error2 = error2;
    }
    refill() {
        // console.log('RowFormDateTimeFieldController.refill');
        super.refill();
        this.view2.setValue(this.getValueForTime());
        this.resetErrors2();
        this.refreshChanged();
    }
    isValid() {
        return super.isValid() && this.state.error2 === null;
    }
    getErrorMessage() {
        if (this.state.error === null && this.state.error2 === null) return null;
        return [
            ...(this.state.error  ? [this.state.error]  : []),
            ...(this.state.error2 ? [this.state.error2] : [])
        ].join(', ');
    }
}
<|endoftext|>


<commit_before>var app = angular.module('NoteApp', [
    'ui.router',
    'ui.tinymce'
]);

app.constant('BASE_URL', 'api/notes/');

app.config(function($stateProvider, $urlRouterProvider, $httpProvider) {
    // Set CSRF token cookies to match Django's defaults
    $httpProvider.defaults.xsrfCookieName = 'csrftoken';
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';

    // Set head so Django understands POST format
    $httpProvider.defaults.headers.post['CONTENT-TYPE'] =
        'application/x-www-form-urlencoded';

    // Set up templating URLs
    $stateProvider
        .state('home', {
            url: '/',
            templateUrl: '/static/templates/home.html',
            controller: 'mainController'
        });

    $urlRouterProvider.otherwise('/');
});

app.service('Notes', function($http, BASE_URL) {
    var Notes = {};

    Notes.all = function() {
        return $http.get(BASE_URL);
    };

    Notes.get = function(id) {
        return $http.get(BASE_URL + id + '/');
    };

    Notes.save = function(updatedNote) {
        return $http.put(BASE_URL + updatedNote.id + '/', updatedNote);
    };

    Notes.delete = function(id) {
        return $http.delete(BASE_URL + id + '/');
    };

    Notes.add = function(newNote) {
        return $http.post(BASE_URL, newNote);
    };

    return Notes;
});

app.controller('mainController', function($scope, Notes, $state) {
    $scope.currentNote = {};
    $scope.expandedTagList = [];
    $scope.noteTitleList = [];

    // Refresh list of notes
    updateList = function() {
        return Notes.all().then(function(res) {
            // Sort by tag, then display notes with expanded tags or no tags
            res.data.sort(function(a, b) {
                if (a.tags == null) return 1;
                if (b.tags == null) return -1;
                if (a.tags.toUpperCase() < b.tags.toUpperCase()) {
                    return -1;
                }
                return 1;
            });

            $scope.noteTitleList = [];
            for (var i=0; i < res.data.length; i++) {
                if (!res.data[i].tags) { // doesn't have tag: always display
                    $scope.noteTitleList.push({ title: res.data[i].note_title,
                                                id: res.data[i].id,
                                                type: 'untagged-note' });
                } else { // does have tag: display if expanded
                    var expanded = $scope.expandedTagList.indexOf(res.data[i].tags) > -1;
                    // add tag name if not already in list
                    if (i == 0 || res.data[i].tags != res.data[i-1].tags) {
                        $scope.noteTitleList.push({ title: res.data[i].tags,
                                                    id: 'tag-click:' + res.data[i].tags,
                                                    type: 'tag',
                                                    isExpanded: expanded });
                    }
                    // add note title if it's in an expanded tag
                    if (expanded) {
                        $scope.noteTitleList.push({ title: res.data[i].note_title,
                                                    id: res.data[i].id,
                                                    type: 'tagged-note' });
                    }
                }
            }
        });
    };

    // Handle click on an item (tag or note title) in the list
    $scope.listSelect = function(listID) {
        var s = listID.split(':');
        // A tag has been clicked: toggle category's expansion
        if (s[0] == 'tag-click') {
            var tag = s.slice(1).join(':'); // Rejoin tags that had a colon in them
            tagIndex = $scope.expandedTagList.indexOf(tag);
            if (tagIndex > -1) { // Remove from expandedTagList if already expanded
                $scope.expandedTagList.splice(tagIndex, 1);
            } else {
                $scope.expandedTagList.push(tag);
            }
        // A Note has been clicked: select in editor
        } else {
            $scope.selectNote(listID);
        }
        updateList();
    };

    // Select a note to begin editing
    $scope.selectNote = function(id) {
        Notes.get(id).then(function(res) {
            $scope.currentNote = res.data;
        });
    };

    // Create a blank note object (with no ID) and assign to $scope.currentNote
    $scope.addNote = function() {
        newNote = { note_title: 'A New Note',
                    note_text: '<i>Inspired thoughts go here!<i>' };
        $scope.currentNote = newNote;
        $scope.message = '';
    };

    // Save note on submission of tinyMCE form
    $scope.saveNote = function() {
        $scope.message = 'Note saved';

        // If selected note already exists, update in database
        if ($scope.currentNote.hasOwnProperty('id')) {
            Notes.save($scope.currentNote).then(updateList);
        // If it's a brand new note (not yet in DB), add to database
        } else {
            Notes.add($scope.currentNote).then(function(res) {
                $scope.currentNote = res.data;
                updateList();
            });
        }
    };

    // Delete selected note
    $scope.deleteNote = function() {
        // If default new note is loaded (not in DB), skip
        if (!$scope.currentNote.id) return;

        // Remove from database
        id = $scope.currentNote.id;
        Notes.delete(id);

        // filter the note list to exclude the deleted item
        $scope.noteTitleList = $scope.noteTitleList.filter(function(note) {
            return note.id !== id;
        });

        // create a fresh new note
        $scope.addNote();
        $scope.message = 'Note deleted';
    };

    // On initial run, load the list of user's notes
    // TODO: select the last modified
    updateList().then($scope.addNote);

    // Set up tinyMCE editor controls
    $scope.tinymceModel = "Write your note here!";

    tinySetup = function(ed) {
        console.log("coloring....");
        ed.getBody().style.backgroundColor = '#fff';
    };
    $scope.tinymceOptions = {
        theme: 'modern',
        resize: 'both',
        height: '400px',
        background: 'white',

        plugins: [
          'advlist autolink link lists charmap preview hr anchor pagebreak',
          'searchreplace wordcount visualchars code fullscreen insertdatetime nonbreaking emoticons template paste textcolor save'
        ],

        toolbar: 'save undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | forecolor backcolor emoticons',

        // Call save function when editor's Save button is clicked
        save_onsavecallback: function() { $scope.saveNote(); },

        // allow users to save even when no changes have been made in tinyMCE editor (since title might've been changed)
        save_enablewhendirty: false,

        // Match up styling with the rest of page
        content_css: stylesheetPath,

        setup: function (ed) {
            ed.on('init', function(e) {
                tinymce.activeEditor.contentDocument.body.style.backgroundColor = '#fff';
            });
        }
    };
});
<commit_msg>improved delete functionality: initial delete moves to Trash tag<commit_after>var app = angular.module('NoteApp', [
    'ui.router',
    'ui.tinymce'
]);

app.constant('BASE_URL', 'api/notes/');

app.config(function($stateProvider, $urlRouterProvider, $httpProvider) {
    // Set CSRF token cookies to match Django's defaults
    $httpProvider.defaults.xsrfCookieName = 'csrftoken';
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';

    // Set head so Django understands POST format
    $httpProvider.defaults.headers.post['CONTENT-TYPE'] =
        'application/x-www-form-urlencoded';

    // Set up templating URLs
    $stateProvider
        .state('home', {
            url: '/',
            templateUrl: '/static/templates/home.html',
            controller: 'mainController'
        });

    $urlRouterProvider.otherwise('/');
});

app.service('Notes', function($http, BASE_URL) {
    var Notes = {};

    Notes.all = function() {
        return $http.get(BASE_URL);
    };

    Notes.get = function(id) {
        return $http.get(BASE_URL + id + '/');
    };

    Notes.save = function(updatedNote) {
        return $http.put(BASE_URL + updatedNote.id + '/', updatedNote);
    };

    Notes.delete = function(id) {
        return $http.delete(BASE_URL + id + '/');
    };

    Notes.add = function(newNote) {
        return $http.post(BASE_URL, newNote);
    };

    return Notes;
});

app.controller('mainController', function($scope, Notes, $state) {
    $scope.currentNote = {};
    $scope.expandedTagList = [];
    $scope.noteTitleList = [];

    // Refresh list of notes
    updateList = function() {
        return Notes.all().then(function(res) {
            // Sort by tag, then display notes with expanded tags or no tags
            res.data.sort(function(a, b) {
                if (a.tags == null) return 1;
                if (b.tags == null) return -1;
                if (a.tags.toUpperCase() < b.tags.toUpperCase()) {
                    return -1;
                }
                return 1;
            });

            $scope.noteTitleList = [];
            for (var i=0; i < res.data.length; i++) {
                if (!res.data[i].tags) { // doesn't have tag: always display
                    $scope.noteTitleList.push({ title: res.data[i].note_title,
                                                id: res.data[i].id,
                                                type: 'untagged-note' });
                } else { // does have tag: display if expanded
                    var expanded = $scope.expandedTagList.indexOf(res.data[i].tags) > -1;
                    // add tag name if not already in list
                    if (i == 0 || res.data[i].tags != res.data[i-1].tags) {
                        $scope.noteTitleList.push({ title: res.data[i].tags,
                                                    id: 'tag-click:' + res.data[i].tags,
                                                    type: 'tag',
                                                    isExpanded: expanded });
                    }
                    // add note title if it's in an expanded tag
                    if (expanded) {
                        $scope.noteTitleList.push({ title: res.data[i].note_title,
                                                    id: res.data[i].id,
                                                    type: 'tagged-note' });
                    }
                }
            }
        });
    };

    // Handle click on an item (tag or note title) in the list
    $scope.listSelect = function(listID) {
        var s = listID.split(':');
        // A tag has been clicked: toggle category's expansion
        if (s[0] == 'tag-click') {
            var tag = s.slice(1).join(':'); // Rejoin tags that had a colon in them
            tagIndex = $scope.expandedTagList.indexOf(tag);
            if (tagIndex > -1) { // Remove from expandedTagList if already expanded
                $scope.expandedTagList.splice(tagIndex, 1);
            } else {
                $scope.expandedTagList.push(tag);
            }
        // A Note has been clicked: select in editor
        } else {
            $scope.selectNote(listID);
        }
        updateList();
    };

    // Select a note to begin editing
    $scope.selectNote = function(id) {
        Notes.get(id).then(function(res) {
            $scope.currentNote = res.data;
        });
    };

    // Create a blank note object (with no ID) and assign to $scope.currentNote
    $scope.addNote = function() {
        newNote = { note_title: 'A New Note',
                    note_text: '<i>Inspired thoughts go here!<i>' };
        $scope.currentNote = newNote;
        $scope.message = '';
    };

    // Save currently selected note on submission of tinyMCE form
    $scope.saveNote = function() {
        $scope.message = 'Note saved';

        // If selected note already exists, update in database
        if ($scope.currentNote.hasOwnProperty('id')) {
            Notes.save($scope.currentNote).then(updateList);
        // If it's a brand new note (not yet in DB), add to database
        } else {
            Notes.add($scope.currentNote).then(function(res) {
                $scope.currentNote = res.data;
                updateList();
            });
        }
    };

    // Delete selected note
    $scope.deleteNote = function() {
        // If default new note is loaded (not in DB), skip
        if (!$scope.currentNote.id) return;

        // Move note to Trash (if not already)
        var originalTag = $scope.currentNote.tags;
        $scope.currentNote.tags = 'Trash';
        $scope.saveNote();

        // If already in Trash, delete from database
        if (originalTag == 'Trash') {
            id = $scope.currentNote.id;
            Notes.delete(id).then(updateList);
        }

        // create a fresh new note
        $scope.addNote();
        $scope.message = 'Note deleted';
    };

    // On initial run, load the list of user's notes
    // TODO: select the last modified
    updateList().then($scope.addNote);

    // Set up tinyMCE editor controls
    $scope.tinymceModel = '';
    $scope.tinymceOptions = {
        theme: 'modern',
        resize: 'both',
        height: '400px',
        background: 'white',

        plugins: [
          'advlist autolink link lists charmap preview hr anchor pagebreak',
          'searchreplace wordcount visualchars code fullscreen insertdatetime nonbreaking emoticons template paste textcolor save'
        ],

        toolbar: 'save undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | forecolor backcolor emoticons',

        // Call save function when editor's Save button is clicked
        save_onsavecallback: function() { $scope.saveNote(); },

        // allow users to save even when no changes have been made in tinyMCE editor (since title might've been changed)
        save_enablewhendirty: false,

        // Match up styling with the rest of page
        content_css: stylesheetPath,

        setup: function (ed) {
            ed.on('init', function(e) {
                tinymce.activeEditor.contentDocument.body.style.backgroundColor = '#fff';
            });
        }
    };
});
<|endoftext|>


<commit_before>var path = require('path');
var webpack = require('webpack');
var Mix = require('laravel-mix').config;
var plugins = require('laravel-mix').plugins;

/*
 |--------------------------------------------------------------------------
 | Mix Initialization
 |--------------------------------------------------------------------------
 |
 | As our first step, we'll require the project's Laravel Mix file
 | and record the user's requested compilation and build steps.
 | Once those steps have been recorded, we may get to work.
 |
 */

Mix.finalize();

/*
 |--------------------------------------------------------------------------
 | Webpack Context
 |--------------------------------------------------------------------------
 |
 | This prop will determine the appropriate context, when running Webpack.
 | Since you have the option of publishing this webpack.config.js file
 | to your project root, we will dynamically set the path for you.
 |
 */

module.exports.context = Mix.contextPath();

/*
 |--------------------------------------------------------------------------
 | Webpack Entry
 |--------------------------------------------------------------------------
 |
 | We'll first specify the entry point for Webpack. By default, we'll
 | assume a single bundled file, but you may call Mix.extract()
 | to make a separate bundle specifically for vendor libraries.
 |
 */

module.exports.entry = Mix.entry();

if (Mix.js.vendor) {
    module.exports.entry.vendor = Mix.js.vendor;
}

/*
 |--------------------------------------------------------------------------
 | Webpack Output
 |--------------------------------------------------------------------------
 |
 | Webpack naturally requires us to specify our desired output path and
 | file name. We'll simply echo what you passed to with Mix.js().
 | Note that, for Mix.version(), we'll properly hash the file.
 |
 */

module.exports.output = Mix.output();

/*
 |--------------------------------------------------------------------------
 | Rules
 |--------------------------------------------------------------------------
 |
 | Webpack rules allow us to register any number of loaders and options.
 | Out of the box, we'll provide a handful to get you up and running
 | as quickly as possible, though feel free to add to this list.
 |
 */

module.exports.module = {
    rules: [
        {
            test: /\.vue$/,
            loader: 'vue-loader',
            options: {
                loaders: {
                    js: 'babel-loader' + Mix.babelConfig()
                },

                postcss: [
                    require('autoprefixer')
                ]
            }
        },

        {
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            loader: 'babel-loader' + Mix.babelConfig()
        },

        {
            test: /\.(png|jpg|gif)$/,
            loader: 'file-loader',
            options: {
                name: '[name].[ext]?[hash]'
            }
        },

        {
            test: /\.(woff2?|ttf|eot|svg)$/,
            loader: 'file-loader',
            options: {
                name: '/fonts/[name].[ext]?[hash]'
            }
        }
    ]
};

if (Mix.sass) {
    module.exports.module.rules.push({
        test: /\.s[ac]ss$/,
        loader: plugins.ExtractTextPlugin.extract({
            fallbackLoader: 'style-loader',
            loader: [
                'css-loader', 'postcss-loader',
                'resolve-url-loader', 'sass-loader?sourceMap'
            ]
        })
    });
}

if (Mix.less) {
    module.exports.module.rules.push({
        test: /\.less$/,
        loader: plugins.ExtractTextPlugin.extract({
            fallbackLoader: 'style-loader',
            loader: ['css-loader', 'postcss-loader', 'less-loader']
        })
    });
}

/*
 |--------------------------------------------------------------------------
 | Resolve
 |--------------------------------------------------------------------------
 |
 | Here, we may set any options/aliases that affect Webpack's resolving
 | of modules. To begin, we will provide the necessary Vue alias to
 | load the Vue common library. You may delete this, if needed.
 |
 */

module.exports.resolve = {
    extensions: ['*', '.js', '.jsx', '.vue'],

    alias: {
        'vue$': 'vue/dist/vue.common.js'
    }
};

/*
 |--------------------------------------------------------------------------
 | Stats
 |--------------------------------------------------------------------------
 |
 | By default, Webpack spits a lot of information out to the terminal,
 | each you time you compile. Let's keep things a bit more minimal
 | and hide a few of those bits and pieces. Adjust as you wish.
 |
 */

module.exports.stats = {
    hash: false,
    version: false,
    timings: false,
    children: false
};

module.exports.performance = { hints: false};

/*
 |--------------------------------------------------------------------------
 | Devtool
 |--------------------------------------------------------------------------
 |
 | Sourcemaps allow us to access our original source code within the
 | browser, even if we're serving a bundled script or stylesheet.
 | You may activate sourcemaps, by adding Mix.sourceMaps().
 |
 */

module.exports.devtool = Mix.sourcemaps;

/*
 |--------------------------------------------------------------------------
 | Webpack Dev Server Configuration
 |--------------------------------------------------------------------------
 |
 | If you want to use that flashy hot module replacement feature, then
 | we've got you covered. Here, we'll set some basic initial config
 | for the Node server. You very likely won't want to edit this.
 |
 */
module.exports.devServer = {
    historyApiFallback: true,
    noInfo: true
};

/*
 |--------------------------------------------------------------------------
 | Plugins
 |--------------------------------------------------------------------------
 |
 | Lastly, we'll register a number of plugins to extend and configure
 | Webpack. To get you started, we've included a handful of useful
 | extensions, for versioning, OS notifications, and much more.
 |
 */

module.exports.plugins = [];

if (Mix.notifications) {
    module.exports.plugins.push(
        new plugins.WebpackNotifierPlugin({
            title: 'Laravel Mix',
            alwaysNotify: true,
            contentImage: 'node_modules/laravel-mix/icons/laravel.png'
        })
    );
}

module.exports.plugins.push(
    function() {
        this.plugin('done', stats => Mix.manifest.write(stats));
    }
);

module.exports.plugins.push(
    new webpack.LoaderOptionsPlugin({
        minimize: Mix.inProduction,
        options: {
            postcss: [
                require('autoprefixer')
            ],
            context: __dirname,
            output: { path: './' }
        }
    })
);

if (Mix.versioning.enabled) {
    Mix.versioning.record();

    module.exports.plugins.push(
        new plugins.WebpackOnBuildPlugin(() => {
            Mix.versioning.prune(Mix.publicPath);
        })
    );
}

if (Mix.combine || Mix.minify) {
    module.exports.plugins.push(
        new plugins.WebpackOnBuildPlugin(() => {
            Mix.concatenateAll().minifyAll();
        })
    );
}

if (Mix.copy) {
    Mix.copy.forEach(copy => {
        module.exports.plugins.push(
            new plugins.CopyWebpackPlugin([copy])
        );
    });
}

if (Mix.js.vendor) {
    module.exports.plugins.push(
        new webpack.optimize.CommonsChunkPlugin({
            names: ['vendor', 'manifest']
        })
    );
}

if (Mix.cssPreprocessor) {
    module.exports.plugins.push(
        new plugins.ExtractTextPlugin({
            filename: Mix.cssOutput()
        })
    );
}

if (Mix.inProduction) {
    module.exports.plugins = module.exports.plugins.concat([
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: '"production"'
            }
        }),

        new webpack.optimize.UglifyJsPlugin({
            sourceMap: true,
            compress: {
                warnings: false
            }
        })
    ]);
}
<commit_msg>Move things around<commit_after>var path = require('path');
var webpack = require('webpack');
var Mix = require('laravel-mix').config;
var plugins = require('laravel-mix').plugins;

/*
 |--------------------------------------------------------------------------
 | Mix Initialization
 |--------------------------------------------------------------------------
 |
 | As our first step, we'll require the project's Laravel Mix file
 | and record the user's requested compilation and build steps.
 | Once those steps have been recorded, we may get to work.
 |
 */

Mix.finalize();

/*
 |--------------------------------------------------------------------------
 | Webpack Context
 |--------------------------------------------------------------------------
 |
 | This prop will determine the appropriate context, when running Webpack.
 | Since you have the option of publishing this webpack.config.js file
 | to your project root, we will dynamically set the path for you.
 |
 */

module.exports.context = Mix.contextPath();

/*
 |--------------------------------------------------------------------------
 | Webpack Entry
 |--------------------------------------------------------------------------
 |
 | We'll first specify the entry point for Webpack. By default, we'll
 | assume a single bundled file, but you may call Mix.extract()
 | to make a separate bundle specifically for vendor libraries.
 |
 */

module.exports.entry = Mix.entry();

if (Mix.js.vendor) {
    module.exports.entry.vendor = Mix.js.vendor;
}

/*
 |--------------------------------------------------------------------------
 | Webpack Output
 |--------------------------------------------------------------------------
 |
 | Webpack naturally requires us to specify our desired output path and
 | file name. We'll simply echo what you passed to with Mix.js().
 | Note that, for Mix.version(), we'll properly hash the file.
 |
 */

module.exports.output = Mix.output();

/*
 |--------------------------------------------------------------------------
 | Rules
 |--------------------------------------------------------------------------
 |
 | Webpack rules allow us to register any number of loaders and options.
 | Out of the box, we'll provide a handful to get you up and running
 | as quickly as possible, though feel free to add to this list.
 |
 */

module.exports.module = {
    rules: [
        {
            test: /\.vue$/,
            loader: 'vue-loader',
            options: {
                loaders: {
                    js: 'babel-loader' + Mix.babelConfig()
                },

                postcss: [
                    require('autoprefixer')
                ]
            }
        },

        {
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            loader: 'babel-loader' + Mix.babelConfig()
        },

        {
            test: /\.(png|jpg|gif)$/,
            loader: 'file-loader',
            options: {
                name: '[name].[ext]?[hash]'
            }
        },

        {
            test: /\.(woff2?|ttf|eot|svg)$/,
            loader: 'file-loader',
            options: {
                name: '/fonts/[name].[ext]?[hash]'
            }
        }
    ]
};

if (Mix.sass) {
    module.exports.module.rules.push({
        test: /\.s[ac]ss$/,
        loader: plugins.ExtractTextPlugin.extract({
            fallbackLoader: 'style-loader',
            loader: [
                'css-loader', 'postcss-loader',
                'resolve-url-loader', 'sass-loader?sourceMap'
            ]
        })
    });
}

if (Mix.less) {
    module.exports.module.rules.push({
        test: /\.less$/,
        loader: plugins.ExtractTextPlugin.extract({
            fallbackLoader: 'style-loader',
            loader: ['css-loader', 'postcss-loader', 'less-loader']
        })
    });
}

/*
 |--------------------------------------------------------------------------
 | Resolve
 |--------------------------------------------------------------------------
 |
 | Here, we may set any options/aliases that affect Webpack's resolving
 | of modules. To begin, we will provide the necessary Vue alias to
 | load the Vue common library. You may delete this, if needed.
 |
 */

module.exports.resolve = {
    extensions: ['*', '.js', '.jsx', '.vue'],

    alias: {
        'vue$': 'vue/dist/vue.common.js'
    }
};

/*
 |--------------------------------------------------------------------------
 | Stats
 |--------------------------------------------------------------------------
 |
 | By default, Webpack spits a lot of information out to the terminal,
 | each you time you compile. Let's keep things a bit more minimal
 | and hide a few of those bits and pieces. Adjust as you wish.
 |
 */

module.exports.stats = {
    hash: false,
    version: false,
    timings: false,
    children: false
};

module.exports.performance = { hints: false};

/*
 |--------------------------------------------------------------------------
 | Devtool
 |--------------------------------------------------------------------------
 |
 | Sourcemaps allow us to access our original source code within the
 | browser, even if we're serving a bundled script or stylesheet.
 | You may activate sourcemaps, by adding Mix.sourceMaps().
 |
 */

module.exports.devtool = Mix.sourcemaps;

/*
 |--------------------------------------------------------------------------
 | Webpack Dev Server Configuration
 |--------------------------------------------------------------------------
 |
 | If you want to use that flashy hot module replacement feature, then
 | we've got you covered. Here, we'll set some basic initial config
 | for the Node server. You very likely won't want to edit this.
 |
 */
module.exports.devServer = {
    historyApiFallback: true,
    noInfo: true
};

/*
 |--------------------------------------------------------------------------
 | Plugins
 |--------------------------------------------------------------------------
 |
 | Lastly, we'll register a number of plugins to extend and configure
 | Webpack. To get you started, we've included a handful of useful
 | extensions, for versioning, OS notifications, and much more.
 |
 */

module.exports.plugins = [];

module.exports.plugins.push(
    function() {
        this.plugin('done', stats => Mix.manifest.write(stats));
    }
);

module.exports.plugins.push(
    new webpack.LoaderOptionsPlugin({
        minimize: Mix.inProduction,
        options: {
            postcss: [
                require('autoprefixer')
            ],
            context: __dirname,
            output: { path: './' }
        }
    })
);

if (Mix.notifications) {
    module.exports.plugins.push(
        new plugins.WebpackNotifierPlugin({
            title: 'Laravel Mix',
            alwaysNotify: true,
            contentImage: 'node_modules/laravel-mix/icons/laravel.png'
        })
    );
}

if (Mix.versioning.enabled) {
    Mix.versioning.record();

    module.exports.plugins.push(
        new plugins.WebpackOnBuildPlugin(() => {
            Mix.versioning.prune(Mix.publicPath);
        })
    );
}

if (Mix.combine || Mix.minify) {
    module.exports.plugins.push(
        new plugins.WebpackOnBuildPlugin(() => {
            Mix.concatenateAll().minifyAll();
        })
    );
}

if (Mix.copy) {
    Mix.copy.forEach(copy => {
        module.exports.plugins.push(
            new plugins.CopyWebpackPlugin([copy])
        );
    });
}

if (Mix.js.vendor) {
    module.exports.plugins.push(
        new webpack.optimize.CommonsChunkPlugin({
            names: ['vendor', 'manifest']
        })
    );
}

if (Mix.cssPreprocessor) {
    module.exports.plugins.push(
        new plugins.ExtractTextPlugin({
            filename: Mix.cssOutput()
        })
    );
}

if (Mix.inProduction) {
    module.exports.plugins = module.exports.plugins.concat([
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: '"production"'
            }
        }),

        new webpack.optimize.UglifyJsPlugin({
            sourceMap: true,
            compress: {
                warnings: false
            }
        })
    ]);
}
<|endoftext|>


<commit_before>"use strict"

define(['jquery', 'Bacon', 'consts', 'rz_bus', 'autocomplete', 'rz_core', 'textanalysis'],
function($,        Bacon,   consts,   rz_bus,   autocomplete,   rz_core,   textanalysis) {

var text = "", // Last text of sentence
    element_name = '#textanalyser',
    element = $(element_name),
    suggestionChange,
    plus_button = $('.add-button');

var typeselection = function TypeSelectionDialog() {
    var e = $('.typeselection'),
        typeselection = {};

    typeselection.analysisNodeStart = function() {
        typeselection.show();
        e.html('

Use [TAB] key to pick a type

');
    }
    typeselection.show = function() {
        e.css({
            top: window.innerHeight/2-115,
            left: window.innerWidth/2-325});
    }
    typeselection.hide = function() {
        e.css('top', -300);
        e.css('left', 0);
    }
    typeselection.showChosenType = function(nodetype) {
        e.html('

' + "Chosen Type: " + nodetype + '

');
    }
    return typeselection;
}();

function analyzeSentence(sentence, finalize)
{
    var ret = textanalysis.textAnalyser(sentence, finalize);

    switch (ret.state) {
    case textanalysis.ANALYSIS_NODE_START:
        typeselection.analysisNodeStart();
        break;
    case textanalysis.ANALYSIS_LINK:
        typeselection.hide();
        break;
    }

    var backend_commit = false;
    ret.applyToGraph(rz_core.graph, backend_commit);

    if (finalize || sentence.length == 0) {
        typeselection.hide();
        $('span.ui-helper-hidden-accessible').hide();
    } else {
        $('span.ui-helper-hidden-accessible').show();
    }
}

function textSelect(inp, s, e) {
    e = e || s;
    if (inp.createTextRange) {
        var r = inp.createTextRange();
        r.collapse(true);
        r.moveEnd('character', e);
        r.moveStart('character', s);
        r.select();
    }else if(inp.setSelectionRange) {
        inp.focus();
        inp.setSelectionRange(s, e);
    }
}

function changeType(arg) {
    var lastnode = textanalysis.lastnode(),
        nodetype,
        id;

    if(!lastnode) {
        id = "new node";
    } else {
        id = lastnode.id;
    }
    nodetype = (arg === 'up'? textanalysis.selected_type_next() : textanalysis.selected_type_prev());

    if (arg === 'up') {
        rz_core.graph.editType(id, null, nodetype);
        typeselection.showChosenType(nodetype);
        rz_core.graph.findCoordinates(id, null);
    } else {
        rz_core.graph.editType(id, null, nodetype);
        typeselection.showChosenType(nodetype);
        rz_core.graph.findCoordinates(id, null);
    }
    rz_core.update_view__graph(true);
}

return {
    analyzeSentence: analyzeSentence,
    main:function () {
        if (element.length != 1) {
            return;
        }

        element.autocompleteTrigger({
            triggerStart: '#',
            triggerEnd: '',
            source: textanalysis.autocompleteCallback,
            response: function(element, ui) {
                ui.content.forEach(function (x) {
                    if (x.value.search(' ') != -1) {
                        x.value = '"' + x.value + '"';
                    }
                });
            },
            open: function() {
                $('.ui-autocomplete').css('width', '10px');
            },
        });

        var document_keypress = new Bacon.Bus();
        rz_bus.ui_key.plug(document_keypress);

        element.keydown(function(e) {
            var ret = undefined;

            switch (e.keyCode) {
            case 9: //TAB
                if (textanalysis.lastnode()) {
                    e.preventDefault();
                    changeType(e.shiftKey ? "up" : "down", textanalysis.lastnode());
                    ret = false;
                }
                break;
            case 37: //UP
                $('html, body').scrollLeft(0);
                break;
            case 39: //DOWN
                $('html, body').scrollLeft(0);
                break;
            case 38: //UP
                suggestionChange = true;
                break;
            case 40: //DOWN
                suggestionChange = true;
                break;
            }
            document_keypress.push({where: consts.KEYSTROKE_WHERE_DOCUMENT, keys: [e.keyCode]});
            return ret;
        });

        function maybeSubmitNewSentence() {
            if(!suggestionChange) {
                text = element.val();
                element.val("");
                analyzeSentence(text, true);
            } else {
                suggestionChange = false;
            }
        }

        plus_button.bind("click", maybeSubmitNewSentence);

        var element_keypress = new Bacon.Bus();
        rz_bus.ui_key.plug(element_keypress);
        element.keypress(function(e) {
            var ret = undefined;
            switch (e.which) {
            case 13:
                maybeSubmitNewSentence();
                ret = false;
                break;
            case 37: //RIGHT
                $('body').scrollLeft(0);
                e.stopPropagation();
                ret = false;
                break;
            case 39: //LEFT
                $('body').scrollLeft(0);
                e.stopPropagation();
                ret = false;
                break;
            }
            element_keypress.push({where: consts.KEYSTROKE_WHERE_TEXTANALYSIS, keys:[e.which]});
            return ret;
        });

        var input = new Bacon.Bus();
        rz_bus.ui_input.plug(input);
        if ('oninput' in document.documentElement) {
            element.on('input', function(e) {
                text = element.val();
                analyzeSentence(text, false);
                input.push({where: consts.INPUT_WHERE_TEXTANALYSIS, input: text});
            });
        } else {
            console.log('textanalysis.ui: fallback to polling');
            window.setInterval(function() {
                if (element.val() != text) {
                    if (text.length * 8 > 500) {
                        element.css('width', text.length * 8 + 20);
                    }
                    // text changed
                    text = element.val();
                    analyzeSentence(text, false);
                    suggestionChange = false;
                }
            }, 50);
        }
    }
};
}); // define
<commit_msg>Fixes #178: handle clicks on the plus button<commit_after>"use strict"

define(['jquery', 'Bacon', 'consts', 'rz_bus', 'autocomplete', 'rz_core', 'textanalysis'],
function($,        Bacon,   consts,   rz_bus,   autocomplete,   rz_core,   textanalysis) {

var text = "", // Last text of sentence
    element_name = '#textanalyser',
    element = $(element_name),
    suggestionChange,
    plus_button = $('.add-button');

var typeselection = function TypeSelectionDialog() {
    var e = $('.typeselection'),
        typeselection = {};

    typeselection.analysisNodeStart = function() {
        typeselection.show();
        e.html('

Use [TAB] key to pick a type

');
    }
    typeselection.show = function() {
        e.css({
            top: window.innerHeight/2-115,
            left: window.innerWidth/2-325});
    }
    typeselection.hide = function() {
        e.css('top', -300);
        e.css('left', 0);
    }
    typeselection.showChosenType = function(nodetype) {
        e.html('

' + "Chosen Type: " + nodetype + '

');
    }
    return typeselection;
}();

function analyzeSentence(sentence, finalize)
{
    var ret = textanalysis.textAnalyser(sentence, finalize);

    switch (ret.state) {
    case textanalysis.ANALYSIS_NODE_START:
        typeselection.analysisNodeStart();
        break;
    case textanalysis.ANALYSIS_LINK:
        typeselection.hide();
        break;
    }

    var backend_commit = false;
    ret.applyToGraph(rz_core.graph, backend_commit);

    if (finalize || sentence.length == 0) {
        typeselection.hide();
        $('span.ui-helper-hidden-accessible').hide();
    } else {
        $('span.ui-helper-hidden-accessible').show();
    }
}

function textSelect(inp, s, e) {
    e = e || s;
    if (inp.createTextRange) {
        var r = inp.createTextRange();
        r.collapse(true);
        r.moveEnd('character', e);
        r.moveStart('character', s);
        r.select();
    }else if(inp.setSelectionRange) {
        inp.focus();
        inp.setSelectionRange(s, e);
    }
}

function changeType(arg) {
    var lastnode = textanalysis.lastnode(),
        nodetype,
        id;

    if(!lastnode) {
        id = "new node";
    } else {
        id = lastnode.id;
    }
    nodetype = (arg === 'up'? textanalysis.selected_type_next() : textanalysis.selected_type_prev());

    if (arg === 'up') {
        rz_core.graph.editType(id, null, nodetype);
        typeselection.showChosenType(nodetype);
        rz_core.graph.findCoordinates(id, null);
    } else {
        rz_core.graph.editType(id, null, nodetype);
        typeselection.showChosenType(nodetype);
        rz_core.graph.findCoordinates(id, null);
    }
    rz_core.update_view__graph(true);
}

return {
    analyzeSentence: analyzeSentence,
    main:function () {
        if (element.length != 1) {
            return;
        }

        element.autocompleteTrigger({
            triggerStart: '#',
            triggerEnd: '',
            source: textanalysis.autocompleteCallback,
            response: function(element, ui) {
                ui.content.forEach(function (x) {
                    if (x.value.search(' ') != -1) {
                        x.value = '"' + x.value + '"';
                    }
                });
            },
            open: function() {
                $('.ui-autocomplete').css('width', '10px');
            },
        });

        var document_keypress = new Bacon.Bus();
        rz_bus.ui_key.plug(document_keypress);

        element.keydown(function(e) {
            var ret = undefined;

            switch (e.keyCode) {
            case 9: //TAB
                if (textanalysis.lastnode()) {
                    e.preventDefault();
                    changeType(e.shiftKey ? "up" : "down", textanalysis.lastnode());
                    ret = false;
                }
                break;
            case 37: //UP
                $('html, body').scrollLeft(0);
                break;
            case 39: //DOWN
                $('html, body').scrollLeft(0);
                break;
            case 38: //UP
                suggestionChange = true;
                break;
            case 40: //DOWN
                suggestionChange = true;
                break;
            }
            document_keypress.push({where: consts.KEYSTROKE_WHERE_DOCUMENT, keys: [e.keyCode]});
            return ret;
        });

        function maybeSubmitNewSentence() {
            if(!suggestionChange) {
                text = element.val();
                element.val("");
                analyzeSentence(text, true);
                text = "";
            } else {
                suggestionChange = false;
            }
        }

        // Click is required to prevent the default action - this is a form so that's a post,
        // and away we go.
        // The mousedown is required because CSS3 transitions eat some events sometimes. This is
        // the closest I've come to an explanation:
        //   http://stackoverflow.com/questions/15786891/browser-sometimes-ignores-a-jquery-click-event-during-a-css3-transform
        plus_button.bind("click mousedown", function(e) {
            console.dir(e);
            maybeSubmitNewSentence();
            e.preventDefault();
        });

        var element_keypress = new Bacon.Bus();
        rz_bus.ui_key.plug(element_keypress);
        element.keypress(function(e) {
            var ret = undefined;
            switch (e.which) {
            case 13:
                maybeSubmitNewSentence();
                ret = false;
                break;
            case 37: //RIGHT
                $('body').scrollLeft(0);
                e.stopPropagation();
                ret = false;
                break;
            case 39: //LEFT
                $('body').scrollLeft(0);
                e.stopPropagation();
                ret = false;
                break;
            }
            element_keypress.push({where: consts.KEYSTROKE_WHERE_TEXTANALYSIS, keys:[e.which]});
            return ret;
        });

        var input = new Bacon.Bus();
        rz_bus.ui_input.plug(input);
        if ('oninput' in document.documentElement) {
            element.on('input', function(e) {
                text = element.val();
                analyzeSentence(text, false);
                input.push({where: consts.INPUT_WHERE_TEXTANALYSIS, input: text});
            });
        } else {
            console.log('textanalysis.ui: fallback to polling');
            window.setInterval(function() {
                if (element.val() != text) {
                    if (text.length * 8 > 500) {
                        element.css('width', text.length * 8 + 20);
                    }
                    // text changed
                    text = element.val();
                    analyzeSentence(text, false);
                    suggestionChange = false;
                }
            }, 50);
        }
    }
};
}); // define
<|endoftext|>