From 0d605d4722a31b16c85d589af7f6c2b5e1f3222f Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 25 Jul 2016 15:10:34 -0500 Subject: [PATCH 1/5] Started working on options parser --- helpers/odmOptionsToJson.py | 38 +++++++++++++++++++ index.js | 6 ++- libs/Task.js | 2 +- libs/odmOptionsParser.js | 75 +++++++++++++++++++++++++++++++++++++ libs/odmRunner.js | 25 +++++++++++++ 5 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 helpers/odmOptionsToJson.py create mode 100644 libs/odmOptionsParser.js diff --git a/helpers/odmOptionsToJson.py b/helpers/odmOptionsToJson.py new file mode 100644 index 0000000..578d3ce --- /dev/null +++ b/helpers/odmOptionsToJson.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +''' +Node-OpenDroneMap Node.js App and REST API to access OpenDroneMap. +Copyright (C) 2016 Node-OpenDroneMap Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +''' + +import sys +import imp +import argparse +import json + +imp.load_source('context', sys.argv[2] + '/opendm/context.py') +odm = imp.load_source('config', sys.argv[2] + '/opendm/config.py') + +options = {} +class ArgumentParserStub(argparse.ArgumentParser): + def add_argument(self, *args, **kwargs): + argparse.ArgumentParser.add_argument(self, *args, **kwargs) + options[args[0]] = {} + for name, value in kwargs.items(): + options[args[0]][str(name)] = str(value) + +odm.parser = ArgumentParserStub() +odm.config() +print json.dumps(options) diff --git a/index.js b/index.js index 2d9d3ec..ce0149f 100644 --- a/index.js +++ b/index.js @@ -158,7 +158,7 @@ process.on ('SIGINT', gracefulShutdown); // Startup let taskManager; let server; - +/* async.series([ cb => { taskManager = new TaskManager(cb); }, cb => { server = app.listen(3000, err => { @@ -168,4 +168,6 @@ async.series([ } ], err => { if (err) console.log("Error during startup: " + err.message); -}); +});*/ +let odmOptionsParser = require('./libs/odmOptionsParser'); +odmOptionsParser.getOptions(function(){}); diff --git a/libs/Task.js b/libs/Task.js index 9a3eea6..ff5c077 100644 --- a/libs/Task.js +++ b/libs/Task.js @@ -137,7 +137,7 @@ module.exports = class Task{ this.setStatus(statusCodes.CANCELED); if (wasRunning && this.runnerProcess){ - // TODO: this does guarantee that + // TODO: this does NOT guarantee that // the process will immediately terminate. // In fact, often times ODM will continue running for a while // This might need to be fixed on ODM's end. diff --git a/libs/odmOptionsParser.js b/libs/odmOptionsParser.js new file mode 100644 index 0000000..8329db9 --- /dev/null +++ b/libs/odmOptionsParser.js @@ -0,0 +1,75 @@ +/* +Node-OpenDroneMap Node.js App and REST API to access OpenDroneMap. +Copyright (C) 2016 Node-OpenDroneMap Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +"use strict"; +let odmRunner = require('./odmRunner'); + +module.exports = { + getOptions: function(done){ + odmRunner.getJsonOptions((err, json) => { + if (err) done(err); + else{ + for (let option in json){ + if (option === "-h") continue; + let values = json[option]; + + let type = ""; + let defaultValue = ""; + let help = values.help || ""; + let range = values.metavar.replace(/[<>]/g, "").trim(); + + switch(values.type.trim()){ + case "": + type = "int"; + defaultValue = values['default'] !== undefined ? + parseInt(values['default']) : + 0; + break; + case "": + type = "float"; + defaultValue = values['default'] !== undefined ? + parseFloat(values['default']) : + 0.0; + break; + default: + type = "string"; + defaultValue = values['default'].trim(); + } + + if (values['default'] === "True"){ + type = "bool"; + defaultValue = true; + }else if (values['default'] === "False"){ + type = "bool"; + defaultValue = false; + } + + + + let result = { + type, defaultValue, range, help + }; + + console.log(values); + console.log(result); + console.log('-----'); + } + done(); + } + }); + } +}; diff --git a/libs/odmRunner.js b/libs/odmRunner.js index 6cd41f7..281433c 100644 --- a/libs/odmRunner.js +++ b/libs/odmRunner.js @@ -44,5 +44,30 @@ module.exports = { }); return childProcess; + }, + + getJsonOptions: function(done){ + // Launch + let childProcess = spawn("python", [`${__dirname}/../helpers/odmOptionsToJson.py`, + "--project-path", ODM_PATH]); + let output = []; + + childProcess + .on('exit', (code, signal) => { + try{ + let json = JSON.parse(output.join("")); + done(null, json); + }catch(err){ + done(err); + } + }) + .on('error', done); + + let processOutput = chunk => { + output.push(chunk.toString()); + }; + + childProcess.stdout.on('data', processOutput); + childProcess.stderr.on('data', processOutput); } }; From a769b5425f0fa617447291045a382773010b10b7 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 25 Jul 2016 20:10:18 -0500 Subject: [PATCH 2/5] Options retrieval working --- index.js | 14 +- libs/odmOptionsParser.js | 46 +- public/css/main.css | 4 + public/css/selectric.css | 198 +++++++ public/index.html | 15 +- public/js/jquery.selectric.js | 911 ++++++++++++++++++++++++++++++ public/js/jquery.selectric.min.js | 2 + public/js/main.js | 35 +- 8 files changed, 1199 insertions(+), 26 deletions(-) create mode 100644 public/css/selectric.css create mode 100644 public/js/jquery.selectric.js create mode 100644 public/js/jquery.selectric.min.js diff --git a/index.js b/index.js index ce0149f..a23f4ea 100644 --- a/index.js +++ b/index.js @@ -29,6 +29,7 @@ let bodyParser = require('body-parser'); let morgan = require('morgan'); let TaskManager = require('./libs/taskManager'); let Task = require('./libs/Task'); +let odmOptionsParser = require('./libs/odmOptionsParser'); app.use(morgan('tiny')); app.use(bodyParser.urlencoded({extended: true})); @@ -137,6 +138,13 @@ app.post('/task/restart', uuidCheck, (req, res) => { taskManager.restart(req.body.uuid, successHandler(res)); }); +app.get('/getOptions', (req, res) => { + odmOptionsParser.getOptions((err, options) => { + if (err) res.json({error: err.message}); + else res.json(options); + }); +}); + let gracefulShutdown = done => { async.series([ cb => { taskManager.dumpTaskList(cb) }, @@ -158,7 +166,7 @@ process.on ('SIGINT', gracefulShutdown); // Startup let taskManager; let server; -/* + async.series([ cb => { taskManager = new TaskManager(cb); }, cb => { server = app.listen(3000, err => { @@ -168,6 +176,4 @@ async.series([ } ], err => { if (err) console.log("Error during startup: " + err.message); -});*/ -let odmOptionsParser = require('./libs/odmOptionsParser'); -odmOptionsParser.getOptions(function(){}); +}); diff --git a/libs/odmOptionsParser.js b/libs/odmOptionsParser.js index 8329db9..8066cfa 100644 --- a/libs/odmOptionsParser.js +++ b/libs/odmOptionsParser.js @@ -18,57 +18,69 @@ along with this program. If not, see . "use strict"; let odmRunner = require('./odmRunner'); +let options = null; + module.exports = { getOptions: function(done){ + if (options){ + done(null, options); + return; + } + odmRunner.getJsonOptions((err, json) => { if (err) done(err); else{ + options = {}; for (let option in json){ - if (option === "-h") continue; + if (["-h", "--project-path", "--zip-results"].indexOf(option) !== -1) continue; + let values = json[option]; + option = option.replace(/^--/, ""); let type = ""; - let defaultValue = ""; + let value = ""; let help = values.help || ""; - let range = values.metavar.replace(/[<>]/g, "").trim(); + let domain = values.metavar !== undefined ? + values.metavar.replace(/^[<>]/g, "") + .replace(/[<>]$/g, "") + .trim() : + ""; - switch(values.type.trim()){ + switch((values.type || "").trim()){ case "": type = "int"; - defaultValue = values['default'] !== undefined ? + value = values['default'] !== undefined ? parseInt(values['default']) : 0; break; case "": type = "float"; - defaultValue = values['default'] !== undefined ? + value = values['default'] !== undefined ? parseFloat(values['default']) : 0.0; break; default: type = "string"; - defaultValue = values['default'].trim(); + value = values['default'] !== undefined ? + values['default'].trim() : + ""; } if (values['default'] === "True"){ type = "bool"; - defaultValue = true; + value = true; }else if (values['default'] === "False"){ type = "bool"; - defaultValue = false; + value = false; } + help = help.replace(/\%\(default\)s/g, value); - - let result = { - type, defaultValue, range, help + options[option] = { + type, value, domain, help }; - - console.log(values); - console.log(result); - console.log('-----'); } - done(); + done(null, options); } }); } diff --git a/public/css/main.css b/public/css/main.css index 8b5d502..64b802a 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -55,4 +55,8 @@ height: 200px; font-family: monospace; font-size: 90%; +} + +.selectric-items li{ + background: #fff; } \ No newline at end of file diff --git a/public/css/selectric.css b/public/css/selectric.css new file mode 100644 index 0000000..8508f16 --- /dev/null +++ b/public/css/selectric.css @@ -0,0 +1,198 @@ +/*====================================== + Selectric v1.10.1 +======================================*/ + +.selectric-wrapper { + position: relative; + cursor: pointer; +} + +.selectric-responsive { + width: 100%; +} + +.selectric { + border: 1px solid #DDD; + background: #F8F8F8; + position: relative; +} +.selectric .label { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 0 38px 0 10px; + font-size: 12px; + line-height: 38px; + color: #444; + height: 38px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.selectric .button { + display: block; + position: absolute; + right: 0; + top: 0; + width: 38px; + height: 38px; + color: #BBB; + text-align: center; + font: 0/0 a; + *font: 20px/38px Lucida Sans Unicode, Arial Unicode MS, Arial; +} +.selectric .button:after { + content: " "; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + width: 0; + height: 0; + border: 4px solid transparent; + border-top-color: #BBB; + border-bottom: none; +} + +.selectric-focus .selectric { + border-color: #AAA; +} + +.selectric-hover .selectric { + border-color: #C4C4C4; +} +.selectric-hover .selectric .button { + color: #A2A2A2; +} +.selectric-hover .selectric .button:after { + border-top-color: #A2A2A2; +} + +.selectric-open { + z-index: 9999; +} +.selectric-open .selectric { + border-color: #C4C4C4; +} +.selectric-open .selectric-items { + display: block; +} + +.selectric-disabled { + filter: alpha(opacity=50); + opacity: 0.5; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.selectric-hide-select { + position: relative; + overflow: hidden; + width: 0; + height: 0; +} +.selectric-hide-select select { + position: absolute; + left: -100%; + display: none; +} + +.selectric-input { + position: absolute !important; + top: 0 !important; + left: 0 !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + margin: 0 !important; + padding: 0 !important; + width: 1px !important; + height: 1px !important; + outline: none !important; + border: none !important; + *font: 0/0 a !important; + background: none !important; +} + +.selectric-temp-show { + position: absolute !important; + visibility: hidden !important; + display: block !important; +} + +/* Items box */ +.selectric-items { + display: none; + position: absolute; + top: 100%; + left: 0; + background: #F8F8F8; + border: 1px solid #C4C4C4; + z-index: -1; + box-shadow: 0 0 10px -6px; +} +.selectric-items .selectric-scroll { + height: 100%; + overflow: auto; +} +.selectric-above .selectric-items { + top: auto; + bottom: 100%; +} +.selectric-items ul, .selectric-items li { + list-style: none; + padding: 0; + margin: 0; + font-size: 12px; + line-height: 20px; + min-height: 20px; +} +.selectric-items li { + display: block; + padding: 10px; + color: #666; + cursor: pointer; +} +.selectric-items li.selected { + background: #E0E0E0; + color: #444; +} +.selectric-items li:hover { + background: #D5D5D5; + color: #444; +} +.selectric-items .disabled { + filter: alpha(opacity=50); + opacity: 0.5; + cursor: default !important; + background: none !important; + color: #666 !important; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.selectric-items .selectric-group .selectric-group-label { + font-weight: bold; + padding-left: 10px; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background: none; + color: #444; +} +.selectric-items .selectric-group.disabled li { + filter: alpha(opacity=100); + opacity: 1; +} +.selectric-items .selectric-group li { + padding-left: 25px; +} diff --git a/public/index.html b/public/index.html index 6b970e5..55e59c8 100644 --- a/public/index.html +++ b/public/index.html @@ -15,6 +15,7 @@ } + @@ -47,14 +48,18 @@
-
- +
+
+ + +
-
-
+

Current Tasks ()

No running tasks.

@@ -113,6 +118,8 @@ + + diff --git a/public/js/jquery.selectric.js b/public/js/jquery.selectric.js new file mode 100644 index 0000000..fbde720 --- /dev/null +++ b/public/js/jquery.selectric.js @@ -0,0 +1,911 @@ +/*! + * ,/ + * ,'/ + * ,' / + * ,' /_____, + * .'____ ,' + * / ,' + * / ,' + * /,' + * /' + * + * Selectric ϟ v1.10.1 (Jun 30 2016) - http://lcdsantos.github.io/jQuery-Selectric/ + * + * Copyright (c) 2016 Leonardo Santos; MIT License + * + */ + +(function(factory) { + /* global define */ + /* istanbul ignore next */ + if ( typeof define === 'function' && define.amd ) { + define(['jquery'], factory); + } else if ( typeof module === 'object' && module.exports ) { + // Node/CommonJS + module.exports = function( root, jQuery ) { + if ( jQuery === undefined ) { + if ( typeof window !== 'undefined' ) { + jQuery = require('jquery'); + } else { + jQuery = require('jquery')(root); + } + } + factory(jQuery); + return jQuery; + }; + } else { + // Browser globals + factory(jQuery); + } +}(function($) { + 'use strict'; + + var $doc = $(document); + var $win = $(window); + + var pluginName = 'selectric'; + var classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Focus Hover Responsive Above Scroll Group GroupLabel'; + var bindSufix = '.sl'; + + var chars = ['a', 'e', 'i', 'o', 'u', 'n', 'c', 'y']; + var diacritics = [ + /[\xE0-\xE5]/g, // a + /[\xE8-\xEB]/g, // e + /[\xEC-\xEF]/g, // i + /[\xF2-\xF6]/g, // o + /[\xF9-\xFC]/g, // u + /[\xF1]/g, // n + /[\xE7]/g, // c + /[\xFD-\xFF]/g // y + ]; + + /** + * Create an instance of Selectric + * + * @constructor + * @param {Node} element - The <select> element + * @param {object} opts - Options + */ + var Selectric = function(element, opts) { + var _this = this; + + _this.element = element; + _this.$element = $(element); + + _this.state = { + enabled : false, + opened : false, + currValue : -1, + selectedIdx : -1 + }; + + _this.eventTriggers = { + open : _this.open, + close : _this.close, + destroy : _this.destroy, + refresh : _this.refresh, + init : _this.init + }; + + _this.init(opts); + }; + + Selectric.prototype = { + utils: { + /** + * Detect mobile browser + * + * @return {boolean} + */ + isMobile: function() { + return /android|ip(hone|od|ad)/i.test(navigator.userAgent); + }, + + /** + * Escape especial characters in string (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) + * + * @param {string} str - The string to be escaped + * @return {string} The string with the special characters escaped + */ + escapeRegExp: function(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + }, + + /** + * Replace diacritics + * + * @param {string} str - The string to replace the diacritics + * @return {string} The string with diacritics replaced with ascii characters + */ + replaceDiacritics: function(str) { + var k = diacritics.length; + + while (k--) { + str = str.toLowerCase().replace(diacritics[k], chars[k]); + } + + return str; + }, + + /** + * Format string + * https://gist.github.com/atesgoral/984375 + * + * @param {string} f - String to be formated + * @return {string} String formated + */ + format: function (f) { + var a = arguments; // store outer arguments + return ('' + f) // force format specifier to String + .replace( // replace tokens in format specifier + /\{(?:(\d+)|(\w+))\}/g, // match {token} references + function ( + s, // the matched string (ignored) + i, // an argument index + p // a property name + ) { + return p && a[1] // if property name and first argument exist + ? a[1][p] // return property from first argument + : a[i]; // assume argument index and return i-th argument + }); + }, + + /** + * Get the next enabled item in the options list. + * + * @param {object} selectItems - The options object. + * @param {number} selected - Index of the currently selected option. + * @return {object} The next enabled item. + */ + nextEnabledItem: function(selectItems, selected) { + while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) { + // empty + } + return selected; + }, + + /** + * Get the previous enabled item in the options list. + * + * @param {object} selectItems - The options object. + * @param {number} selected - Index of the currently selected option. + * @return {object} The previous enabled item. + */ + previousEnabledItem: function(selectItems, selected) { + while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) { + // empty + } + return selected; + }, + + /** + * Transform camelCase string to dash-case. + * + * @param {string} str - The camelCased string. + * @return {string} The string transformed to dash-case. + */ + toDash: function(str) { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + }, + + /** + * Calls the events and hooks registered with function name. + * + * @param {string} fn - The name of the function. + * @param {number} scope - Scope that should be set on the function. + */ + triggerCallback: function(fn, scope) { + var elm = scope.element; + var func = scope.options['on' + fn]; + + if ( $.isFunction(func) ) { + func.call(elm, elm, scope); + } + + if ( $.fn[pluginName].hooks[fn] ) { + $.each($.fn[pluginName].hooks[fn], function() { + this.call(elm, elm, scope); + }); + } + + $(elm).trigger(pluginName + '-' + this.toDash(fn), scope); + } + }, + + /** Initializes */ + init: function(opts) { + var _this = this; + + // Set options + _this.options = $.extend(true, {}, $.fn[pluginName].defaults, _this.options, opts); + + _this.utils.triggerCallback('BeforeInit', _this); + + // Preserve data + _this.destroy(true); + + // Disable on mobile browsers + if ( _this.options.disableOnMobile && _this.utils.isMobile() ) { + _this.disableOnMobile = true; + return; + } + + // Get classes + _this.classes = _this.getClassNames(); + + // Create elements + var input = $('', { 'class': _this.classes.input, 'readonly': _this.utils.isMobile() }); + var items = $('
', { 'class': _this.classes.items, 'tabindex': -1 }); + var itemsScroll = $('
', { 'class': _this.classes.scroll }); + var wrapper = $('
', { 'class': _this.classes.prefix, 'html': _this.options.arrowButtonMarkup }); + var label = $('', { 'class': 'label' }); + var outerWrapper = _this.$element.wrap('
').parent().append(wrapper.prepend(label), items, input); + + _this.elements = { + input : input, + items : items, + itemsScroll : itemsScroll, + wrapper : wrapper, + label : label, + outerWrapper : outerWrapper + }; + + _this.$element + .on(_this.eventTriggers) + .wrap('
'); + + _this.originalTabindex = _this.$element.prop('tabindex'); + _this.$element.prop('tabindex', false); + + _this.populate(); + _this.activate(); + + _this.utils.triggerCallback('Init', _this); + }, + + /** Activates the plugin */ + activate: function() { + var _this = this; + var originalWidth = _this.$element.width(); + + _this.utils.triggerCallback('BeforeActivate', _this); + + _this.elements.outerWrapper.prop('class', [ + _this.classes.wrapper, + _this.$element.prop('class').replace(/\S+/g, _this.classes.prefix + '-$&'), + _this.options.responsive ? _this.classes.responsive : '' + ].join(' ')); + + if ( _this.options.inheritOriginalWidth && originalWidth > 0 ) { + _this.elements.outerWrapper.width(originalWidth); + } + + if ( !_this.$element.prop('disabled') ) { + _this.state.enabled = true; + + // Not disabled, so... Removing disabled class + _this.elements.outerWrapper.removeClass(_this.classes.disabled); + + // Remove styles from items box + // Fix incorrect height when refreshed is triggered with fewer options + _this.$li = _this.elements.items.removeAttr('style').find('li'); + + _this.bindEvents(); + } else { + _this.elements.outerWrapper.addClass(_this.classes.disabled); + _this.elements.input.prop('disabled', true); + } + + _this.utils.triggerCallback('Activate', _this); + }, + + /** + * Generate classNames for elements + * + * @return {object} Classes object + */ + getClassNames: function() { + var _this = this; + var customClass = _this.options.customClass; + var classesObj = {}; + + $.each(classList.split(' '), function(i, currClass) { + var c = customClass.prefix + currClass; + classesObj[currClass.toLowerCase()] = customClass.camelCase ? c : _this.utils.toDash(c); + }); + + classesObj.prefix = customClass.prefix; + + return classesObj; + }, + + /** Set the label text */ + setLabel: function() { + var _this = this; + var labelBuilder = _this.options.labelBuilder; + var currItem = _this.lookupItems[_this.state.currValue]; + + _this.elements.label.html( + $.isFunction(labelBuilder) + ? labelBuilder(currItem) + : _this.utils.format(labelBuilder, currItem) + ); + }, + + /** Get and save the available options */ + populate: function() { + var _this = this; + var $options = _this.$element.children(); + var $justOptions = _this.$element.find('option'); + var selectedIndex = $justOptions.index($justOptions.filter(':selected')); + var currIndex = 0; + + _this.state.currValue = (_this.state.selected = ~selectedIndex ? selectedIndex : 0); + _this.state.selectedIdx = _this.state.currValue; + _this.items = []; + _this.lookupItems = []; + + if ( $options.length ) { + // Build options markup + $options.each(function(i) { + var $elm = $(this); + + if ( $elm.is('optgroup') ) { + + var optionsGroup = { + element : $elm, + label : $elm.prop('label'), + groupDisabled : $elm.prop('disabled'), + items : [] + }; + + $elm.children().each(function(i) { + var $elm = $(this); + var optionText = $elm.html(); + + optionsGroup.items[i] = { + index : currIndex, + element : $elm, + value : $elm.val(), + text : optionText, + slug : _this.utils.replaceDiacritics(optionText), + disabled : optionsGroup.groupDisabled + }; + + _this.lookupItems[currIndex] = optionsGroup.items[i]; + + currIndex++; + }); + + _this.items[i] = optionsGroup; + + } else { + + var optionText = $elm.html(); + + _this.items[i] = { + index : currIndex, + element : $elm, + value : $elm.val(), + text : optionText, + slug : _this.utils.replaceDiacritics(optionText), + disabled : $elm.prop('disabled') + }; + + _this.lookupItems[currIndex] = _this.items[i]; + + currIndex++; + + } + }); + + _this.setLabel(); + _this.elements.items.append( _this.elements.itemsScroll.html( _this.getItemsMarkup(_this.items) ) ); + } + }, + + /** + * Generate options markup + * + * @param {object} items - Object containing all available options + * @return {string} HTML for the options box + */ + getItemsMarkup: function(items) { + var _this = this; + var markup = '
    '; + + $.each(items, function(i, elm) { + if ( elm.label !== undefined ) { + + markup += _this.utils.format('
    • {3}
    • ', + $.trim([_this.classes.group, elm.groupDisabled ? 'disabled' : '', elm.element.prop('class')].join(' ')), + _this.classes.grouplabel, + elm.element.prop('label') + ); + + $.each(elm.items, function(i, elm) { + markup += _this.getItemMarkup(elm.index, elm); + }); + + markup += '
    '; + + } else { + + markup += _this.getItemMarkup(elm.index, elm); + + } + }); + + return markup + '
'; + }, + + /** + * Generate every option markup + * + * @param {number} i - Index of current item + * @param {object} elm - Current item + * @return {string} HTML for the option + */ + getItemMarkup: function(i, elm) { + var _this = this; + var itemBuilder = _this.options.optionsItemBuilder; + + return _this.utils.format('
  • {3}
  • ', + i, + $.trim([ + i === _this.state.currValue ? 'selected' : '', + i === _this.items.length - 1 ? 'last' : '', + elm.disabled ? 'disabled' : '' + ].join(' ')), + $.isFunction(itemBuilder) ? itemBuilder(elm, elm.element, i) : _this.utils.format(itemBuilder, elm) + ); + }, + + /** Bind events on the elements */ + bindEvents: function() { + var _this = this; + + _this.elements.wrapper + .add(_this.$element) + .add(_this.elements.outerWrapper) + .add(_this.elements.input) + .off(bindSufix); + + _this.elements.outerWrapper.on('mouseenter' + bindSufix + ' mouseleave' + bindSufix, function(e) { + $(this).toggleClass(_this.classes.hover, e.type === 'mouseenter'); + + // Delay close effect when openOnHover is true + if ( _this.options.openOnHover ) { + clearTimeout(_this.closeTimer); + + if ( e.type === 'mouseleave' ) { + _this.closeTimer = setTimeout($.proxy(_this.close, _this), _this.options.hoverIntentTimeout); + } else { + _this.open(); + } + } + }); + + // Toggle open/close + _this.elements.wrapper.on('click' + bindSufix, function(e) { + _this.state.opened ? _this.close() : _this.open(e); + }); + + _this.elements.input + .prop({ tabindex: _this.originalTabindex, disabled: false }) + .on('keydown' + bindSufix, $.proxy(_this.handleKeys, _this)) + .on('focusin' + bindSufix, function(e) { + _this.elements.outerWrapper.addClass(_this.classes.focus); + + // Prevent the flicker when focusing out and back again in the browser window + _this.elements.input.one('blur', function() { + _this.elements.input.blur(); + }); + + if ( _this.options.openOnFocus && !_this.state.opened ) { + _this.open(e); + } + }) + .on('focusout' + bindSufix, function() { + _this.elements.outerWrapper.removeClass(_this.classes.focus); + }) + .on('input propertychange', function() { + var val = _this.elements.input.val(); + + // Clear search + clearTimeout(_this.resetStr); + _this.resetStr = setTimeout(function() { + _this.elements.input.val(''); + }, _this.options.keySearchTimeout); + + if ( val.length ) { + // Search in select options + $.each(_this.items, function(i, elm) { + if ( RegExp('^' + _this.utils.escapeRegExp(val), 'i').test(elm.slug) && !elm.disabled ) { + _this.select(i); + return false; + } + }); + } + }); + + _this.$li.on({ + // Prevent blur on Chrome + mousedown: function(e) { + e.preventDefault(); + e.stopPropagation(); + }, + click: function() { + // The second parameter is to close the box after click + _this.select($(this).data('index'), true); + + // Chrome doesn't close options box if select is wrapped with a label + // We need to 'return false' to avoid that + return false; + } + }); + }, + + /** + * Behavior when keyboard keys is pressed + * + * @param {object} e - Event object + */ + handleKeys: function(e) { + var _this = this; + var key = e.keyCode || e.which; + var keys = _this.options.keys; + + var isPrev = $.inArray(key, keys.previous) > -1; + var isNext = $.inArray(key, keys.next) > -1; + var isSelect = $.inArray(key, keys.select) > -1; + var isOpen = $.inArray(key, keys.open) > -1; + var idx = _this.state.selectedIdx; + var isFirstOrLastItem = (isPrev && idx === 0) || (isNext && (idx + 1) === _this.items.length); + var goToItem = 0; + + // Enter / Space + if ( key === 13 || key === 32 ) { + e.preventDefault(); + } + + // If it's a directional key + if ( isPrev || isNext ) { + if ( !_this.options.allowWrap && isFirstOrLastItem ) { + return; + } + + if ( isPrev ) { + goToItem = _this.utils.previousEnabledItem(_this.items, idx); + } + + if ( isNext ) { + goToItem = _this.utils.nextEnabledItem(_this.items, idx); + } + + _this.select(goToItem); + } + + // Tab / Enter / ESC + if ( isSelect && _this.state.opened ) { + _this.select(idx, true); + return; + } + + // Space / Enter / Left / Up / Right / Down + if ( isOpen && !_this.state.opened ) { + _this.open(); + } + }, + + /** Update the items object */ + refresh: function() { + var _this = this; + + _this.populate(); + _this.activate(); + _this.utils.triggerCallback('Refresh', _this); + }, + + /** Set options box width/height */ + setOptionsDimensions: function() { + var _this = this; + + // Calculate options box height + // Set a temporary class on the hidden parent of the element + var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow); + var maxHeight = _this.options.maxHeight; + var itemsWidth = _this.elements.items.outerWidth(); + var wrapperWidth = _this.elements.wrapper.outerWidth() - (itemsWidth - _this.elements.items.width()); + + // Set the dimensions, minimum is wrapper width, expand for long items if option is true + if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth ) { + _this.finalWidth = wrapperWidth; + } else { + // Make sure the scrollbar width is included + _this.elements.items.css('overflow', 'scroll'); + + // Set a really long width for _this.elements.outerWrapper + _this.elements.outerWrapper.width(9e4); + _this.finalWidth = _this.elements.items.width(); + // Set scroll bar to auto + _this.elements.items.css('overflow', ''); + _this.elements.outerWrapper.width(''); + } + + _this.elements.items.width(_this.finalWidth).height() > maxHeight && _this.elements.items.height(maxHeight); + + // Remove the temporary class + hiddenChildren.removeClass(_this.classes.tempshow); + }, + + /** Detect if the options box is inside the window */ + isInViewport: function() { + var _this = this; + var scrollTop = $win.scrollTop(); + var winHeight = $win.height(); + var uiPosX = _this.elements.outerWrapper.offset().top; + var uiHeight = _this.elements.outerWrapper.outerHeight(); + + var fitsDown = (uiPosX + uiHeight + _this.itemsHeight) <= (scrollTop + winHeight); + var fitsAbove = (uiPosX - _this.itemsHeight) > scrollTop; + + // If it does not fit below, only render it + // above it fit's there. + // It's acceptable that the user needs to + // scroll the viewport to see the cut off UI + var renderAbove = !fitsDown && fitsAbove; + + _this.elements.outerWrapper.toggleClass(_this.classes.above, renderAbove); + }, + + /** + * Detect if currently selected option is visible and scroll the options box to show it + * + * @param {number} index - Index of the selected items + */ + detectItemVisibility: function(index) { + var _this = this; + var liHeight = _this.$li.eq(index).outerHeight(); + var liTop = _this.$li[index].offsetTop; + var itemsScrollTop = _this.elements.itemsScroll.scrollTop(); + var scrollT = liTop + liHeight * 2; + + _this.elements.itemsScroll.scrollTop( + scrollT > itemsScrollTop + _this.itemsHeight ? scrollT - _this.itemsHeight : + liTop - liHeight < itemsScrollTop ? liTop - liHeight : + itemsScrollTop + ); + }, + + /** + * Open the select options box + * + * @param {event} e - Event + */ + open: function(e) { + var _this = this; + + _this.utils.triggerCallback('BeforeOpen', _this); + + if ( e ) { + e.preventDefault(); + e.stopPropagation(); + } + + if ( _this.state.enabled ) { + _this.setOptionsDimensions(); + + // Find any other opened instances of select and close it + $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close'); + + _this.state.opened = true; + _this.itemsHeight = _this.elements.items.outerHeight(); + _this.itemsInnerHeight = _this.elements.items.height(); + + // Toggle options box visibility + _this.elements.outerWrapper.addClass(_this.classes.open); + + // Give dummy input focus + _this.elements.input.val(''); + if ( e && e.type !== 'focusin' ) { + _this.elements.input.focus(); + } + + $doc + .on('click' + bindSufix, $.proxy(_this.close, _this)) + .on('scroll' + bindSufix, $.proxy(_this.isInViewport, _this)); + _this.isInViewport(); + + // Prevent window scroll when using mouse wheel inside items box + if ( _this.options.preventWindowScroll ) { + /* istanbul ignore next */ + $doc.on('mousewheel' + bindSufix + ' DOMMouseScroll' + bindSufix, '.' + _this.classes.scroll, function(e) { + var orgEvent = e.originalEvent; + var scrollTop = $(this).scrollTop(); + var deltaY = 0; + + if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } + if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } + if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } + if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; } + + if ( scrollTop === (this.scrollHeight - _this.itemsInnerHeight) && deltaY < 0 || scrollTop === 0 && deltaY > 0 ) { + e.preventDefault(); + } + }); + } + + _this.detectItemVisibility(_this.state.selectedIdx); + + _this.utils.triggerCallback('Open', _this); + } + }, + + /** Close the select options box */ + close: function() { + var _this = this; + + _this.utils.triggerCallback('BeforeClose', _this); + + _this.change(); + + // Remove custom events on document + $doc.off(bindSufix); + + // Remove visible class to hide options box + _this.elements.outerWrapper.removeClass(_this.classes.open); + + _this.state.opened = false; + + _this.utils.triggerCallback('Close', _this); + }, + + /** Select current option and change the label */ + change: function() { + var _this = this; + + _this.utils.triggerCallback('BeforeChange', _this); + + if ( _this.state.currValue !== _this.state.selectedIdx ) { + // Apply changed value to original select + _this.$element + .prop('selectedIndex', _this.state.currValue = _this.state.selectedIdx) + .data('value', _this.lookupItems[_this.state.selectedIdx].text); + + // Change label text + _this.setLabel(); + } + + _this.utils.triggerCallback('Change', _this); + }, + + /** + * Select option + * + * @param {number} index - Index of the option that will be selected + * @param {boolean} close - Close the options box after selecting + */ + select: function(index, close) { + var _this = this; + + // Parameter index is required + if ( index === undefined ) { + return; + } + + // If element is disabled, can't select it + if ( !_this.lookupItems[index].disabled ) { + _this.$li.filter('[data-index]') + .removeClass('selected') + .eq(_this.state.selectedIdx = index) + .addClass('selected'); + + _this.detectItemVisibility(index); + + // If 'close' is false (default), the options box won't close after + // each selected item, this is necessary for keyboard navigation + if ( close ) { + _this.close(); + } + } + }, + + /** + * Unbind and remove + * + * @param {boolean} preserveData - Check if the data on the element should be removed too + */ + destroy: function(preserveData) { + var _this = this; + + if ( _this.state && _this.state.enabled ) { + _this.elements.items.add(_this.elements.wrapper).add(_this.elements.input).remove(); + + if ( !preserveData ) { + _this.$element.removeData(pluginName).removeData('value'); + } + + _this.$element.prop('tabindex', _this.originalTabindex).off(bindSufix).off(_this.eventTriggers).unwrap().unwrap(); + + _this.state.enabled = false; + } + } + }; + + // A really lightweight plugin wrapper around the constructor, + // preventing against multiple instantiations + $.fn[pluginName] = function(args) { + return this.each(function() { + var data = $.data(this, pluginName); + + if ( data && !data.disableOnMobile ) { + (typeof args === 'string' && data[args]) ? data[args]() : data.init(args); + } else { + $.data(this, pluginName, new Selectric(this, args)); + } + }); + }; + + /** + * Hooks for the callbacks + * + * @type {object} + */ + $.fn[pluginName].hooks = { + /** + * @param {string} callbackName - The callback name. + * @param {string} hookName - The name of the hook to be attached. + * @param {function} fn - Callback function. + */ + add: function(callbackName, hookName, fn) { + if ( !this[callbackName] ) { + this[callbackName] = {}; + } + + this[callbackName][hookName] = fn; + }, + + /** + * @param {string} callbackName - The callback name. + * @param {string} hookName - The name of the hook that will be removed. + */ + remove: function(callbackName, hookName) { + delete this[callbackName][hookName]; + } + }; + + /** + * Default plugin options + * + * @type {object} + */ + $.fn[pluginName].defaults = { + onChange : function(elm) { $(elm).change(); }, + maxHeight : 300, + keySearchTimeout : 500, + arrowButtonMarkup : '', + disableOnMobile : true, + openOnFocus : true, + openOnHover : false, + hoverIntentTimeout : 500, + expandToItemText : false, + responsive : false, + preventWindowScroll : true, + inheritOriginalWidth : false, + allowWrap : true, + optionsItemBuilder : '{text}', // function(itemData, element, index) + labelBuilder : '{text}', // function(currItem) + keys : { + previous : [37, 38], // Left / Up + next : [39, 40], // Right / Down + select : [9, 13, 27], // Tab / Enter / Escape + open : [13, 32, 37, 38, 39, 40], // Enter / Space / Left / Up / Right / Down + close : [9, 27] // Tab / Escape + }, + customClass : { + prefix: pluginName, + camelCase: false + } + }; +})); \ No newline at end of file diff --git a/public/js/jquery.selectric.min.js b/public/js/jquery.selectric.min.js new file mode 100644 index 0000000..9dc6077 --- /dev/null +++ b/public/js/jquery.selectric.min.js @@ -0,0 +1,2 @@ +/*! Selectric ϟ v1.10.1 (2016-06-30) - git.io/tjl9sQ - Copyright (c) 2016 Leonardo Santos - MIT License */ +!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof module&&module.exports?module.exports=function(t,s){return void 0===s&&(s="undefined"!=typeof window?require("jquery"):require("jquery")(t)),e(s),s}:e(jQuery)}(function(e){"use strict";var t=e(document),s=e(window),i="selectric",n="Input Items Open Disabled TempShow HideSelect Wrapper Focus Hover Responsive Above Scroll Group GroupLabel",l=".sl",o=["a","e","i","o","u","n","c","y"],a=[/[\xE0-\xE5]/g,/[\xE8-\xEB]/g,/[\xEC-\xEF]/g,/[\xF2-\xF6]/g,/[\xF9-\xFC]/g,/[\xF1]/g,/[\xE7]/g,/[\xFD-\xFF]/g],r=function(t,s){var i=this;i.element=t,i.$element=e(t),i.state={enabled:!1,opened:!1,currValue:-1,selectedIdx:-1},i.eventTriggers={open:i.open,close:i.close,destroy:i.destroy,refresh:i.refresh,init:i.init},i.init(s)};r.prototype={utils:{isMobile:function(){return/android|ip(hone|od|ad)/i.test(navigator.userAgent)},escapeRegExp:function(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")},replaceDiacritics:function(e){for(var t=a.length;t--;)e=e.toLowerCase().replace(a[t],o[t]);return e},format:function(e){var t=arguments;return(""+e).replace(/\{(?:(\d+)|(\w+))\}/g,function(e,s,i){return i&&t[1]?t[1][i]:t[s]})},nextEnabledItem:function(e,t){for(;e[t=(t+1)%e.length].disabled;);return t},previousEnabledItem:function(e,t){for(;e[t=(t>0?t:e.length)-1].disabled;);return t},toDash:function(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()},triggerCallback:function(t,s){var n=s.element,l=s.options["on"+t];e.isFunction(l)&&l.call(n,n,s),e.fn[i].hooks[t]&&e.each(e.fn[i].hooks[t],function(){this.call(n,n,s)}),e(n).trigger(i+"-"+this.toDash(t),s)}},init:function(t){var s=this;if(s.options=e.extend(!0,{},e.fn[i].defaults,s.options,t),s.utils.triggerCallback("BeforeInit",s),s.destroy(!0),s.options.disableOnMobile&&s.utils.isMobile())return void(s.disableOnMobile=!0);s.classes=s.getClassNames();var n=e("",{"class":s.classes.input,readonly:s.utils.isMobile()}),l=e("
    ",{"class":s.classes.items,tabindex:-1}),o=e("
    ",{"class":s.classes.scroll}),a=e("
    ",{"class":s.classes.prefix,html:s.options.arrowButtonMarkup}),r=e("",{"class":"label"}),p=s.$element.wrap("
    ").parent().append(a.prepend(r),l,n);s.elements={input:n,items:l,itemsScroll:o,wrapper:a,label:r,outerWrapper:p},s.$element.on(s.eventTriggers).wrap('
    '),s.originalTabindex=s.$element.prop("tabindex"),s.$element.prop("tabindex",!1),s.populate(),s.activate(),s.utils.triggerCallback("Init",s)},activate:function(){var e=this,t=e.$element.width();e.utils.triggerCallback("BeforeActivate",e),e.elements.outerWrapper.prop("class",[e.classes.wrapper,e.$element.prop("class").replace(/\S+/g,e.classes.prefix+"-$&"),e.options.responsive?e.classes.responsive:""].join(" ")),e.options.inheritOriginalWidth&&t>0&&e.elements.outerWrapper.width(t),e.$element.prop("disabled")?(e.elements.outerWrapper.addClass(e.classes.disabled),e.elements.input.prop("disabled",!0)):(e.state.enabled=!0,e.elements.outerWrapper.removeClass(e.classes.disabled),e.$li=e.elements.items.removeAttr("style").find("li"),e.bindEvents()),e.utils.triggerCallback("Activate",e)},getClassNames:function(){var t=this,s=t.options.customClass,i={};return e.each(n.split(" "),function(e,n){var l=s.prefix+n;i[n.toLowerCase()]=s.camelCase?l:t.utils.toDash(l)}),i.prefix=s.prefix,i},setLabel:function(){var t=this,s=t.options.labelBuilder,i=t.lookupItems[t.state.currValue];t.elements.label.html(e.isFunction(s)?s(i):t.utils.format(s,i))},populate:function(){var t=this,s=t.$element.children(),i=t.$element.find("option"),n=i.index(i.filter(":selected")),l=0;t.state.currValue=t.state.selected=~n?n:0,t.state.selectedIdx=t.state.currValue,t.items=[],t.lookupItems=[],s.length&&(s.each(function(s){var i=e(this);if(i.is("optgroup")){var n={element:i,label:i.prop("label"),groupDisabled:i.prop("disabled"),items:[]};i.children().each(function(s){var i=e(this),o=i.html();n.items[s]={index:l,element:i,value:i.val(),text:o,slug:t.utils.replaceDiacritics(o),disabled:n.groupDisabled},t.lookupItems[l]=n.items[s],l++}),t.items[s]=n}else{var o=i.html();t.items[s]={index:l,element:i,value:i.val(),text:o,slug:t.utils.replaceDiacritics(o),disabled:i.prop("disabled")},t.lookupItems[l]=t.items[s],l++}}),t.setLabel(),t.elements.items.append(t.elements.itemsScroll.html(t.getItemsMarkup(t.items))))},getItemsMarkup:function(t){var s=this,i="
      ";return e.each(t,function(t,n){void 0!==n.label?(i+=s.utils.format('
      • {3}
      • ',e.trim([s.classes.group,n.groupDisabled?"disabled":"",n.element.prop("class")].join(" ")),s.classes.grouplabel,n.element.prop("label")),e.each(n.items,function(e,t){i+=s.getItemMarkup(t.index,t)}),i+="
      "):i+=s.getItemMarkup(n.index,n)}),i+"
    "},getItemMarkup:function(t,s){var i=this,n=i.options.optionsItemBuilder;return i.utils.format('
  • {3}
  • ',t,e.trim([t===i.state.currValue?"selected":"",t===i.items.length-1?"last":"",s.disabled?"disabled":""].join(" ")),e.isFunction(n)?n(s,s.element,t):i.utils.format(n,s))},bindEvents:function(){var t=this;t.elements.wrapper.add(t.$element).add(t.elements.outerWrapper).add(t.elements.input).off(l),t.elements.outerWrapper.on("mouseenter"+l+" mouseleave"+l,function(s){e(this).toggleClass(t.classes.hover,"mouseenter"===s.type),t.options.openOnHover&&(clearTimeout(t.closeTimer),"mouseleave"===s.type?t.closeTimer=setTimeout(e.proxy(t.close,t),t.options.hoverIntentTimeout):t.open())}),t.elements.wrapper.on("click"+l,function(e){t.state.opened?t.close():t.open(e)}),t.elements.input.prop({tabindex:t.originalTabindex,disabled:!1}).on("keydown"+l,e.proxy(t.handleKeys,t)).on("focusin"+l,function(e){t.elements.outerWrapper.addClass(t.classes.focus),t.elements.input.one("blur",function(){t.elements.input.blur()}),t.options.openOnFocus&&!t.state.opened&&t.open(e)}).on("focusout"+l,function(){t.elements.outerWrapper.removeClass(t.classes.focus)}).on("input propertychange",function(){var s=t.elements.input.val();clearTimeout(t.resetStr),t.resetStr=setTimeout(function(){t.elements.input.val("")},t.options.keySearchTimeout),s.length&&e.each(t.items,function(e,i){if(RegExp("^"+t.utils.escapeRegExp(s),"i").test(i.slug)&&!i.disabled)return t.select(e),!1})}),t.$li.on({mousedown:function(e){e.preventDefault(),e.stopPropagation()},click:function(){return t.select(e(this).data("index"),!0),!1}})},handleKeys:function(t){var s=this,i=t.keyCode||t.which,n=s.options.keys,l=e.inArray(i,n.previous)>-1,o=e.inArray(i,n.next)>-1,a=e.inArray(i,n.select)>-1,r=e.inArray(i,n.open)>-1,p=s.state.selectedIdx,u=l&&0===p||o&&p+1===s.items.length,c=0;if(13!==i&&32!==i||t.preventDefault(),l||o){if(!s.options.allowWrap&&u)return;l&&(c=s.utils.previousEnabledItem(s.items,p)),o&&(c=s.utils.nextEnabledItem(s.items,p)),s.select(c)}return a&&s.state.opened?void s.select(p,!0):void(r&&!s.state.opened&&s.open())},refresh:function(){var e=this;e.populate(),e.activate(),e.utils.triggerCallback("Refresh",e)},setOptionsDimensions:function(){var e=this,t=e.elements.items.closest(":visible").children(":hidden").addClass(e.classes.tempshow),s=e.options.maxHeight,i=e.elements.items.outerWidth(),n=e.elements.wrapper.outerWidth()-(i-e.elements.items.width());!e.options.expandToItemText||n>i?e.finalWidth=n:(e.elements.items.css("overflow","scroll"),e.elements.outerWrapper.width(9e4),e.finalWidth=e.elements.items.width(),e.elements.items.css("overflow",""),e.elements.outerWrapper.width("")),e.elements.items.width(e.finalWidth).height()>s&&e.elements.items.height(s),t.removeClass(e.classes.tempshow)},isInViewport:function(){var e=this,t=s.scrollTop(),i=s.height(),n=e.elements.outerWrapper.offset().top,l=e.elements.outerWrapper.outerHeight(),o=n+l+e.itemsHeight<=t+i,a=n-e.itemsHeight>t,r=!o&&a;e.elements.outerWrapper.toggleClass(e.classes.above,r)},detectItemVisibility:function(e){var t=this,s=t.$li.eq(e).outerHeight(),i=t.$li[e].offsetTop,n=t.elements.itemsScroll.scrollTop(),l=i+2*s;t.elements.itemsScroll.scrollTop(l>n+t.itemsHeight?l-t.itemsHeight:i-s0)&&t.preventDefault()}),n.detectItemVisibility(n.state.selectedIdx),n.utils.triggerCallback("Open",n))},close:function(){var e=this;e.utils.triggerCallback("BeforeClose",e),e.change(),t.off(l),e.elements.outerWrapper.removeClass(e.classes.open),e.state.opened=!1,e.utils.triggerCallback("Close",e)},change:function(){var e=this;e.utils.triggerCallback("BeforeChange",e),e.state.currValue!==e.state.selectedIdx&&(e.$element.prop("selectedIndex",e.state.currValue=e.state.selectedIdx).data("value",e.lookupItems[e.state.selectedIdx].text),e.setLabel()),e.utils.triggerCallback("Change",e)},select:function(e,t){var s=this;void 0!==e&&(s.lookupItems[e].disabled||(s.$li.filter("[data-index]").removeClass("selected").eq(s.state.selectedIdx=e).addClass("selected"),s.detectItemVisibility(e),t&&s.close()))},destroy:function(e){var t=this;t.state&&t.state.enabled&&(t.elements.items.add(t.elements.wrapper).add(t.elements.input).remove(),e||t.$element.removeData(i).removeData("value"),t.$element.prop("tabindex",t.originalTabindex).off(l).off(t.eventTriggers).unwrap().unwrap(),t.state.enabled=!1)}},e.fn[i]=function(t){return this.each(function(){var s=e.data(this,i);s&&!s.disableOnMobile?"string"==typeof t&&s[t]?s[t]():s.init(t):e.data(this,i,new r(this,t))})},e.fn[i].hooks={add:function(e,t,s){this[e]||(this[e]={}),this[e][t]=s},remove:function(e,t){delete this[e][t]}},e.fn[i].defaults={onChange:function(t){e(t).change()},maxHeight:300,keySearchTimeout:500,arrowButtonMarkup:'',disableOnMobile:!0,openOnFocus:!0,openOnHover:!1,hoverIntentTimeout:500,expandToItemText:!1,responsive:!1,preventWindowScroll:!0,inheritOriginalWidth:!1,allowWrap:!0,optionsItemBuilder:"{text}",labelBuilder:"{text}",keys:{previous:[37,38],next:[39,40],select:[9,13,27],open:[13,32,37,38,39,40],close:[9,27]},customClass:{prefix:i,camelCase:!1}}}); \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js index 37c5a74..abaa8cb 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -254,7 +254,7 @@ $(function(){ }; var taskList = new TaskList(); - ko.applyBindings(taskList); + ko.applyBindings(taskList, document.getElementById('taskList')); // Handle uploads $("#images").fileinput({ @@ -294,4 +294,37 @@ $(function(){ }) .on('filebatchuploaderror', function(e, data, msg){ }); + + // Load options + function Option(name, params){ + this.name = name; + this.params = params; + } + + function OptionsModel(){ + var self = this; + + this.options = ko.observableArray(); + this.error = ko.observable(); + + $.get("/getOptions") + .done(function(json){ + if (json.error) self.error(json.error); + else{ + for (var optionName in json){ + self.options.push(new Option(optionName, json[optionName])); + } + + $('select').selectric({ + maxHeight: 500 + }); + } + }) + .fail(function(){ + self.error("options are not available."); + }) + } + + var optionsModel = new OptionsModel(); + ko.applyBindings(optionsModel, document.getElementById("options")); }); From 185467b0cd4f3c8c3415e6ec4bf5780613fc1003 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 26 Jul 2016 14:20:37 -0500 Subject: [PATCH 3/5] Options display, sending options to server --- index.js | 44 +- libs/Task.js | 8 +- libs/{odmOptionsParser.js => odmOptions.js} | 24 +- public/css/main.css | 4 + public/css/selectric.css | 198 ----- public/index.html | 45 +- public/js/jquery.selectric.js | 911 -------------------- public/js/jquery.selectric.min.js | 2 - public/js/main.js | 42 +- 9 files changed, 120 insertions(+), 1158 deletions(-) rename libs/{odmOptionsParser.js => odmOptions.js} (75%) delete mode 100644 public/css/selectric.css delete mode 100644 public/js/jquery.selectric.js delete mode 100644 public/js/jquery.selectric.min.js diff --git a/index.js b/index.js index a23f4ea..99006b6 100644 --- a/index.js +++ b/index.js @@ -29,7 +29,7 @@ let bodyParser = require('body-parser'); let morgan = require('morgan'); let TaskManager = require('./libs/taskManager'); let Task = require('./libs/Task'); -let odmOptionsParser = require('./libs/odmOptionsParser'); +let odmOptions = require('./libs/odmOptions'); app.use(morgan('tiny')); app.use(bodyParser.urlencoded({extended: true})); @@ -59,8 +59,18 @@ let upload = multer({ app.post('/task/new', addRequestId, upload.array('images'), (req, res) => { if (req.files.length === 0) res.json({error: "Need at least 1 file."}); else{ - // Move to data async.series([ + cb => { + odmOptions.filterOptions(req.body.options, (err, options) => { + if (err) cb(err); + else{ + req.body.options = options; + cb(null); + } + }); + }, + + // Move uploads to data dir cb => { fs.stat(`data/${req.id}`, (err, stat) => { if (err && err.code === 'ENOENT') cb(); @@ -70,19 +80,21 @@ app.post('/task/new', addRequestId, upload.array('images'), (req, res) => { cb => { fs.mkdir(`data/${req.id}`, undefined, cb); }, cb => { fs.rename(`tmp/${req.id}`, `data/${req.id}/images`, err => { - if (!err){ - new Task(req.id, req.body.name, (err, task) => { - if (err) cb(err); - else{ - taskManager.addNew(task); - res.json({uuid: req.id, success: true}); - cb(); - } - }); - }else{ - cb(new Error("Could not move images folder.")) - } + if (!err) cb(); + else cb(new Error("Could not move images folder.")) }); + }, + + // Create task + cb => { + new Task(req.id, req.body.name, (err, task) => { + if (err) cb(err); + else{ + taskManager.addNew(task); + res.json({uuid: req.id, success: true}); + cb(); + } + }, req.body.options); } ], err => { if (err) res.json({error: err.message}) @@ -139,7 +151,7 @@ app.post('/task/restart', uuidCheck, (req, res) => { }); app.get('/getOptions', (req, res) => { - odmOptionsParser.getOptions((err, options) => { + odmOptions.getOptions((err, options) => { if (err) res.json({error: err.message}); else res.json(options); }); @@ -147,7 +159,7 @@ app.get('/getOptions', (req, res) => { let gracefulShutdown = done => { async.series([ - cb => { taskManager.dumpTaskList(cb) }, + cb => { taskManager.dumpTaskList(cb); }, cb => { console.log("Closing server"); server.close(); diff --git a/libs/Task.js b/libs/Task.js index ff5c077..40f0f99 100644 --- a/libs/Task.js +++ b/libs/Task.js @@ -25,7 +25,7 @@ let archiver = require('archiver'); let statusCodes = require('./statusCodes'); module.exports = class Task{ - constructor(uuid, name, done){ + constructor(uuid, name, done, options = []){ assert(uuid !== undefined, "uuid must be set"); assert(done !== undefined, "ready must be set"); @@ -34,10 +34,12 @@ module.exports = class Task{ this.dateCreated = new Date().getTime(); this.processingTime = -1; this.setStatus(statusCodes.QUEUED); - this.options = {}; + this.options = options; this.output = []; this.runnerProcess = null; + this.options.forEach(option => { console.log(option); }); + // Read images info fs.readdir(this.getImagesFolderPath(), (err, files) => { if (err) done(err); @@ -64,7 +66,7 @@ module.exports = class Task{ } done(null, task); } - }) + }, taskJson.options); } // Get path where images are stored for this task diff --git a/libs/odmOptionsParser.js b/libs/odmOptions.js similarity index 75% rename from libs/odmOptionsParser.js rename to libs/odmOptions.js index 8066cfa..5610f29 100644 --- a/libs/odmOptionsParser.js +++ b/libs/odmOptions.js @@ -32,8 +32,12 @@ module.exports = { else{ options = {}; for (let option in json){ - if (["-h", "--project-path", "--zip-results"].indexOf(option) !== -1) continue; - + // Not all options are useful to the end user + // (num cores can be set programmatically, so can gcpFile, etc.) + if (["-h", "--project-path", + "--zip-results", "--pmvs-num-cores", "--odm_georeferencing-useGcp", + "--start-with", "--odm_georeferencing-gcpFile", "--end-with"].indexOf(option) !== -1) continue; + let values = json[option]; option = option.replace(/^--/, ""); @@ -83,5 +87,21 @@ module.exports = { done(null, options); } }); + }, + + // Checks that the options (as received from the rest endpoint) + // Are valid and within proper ranges. + // The result of filtering is passed back via callback + // @param options[] + filterOptions: function(options, done){ + try{ + if (typeof options === "string") options = JSON.parse(options); + + // TODO: range checks, filtering + + done(null, options); + }catch(e){ + done(e); + } } }; diff --git a/public/css/main.css b/public/css/main.css index 64b802a..0da13d7 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -59,4 +59,8 @@ .selectric-items li{ background: #fff; +} + +#options .checkbox{ + margin-right: 143px; } \ No newline at end of file diff --git a/public/css/selectric.css b/public/css/selectric.css deleted file mode 100644 index 8508f16..0000000 --- a/public/css/selectric.css +++ /dev/null @@ -1,198 +0,0 @@ -/*====================================== - Selectric v1.10.1 -======================================*/ - -.selectric-wrapper { - position: relative; - cursor: pointer; -} - -.selectric-responsive { - width: 100%; -} - -.selectric { - border: 1px solid #DDD; - background: #F8F8F8; - position: relative; -} -.selectric .label { - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin: 0 38px 0 10px; - font-size: 12px; - line-height: 38px; - color: #444; - height: 38px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.selectric .button { - display: block; - position: absolute; - right: 0; - top: 0; - width: 38px; - height: 38px; - color: #BBB; - text-align: center; - font: 0/0 a; - *font: 20px/38px Lucida Sans Unicode, Arial Unicode MS, Arial; -} -.selectric .button:after { - content: " "; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - width: 0; - height: 0; - border: 4px solid transparent; - border-top-color: #BBB; - border-bottom: none; -} - -.selectric-focus .selectric { - border-color: #AAA; -} - -.selectric-hover .selectric { - border-color: #C4C4C4; -} -.selectric-hover .selectric .button { - color: #A2A2A2; -} -.selectric-hover .selectric .button:after { - border-top-color: #A2A2A2; -} - -.selectric-open { - z-index: 9999; -} -.selectric-open .selectric { - border-color: #C4C4C4; -} -.selectric-open .selectric-items { - display: block; -} - -.selectric-disabled { - filter: alpha(opacity=50); - opacity: 0.5; - cursor: default; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.selectric-hide-select { - position: relative; - overflow: hidden; - width: 0; - height: 0; -} -.selectric-hide-select select { - position: absolute; - left: -100%; - display: none; -} - -.selectric-input { - position: absolute !important; - top: 0 !important; - left: 0 !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - margin: 0 !important; - padding: 0 !important; - width: 1px !important; - height: 1px !important; - outline: none !important; - border: none !important; - *font: 0/0 a !important; - background: none !important; -} - -.selectric-temp-show { - position: absolute !important; - visibility: hidden !important; - display: block !important; -} - -/* Items box */ -.selectric-items { - display: none; - position: absolute; - top: 100%; - left: 0; - background: #F8F8F8; - border: 1px solid #C4C4C4; - z-index: -1; - box-shadow: 0 0 10px -6px; -} -.selectric-items .selectric-scroll { - height: 100%; - overflow: auto; -} -.selectric-above .selectric-items { - top: auto; - bottom: 100%; -} -.selectric-items ul, .selectric-items li { - list-style: none; - padding: 0; - margin: 0; - font-size: 12px; - line-height: 20px; - min-height: 20px; -} -.selectric-items li { - display: block; - padding: 10px; - color: #666; - cursor: pointer; -} -.selectric-items li.selected { - background: #E0E0E0; - color: #444; -} -.selectric-items li:hover { - background: #D5D5D5; - color: #444; -} -.selectric-items .disabled { - filter: alpha(opacity=50); - opacity: 0.5; - cursor: default !important; - background: none !important; - color: #666 !important; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.selectric-items .selectric-group .selectric-group-label { - font-weight: bold; - padding-left: 10px; - cursor: default; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background: none; - color: #444; -} -.selectric-items .selectric-group.disabled li { - filter: alpha(opacity=100); - opacity: 1; -} -.selectric-items .selectric-group li { - padding-left: 25px; -} diff --git a/public/index.html b/public/index.html index 55e59c8..d830827 100644 --- a/public/index.html +++ b/public/index.html @@ -15,8 +15,6 @@ } - - @@ -42,22 +40,40 @@
    -
    - -
    -
    -
    -
    - - - + +
    +
    +
    +
    +
    + + + +
    +
    +
    + + + + +
    +
    + + + - +

    +
    +
    +
    +
    +

    Current Tasks ()

    @@ -118,7 +134,6 @@ - diff --git a/public/js/jquery.selectric.js b/public/js/jquery.selectric.js deleted file mode 100644 index fbde720..0000000 --- a/public/js/jquery.selectric.js +++ /dev/null @@ -1,911 +0,0 @@ -/*! - * ,/ - * ,'/ - * ,' / - * ,' /_____, - * .'____ ,' - * / ,' - * / ,' - * /,' - * /' - * - * Selectric ϟ v1.10.1 (Jun 30 2016) - http://lcdsantos.github.io/jQuery-Selectric/ - * - * Copyright (c) 2016 Leonardo Santos; MIT License - * - */ - -(function(factory) { - /* global define */ - /* istanbul ignore next */ - if ( typeof define === 'function' && define.amd ) { - define(['jquery'], factory); - } else if ( typeof module === 'object' && module.exports ) { - // Node/CommonJS - module.exports = function( root, jQuery ) { - if ( jQuery === undefined ) { - if ( typeof window !== 'undefined' ) { - jQuery = require('jquery'); - } else { - jQuery = require('jquery')(root); - } - } - factory(jQuery); - return jQuery; - }; - } else { - // Browser globals - factory(jQuery); - } -}(function($) { - 'use strict'; - - var $doc = $(document); - var $win = $(window); - - var pluginName = 'selectric'; - var classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Focus Hover Responsive Above Scroll Group GroupLabel'; - var bindSufix = '.sl'; - - var chars = ['a', 'e', 'i', 'o', 'u', 'n', 'c', 'y']; - var diacritics = [ - /[\xE0-\xE5]/g, // a - /[\xE8-\xEB]/g, // e - /[\xEC-\xEF]/g, // i - /[\xF2-\xF6]/g, // o - /[\xF9-\xFC]/g, // u - /[\xF1]/g, // n - /[\xE7]/g, // c - /[\xFD-\xFF]/g // y - ]; - - /** - * Create an instance of Selectric - * - * @constructor - * @param {Node} element - The <select> element - * @param {object} opts - Options - */ - var Selectric = function(element, opts) { - var _this = this; - - _this.element = element; - _this.$element = $(element); - - _this.state = { - enabled : false, - opened : false, - currValue : -1, - selectedIdx : -1 - }; - - _this.eventTriggers = { - open : _this.open, - close : _this.close, - destroy : _this.destroy, - refresh : _this.refresh, - init : _this.init - }; - - _this.init(opts); - }; - - Selectric.prototype = { - utils: { - /** - * Detect mobile browser - * - * @return {boolean} - */ - isMobile: function() { - return /android|ip(hone|od|ad)/i.test(navigator.userAgent); - }, - - /** - * Escape especial characters in string (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) - * - * @param {string} str - The string to be escaped - * @return {string} The string with the special characters escaped - */ - escapeRegExp: function(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string - }, - - /** - * Replace diacritics - * - * @param {string} str - The string to replace the diacritics - * @return {string} The string with diacritics replaced with ascii characters - */ - replaceDiacritics: function(str) { - var k = diacritics.length; - - while (k--) { - str = str.toLowerCase().replace(diacritics[k], chars[k]); - } - - return str; - }, - - /** - * Format string - * https://gist.github.com/atesgoral/984375 - * - * @param {string} f - String to be formated - * @return {string} String formated - */ - format: function (f) { - var a = arguments; // store outer arguments - return ('' + f) // force format specifier to String - .replace( // replace tokens in format specifier - /\{(?:(\d+)|(\w+))\}/g, // match {token} references - function ( - s, // the matched string (ignored) - i, // an argument index - p // a property name - ) { - return p && a[1] // if property name and first argument exist - ? a[1][p] // return property from first argument - : a[i]; // assume argument index and return i-th argument - }); - }, - - /** - * Get the next enabled item in the options list. - * - * @param {object} selectItems - The options object. - * @param {number} selected - Index of the currently selected option. - * @return {object} The next enabled item. - */ - nextEnabledItem: function(selectItems, selected) { - while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) { - // empty - } - return selected; - }, - - /** - * Get the previous enabled item in the options list. - * - * @param {object} selectItems - The options object. - * @param {number} selected - Index of the currently selected option. - * @return {object} The previous enabled item. - */ - previousEnabledItem: function(selectItems, selected) { - while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) { - // empty - } - return selected; - }, - - /** - * Transform camelCase string to dash-case. - * - * @param {string} str - The camelCased string. - * @return {string} The string transformed to dash-case. - */ - toDash: function(str) { - return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - }, - - /** - * Calls the events and hooks registered with function name. - * - * @param {string} fn - The name of the function. - * @param {number} scope - Scope that should be set on the function. - */ - triggerCallback: function(fn, scope) { - var elm = scope.element; - var func = scope.options['on' + fn]; - - if ( $.isFunction(func) ) { - func.call(elm, elm, scope); - } - - if ( $.fn[pluginName].hooks[fn] ) { - $.each($.fn[pluginName].hooks[fn], function() { - this.call(elm, elm, scope); - }); - } - - $(elm).trigger(pluginName + '-' + this.toDash(fn), scope); - } - }, - - /** Initializes */ - init: function(opts) { - var _this = this; - - // Set options - _this.options = $.extend(true, {}, $.fn[pluginName].defaults, _this.options, opts); - - _this.utils.triggerCallback('BeforeInit', _this); - - // Preserve data - _this.destroy(true); - - // Disable on mobile browsers - if ( _this.options.disableOnMobile && _this.utils.isMobile() ) { - _this.disableOnMobile = true; - return; - } - - // Get classes - _this.classes = _this.getClassNames(); - - // Create elements - var input = $('', { 'class': _this.classes.input, 'readonly': _this.utils.isMobile() }); - var items = $('
    ', { 'class': _this.classes.items, 'tabindex': -1 }); - var itemsScroll = $('
    ', { 'class': _this.classes.scroll }); - var wrapper = $('
    ', { 'class': _this.classes.prefix, 'html': _this.options.arrowButtonMarkup }); - var label = $('', { 'class': 'label' }); - var outerWrapper = _this.$element.wrap('
    ').parent().append(wrapper.prepend(label), items, input); - - _this.elements = { - input : input, - items : items, - itemsScroll : itemsScroll, - wrapper : wrapper, - label : label, - outerWrapper : outerWrapper - }; - - _this.$element - .on(_this.eventTriggers) - .wrap('
    '); - - _this.originalTabindex = _this.$element.prop('tabindex'); - _this.$element.prop('tabindex', false); - - _this.populate(); - _this.activate(); - - _this.utils.triggerCallback('Init', _this); - }, - - /** Activates the plugin */ - activate: function() { - var _this = this; - var originalWidth = _this.$element.width(); - - _this.utils.triggerCallback('BeforeActivate', _this); - - _this.elements.outerWrapper.prop('class', [ - _this.classes.wrapper, - _this.$element.prop('class').replace(/\S+/g, _this.classes.prefix + '-$&'), - _this.options.responsive ? _this.classes.responsive : '' - ].join(' ')); - - if ( _this.options.inheritOriginalWidth && originalWidth > 0 ) { - _this.elements.outerWrapper.width(originalWidth); - } - - if ( !_this.$element.prop('disabled') ) { - _this.state.enabled = true; - - // Not disabled, so... Removing disabled class - _this.elements.outerWrapper.removeClass(_this.classes.disabled); - - // Remove styles from items box - // Fix incorrect height when refreshed is triggered with fewer options - _this.$li = _this.elements.items.removeAttr('style').find('li'); - - _this.bindEvents(); - } else { - _this.elements.outerWrapper.addClass(_this.classes.disabled); - _this.elements.input.prop('disabled', true); - } - - _this.utils.triggerCallback('Activate', _this); - }, - - /** - * Generate classNames for elements - * - * @return {object} Classes object - */ - getClassNames: function() { - var _this = this; - var customClass = _this.options.customClass; - var classesObj = {}; - - $.each(classList.split(' '), function(i, currClass) { - var c = customClass.prefix + currClass; - classesObj[currClass.toLowerCase()] = customClass.camelCase ? c : _this.utils.toDash(c); - }); - - classesObj.prefix = customClass.prefix; - - return classesObj; - }, - - /** Set the label text */ - setLabel: function() { - var _this = this; - var labelBuilder = _this.options.labelBuilder; - var currItem = _this.lookupItems[_this.state.currValue]; - - _this.elements.label.html( - $.isFunction(labelBuilder) - ? labelBuilder(currItem) - : _this.utils.format(labelBuilder, currItem) - ); - }, - - /** Get and save the available options */ - populate: function() { - var _this = this; - var $options = _this.$element.children(); - var $justOptions = _this.$element.find('option'); - var selectedIndex = $justOptions.index($justOptions.filter(':selected')); - var currIndex = 0; - - _this.state.currValue = (_this.state.selected = ~selectedIndex ? selectedIndex : 0); - _this.state.selectedIdx = _this.state.currValue; - _this.items = []; - _this.lookupItems = []; - - if ( $options.length ) { - // Build options markup - $options.each(function(i) { - var $elm = $(this); - - if ( $elm.is('optgroup') ) { - - var optionsGroup = { - element : $elm, - label : $elm.prop('label'), - groupDisabled : $elm.prop('disabled'), - items : [] - }; - - $elm.children().each(function(i) { - var $elm = $(this); - var optionText = $elm.html(); - - optionsGroup.items[i] = { - index : currIndex, - element : $elm, - value : $elm.val(), - text : optionText, - slug : _this.utils.replaceDiacritics(optionText), - disabled : optionsGroup.groupDisabled - }; - - _this.lookupItems[currIndex] = optionsGroup.items[i]; - - currIndex++; - }); - - _this.items[i] = optionsGroup; - - } else { - - var optionText = $elm.html(); - - _this.items[i] = { - index : currIndex, - element : $elm, - value : $elm.val(), - text : optionText, - slug : _this.utils.replaceDiacritics(optionText), - disabled : $elm.prop('disabled') - }; - - _this.lookupItems[currIndex] = _this.items[i]; - - currIndex++; - - } - }); - - _this.setLabel(); - _this.elements.items.append( _this.elements.itemsScroll.html( _this.getItemsMarkup(_this.items) ) ); - } - }, - - /** - * Generate options markup - * - * @param {object} items - Object containing all available options - * @return {string} HTML for the options box - */ - getItemsMarkup: function(items) { - var _this = this; - var markup = '
      '; - - $.each(items, function(i, elm) { - if ( elm.label !== undefined ) { - - markup += _this.utils.format('
      • {3}
      • ', - $.trim([_this.classes.group, elm.groupDisabled ? 'disabled' : '', elm.element.prop('class')].join(' ')), - _this.classes.grouplabel, - elm.element.prop('label') - ); - - $.each(elm.items, function(i, elm) { - markup += _this.getItemMarkup(elm.index, elm); - }); - - markup += '
      '; - - } else { - - markup += _this.getItemMarkup(elm.index, elm); - - } - }); - - return markup + '
    '; - }, - - /** - * Generate every option markup - * - * @param {number} i - Index of current item - * @param {object} elm - Current item - * @return {string} HTML for the option - */ - getItemMarkup: function(i, elm) { - var _this = this; - var itemBuilder = _this.options.optionsItemBuilder; - - return _this.utils.format('
  • {3}
  • ', - i, - $.trim([ - i === _this.state.currValue ? 'selected' : '', - i === _this.items.length - 1 ? 'last' : '', - elm.disabled ? 'disabled' : '' - ].join(' ')), - $.isFunction(itemBuilder) ? itemBuilder(elm, elm.element, i) : _this.utils.format(itemBuilder, elm) - ); - }, - - /** Bind events on the elements */ - bindEvents: function() { - var _this = this; - - _this.elements.wrapper - .add(_this.$element) - .add(_this.elements.outerWrapper) - .add(_this.elements.input) - .off(bindSufix); - - _this.elements.outerWrapper.on('mouseenter' + bindSufix + ' mouseleave' + bindSufix, function(e) { - $(this).toggleClass(_this.classes.hover, e.type === 'mouseenter'); - - // Delay close effect when openOnHover is true - if ( _this.options.openOnHover ) { - clearTimeout(_this.closeTimer); - - if ( e.type === 'mouseleave' ) { - _this.closeTimer = setTimeout($.proxy(_this.close, _this), _this.options.hoverIntentTimeout); - } else { - _this.open(); - } - } - }); - - // Toggle open/close - _this.elements.wrapper.on('click' + bindSufix, function(e) { - _this.state.opened ? _this.close() : _this.open(e); - }); - - _this.elements.input - .prop({ tabindex: _this.originalTabindex, disabled: false }) - .on('keydown' + bindSufix, $.proxy(_this.handleKeys, _this)) - .on('focusin' + bindSufix, function(e) { - _this.elements.outerWrapper.addClass(_this.classes.focus); - - // Prevent the flicker when focusing out and back again in the browser window - _this.elements.input.one('blur', function() { - _this.elements.input.blur(); - }); - - if ( _this.options.openOnFocus && !_this.state.opened ) { - _this.open(e); - } - }) - .on('focusout' + bindSufix, function() { - _this.elements.outerWrapper.removeClass(_this.classes.focus); - }) - .on('input propertychange', function() { - var val = _this.elements.input.val(); - - // Clear search - clearTimeout(_this.resetStr); - _this.resetStr = setTimeout(function() { - _this.elements.input.val(''); - }, _this.options.keySearchTimeout); - - if ( val.length ) { - // Search in select options - $.each(_this.items, function(i, elm) { - if ( RegExp('^' + _this.utils.escapeRegExp(val), 'i').test(elm.slug) && !elm.disabled ) { - _this.select(i); - return false; - } - }); - } - }); - - _this.$li.on({ - // Prevent blur on Chrome - mousedown: function(e) { - e.preventDefault(); - e.stopPropagation(); - }, - click: function() { - // The second parameter is to close the box after click - _this.select($(this).data('index'), true); - - // Chrome doesn't close options box if select is wrapped with a label - // We need to 'return false' to avoid that - return false; - } - }); - }, - - /** - * Behavior when keyboard keys is pressed - * - * @param {object} e - Event object - */ - handleKeys: function(e) { - var _this = this; - var key = e.keyCode || e.which; - var keys = _this.options.keys; - - var isPrev = $.inArray(key, keys.previous) > -1; - var isNext = $.inArray(key, keys.next) > -1; - var isSelect = $.inArray(key, keys.select) > -1; - var isOpen = $.inArray(key, keys.open) > -1; - var idx = _this.state.selectedIdx; - var isFirstOrLastItem = (isPrev && idx === 0) || (isNext && (idx + 1) === _this.items.length); - var goToItem = 0; - - // Enter / Space - if ( key === 13 || key === 32 ) { - e.preventDefault(); - } - - // If it's a directional key - if ( isPrev || isNext ) { - if ( !_this.options.allowWrap && isFirstOrLastItem ) { - return; - } - - if ( isPrev ) { - goToItem = _this.utils.previousEnabledItem(_this.items, idx); - } - - if ( isNext ) { - goToItem = _this.utils.nextEnabledItem(_this.items, idx); - } - - _this.select(goToItem); - } - - // Tab / Enter / ESC - if ( isSelect && _this.state.opened ) { - _this.select(idx, true); - return; - } - - // Space / Enter / Left / Up / Right / Down - if ( isOpen && !_this.state.opened ) { - _this.open(); - } - }, - - /** Update the items object */ - refresh: function() { - var _this = this; - - _this.populate(); - _this.activate(); - _this.utils.triggerCallback('Refresh', _this); - }, - - /** Set options box width/height */ - setOptionsDimensions: function() { - var _this = this; - - // Calculate options box height - // Set a temporary class on the hidden parent of the element - var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow); - var maxHeight = _this.options.maxHeight; - var itemsWidth = _this.elements.items.outerWidth(); - var wrapperWidth = _this.elements.wrapper.outerWidth() - (itemsWidth - _this.elements.items.width()); - - // Set the dimensions, minimum is wrapper width, expand for long items if option is true - if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth ) { - _this.finalWidth = wrapperWidth; - } else { - // Make sure the scrollbar width is included - _this.elements.items.css('overflow', 'scroll'); - - // Set a really long width for _this.elements.outerWrapper - _this.elements.outerWrapper.width(9e4); - _this.finalWidth = _this.elements.items.width(); - // Set scroll bar to auto - _this.elements.items.css('overflow', ''); - _this.elements.outerWrapper.width(''); - } - - _this.elements.items.width(_this.finalWidth).height() > maxHeight && _this.elements.items.height(maxHeight); - - // Remove the temporary class - hiddenChildren.removeClass(_this.classes.tempshow); - }, - - /** Detect if the options box is inside the window */ - isInViewport: function() { - var _this = this; - var scrollTop = $win.scrollTop(); - var winHeight = $win.height(); - var uiPosX = _this.elements.outerWrapper.offset().top; - var uiHeight = _this.elements.outerWrapper.outerHeight(); - - var fitsDown = (uiPosX + uiHeight + _this.itemsHeight) <= (scrollTop + winHeight); - var fitsAbove = (uiPosX - _this.itemsHeight) > scrollTop; - - // If it does not fit below, only render it - // above it fit's there. - // It's acceptable that the user needs to - // scroll the viewport to see the cut off UI - var renderAbove = !fitsDown && fitsAbove; - - _this.elements.outerWrapper.toggleClass(_this.classes.above, renderAbove); - }, - - /** - * Detect if currently selected option is visible and scroll the options box to show it - * - * @param {number} index - Index of the selected items - */ - detectItemVisibility: function(index) { - var _this = this; - var liHeight = _this.$li.eq(index).outerHeight(); - var liTop = _this.$li[index].offsetTop; - var itemsScrollTop = _this.elements.itemsScroll.scrollTop(); - var scrollT = liTop + liHeight * 2; - - _this.elements.itemsScroll.scrollTop( - scrollT > itemsScrollTop + _this.itemsHeight ? scrollT - _this.itemsHeight : - liTop - liHeight < itemsScrollTop ? liTop - liHeight : - itemsScrollTop - ); - }, - - /** - * Open the select options box - * - * @param {event} e - Event - */ - open: function(e) { - var _this = this; - - _this.utils.triggerCallback('BeforeOpen', _this); - - if ( e ) { - e.preventDefault(); - e.stopPropagation(); - } - - if ( _this.state.enabled ) { - _this.setOptionsDimensions(); - - // Find any other opened instances of select and close it - $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close'); - - _this.state.opened = true; - _this.itemsHeight = _this.elements.items.outerHeight(); - _this.itemsInnerHeight = _this.elements.items.height(); - - // Toggle options box visibility - _this.elements.outerWrapper.addClass(_this.classes.open); - - // Give dummy input focus - _this.elements.input.val(''); - if ( e && e.type !== 'focusin' ) { - _this.elements.input.focus(); - } - - $doc - .on('click' + bindSufix, $.proxy(_this.close, _this)) - .on('scroll' + bindSufix, $.proxy(_this.isInViewport, _this)); - _this.isInViewport(); - - // Prevent window scroll when using mouse wheel inside items box - if ( _this.options.preventWindowScroll ) { - /* istanbul ignore next */ - $doc.on('mousewheel' + bindSufix + ' DOMMouseScroll' + bindSufix, '.' + _this.classes.scroll, function(e) { - var orgEvent = e.originalEvent; - var scrollTop = $(this).scrollTop(); - var deltaY = 0; - - if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } - if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } - if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } - if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; } - - if ( scrollTop === (this.scrollHeight - _this.itemsInnerHeight) && deltaY < 0 || scrollTop === 0 && deltaY > 0 ) { - e.preventDefault(); - } - }); - } - - _this.detectItemVisibility(_this.state.selectedIdx); - - _this.utils.triggerCallback('Open', _this); - } - }, - - /** Close the select options box */ - close: function() { - var _this = this; - - _this.utils.triggerCallback('BeforeClose', _this); - - _this.change(); - - // Remove custom events on document - $doc.off(bindSufix); - - // Remove visible class to hide options box - _this.elements.outerWrapper.removeClass(_this.classes.open); - - _this.state.opened = false; - - _this.utils.triggerCallback('Close', _this); - }, - - /** Select current option and change the label */ - change: function() { - var _this = this; - - _this.utils.triggerCallback('BeforeChange', _this); - - if ( _this.state.currValue !== _this.state.selectedIdx ) { - // Apply changed value to original select - _this.$element - .prop('selectedIndex', _this.state.currValue = _this.state.selectedIdx) - .data('value', _this.lookupItems[_this.state.selectedIdx].text); - - // Change label text - _this.setLabel(); - } - - _this.utils.triggerCallback('Change', _this); - }, - - /** - * Select option - * - * @param {number} index - Index of the option that will be selected - * @param {boolean} close - Close the options box after selecting - */ - select: function(index, close) { - var _this = this; - - // Parameter index is required - if ( index === undefined ) { - return; - } - - // If element is disabled, can't select it - if ( !_this.lookupItems[index].disabled ) { - _this.$li.filter('[data-index]') - .removeClass('selected') - .eq(_this.state.selectedIdx = index) - .addClass('selected'); - - _this.detectItemVisibility(index); - - // If 'close' is false (default), the options box won't close after - // each selected item, this is necessary for keyboard navigation - if ( close ) { - _this.close(); - } - } - }, - - /** - * Unbind and remove - * - * @param {boolean} preserveData - Check if the data on the element should be removed too - */ - destroy: function(preserveData) { - var _this = this; - - if ( _this.state && _this.state.enabled ) { - _this.elements.items.add(_this.elements.wrapper).add(_this.elements.input).remove(); - - if ( !preserveData ) { - _this.$element.removeData(pluginName).removeData('value'); - } - - _this.$element.prop('tabindex', _this.originalTabindex).off(bindSufix).off(_this.eventTriggers).unwrap().unwrap(); - - _this.state.enabled = false; - } - } - }; - - // A really lightweight plugin wrapper around the constructor, - // preventing against multiple instantiations - $.fn[pluginName] = function(args) { - return this.each(function() { - var data = $.data(this, pluginName); - - if ( data && !data.disableOnMobile ) { - (typeof args === 'string' && data[args]) ? data[args]() : data.init(args); - } else { - $.data(this, pluginName, new Selectric(this, args)); - } - }); - }; - - /** - * Hooks for the callbacks - * - * @type {object} - */ - $.fn[pluginName].hooks = { - /** - * @param {string} callbackName - The callback name. - * @param {string} hookName - The name of the hook to be attached. - * @param {function} fn - Callback function. - */ - add: function(callbackName, hookName, fn) { - if ( !this[callbackName] ) { - this[callbackName] = {}; - } - - this[callbackName][hookName] = fn; - }, - - /** - * @param {string} callbackName - The callback name. - * @param {string} hookName - The name of the hook that will be removed. - */ - remove: function(callbackName, hookName) { - delete this[callbackName][hookName]; - } - }; - - /** - * Default plugin options - * - * @type {object} - */ - $.fn[pluginName].defaults = { - onChange : function(elm) { $(elm).change(); }, - maxHeight : 300, - keySearchTimeout : 500, - arrowButtonMarkup : '', - disableOnMobile : true, - openOnFocus : true, - openOnHover : false, - hoverIntentTimeout : 500, - expandToItemText : false, - responsive : false, - preventWindowScroll : true, - inheritOriginalWidth : false, - allowWrap : true, - optionsItemBuilder : '{text}', // function(itemData, element, index) - labelBuilder : '{text}', // function(currItem) - keys : { - previous : [37, 38], // Left / Up - next : [39, 40], // Right / Down - select : [9, 13, 27], // Tab / Enter / Escape - open : [13, 32, 37, 38, 39, 40], // Enter / Space / Left / Up / Right / Down - close : [9, 27] // Tab / Escape - }, - customClass : { - prefix: pluginName, - camelCase: false - } - }; -})); \ No newline at end of file diff --git a/public/js/jquery.selectric.min.js b/public/js/jquery.selectric.min.js deleted file mode 100644 index 9dc6077..0000000 --- a/public/js/jquery.selectric.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! Selectric ϟ v1.10.1 (2016-06-30) - git.io/tjl9sQ - Copyright (c) 2016 Leonardo Santos - MIT License */ -!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof module&&module.exports?module.exports=function(t,s){return void 0===s&&(s="undefined"!=typeof window?require("jquery"):require("jquery")(t)),e(s),s}:e(jQuery)}(function(e){"use strict";var t=e(document),s=e(window),i="selectric",n="Input Items Open Disabled TempShow HideSelect Wrapper Focus Hover Responsive Above Scroll Group GroupLabel",l=".sl",o=["a","e","i","o","u","n","c","y"],a=[/[\xE0-\xE5]/g,/[\xE8-\xEB]/g,/[\xEC-\xEF]/g,/[\xF2-\xF6]/g,/[\xF9-\xFC]/g,/[\xF1]/g,/[\xE7]/g,/[\xFD-\xFF]/g],r=function(t,s){var i=this;i.element=t,i.$element=e(t),i.state={enabled:!1,opened:!1,currValue:-1,selectedIdx:-1},i.eventTriggers={open:i.open,close:i.close,destroy:i.destroy,refresh:i.refresh,init:i.init},i.init(s)};r.prototype={utils:{isMobile:function(){return/android|ip(hone|od|ad)/i.test(navigator.userAgent)},escapeRegExp:function(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")},replaceDiacritics:function(e){for(var t=a.length;t--;)e=e.toLowerCase().replace(a[t],o[t]);return e},format:function(e){var t=arguments;return(""+e).replace(/\{(?:(\d+)|(\w+))\}/g,function(e,s,i){return i&&t[1]?t[1][i]:t[s]})},nextEnabledItem:function(e,t){for(;e[t=(t+1)%e.length].disabled;);return t},previousEnabledItem:function(e,t){for(;e[t=(t>0?t:e.length)-1].disabled;);return t},toDash:function(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()},triggerCallback:function(t,s){var n=s.element,l=s.options["on"+t];e.isFunction(l)&&l.call(n,n,s),e.fn[i].hooks[t]&&e.each(e.fn[i].hooks[t],function(){this.call(n,n,s)}),e(n).trigger(i+"-"+this.toDash(t),s)}},init:function(t){var s=this;if(s.options=e.extend(!0,{},e.fn[i].defaults,s.options,t),s.utils.triggerCallback("BeforeInit",s),s.destroy(!0),s.options.disableOnMobile&&s.utils.isMobile())return void(s.disableOnMobile=!0);s.classes=s.getClassNames();var n=e("",{"class":s.classes.input,readonly:s.utils.isMobile()}),l=e("
    ",{"class":s.classes.items,tabindex:-1}),o=e("
    ",{"class":s.classes.scroll}),a=e("
    ",{"class":s.classes.prefix,html:s.options.arrowButtonMarkup}),r=e("",{"class":"label"}),p=s.$element.wrap("
    ").parent().append(a.prepend(r),l,n);s.elements={input:n,items:l,itemsScroll:o,wrapper:a,label:r,outerWrapper:p},s.$element.on(s.eventTriggers).wrap('
    '),s.originalTabindex=s.$element.prop("tabindex"),s.$element.prop("tabindex",!1),s.populate(),s.activate(),s.utils.triggerCallback("Init",s)},activate:function(){var e=this,t=e.$element.width();e.utils.triggerCallback("BeforeActivate",e),e.elements.outerWrapper.prop("class",[e.classes.wrapper,e.$element.prop("class").replace(/\S+/g,e.classes.prefix+"-$&"),e.options.responsive?e.classes.responsive:""].join(" ")),e.options.inheritOriginalWidth&&t>0&&e.elements.outerWrapper.width(t),e.$element.prop("disabled")?(e.elements.outerWrapper.addClass(e.classes.disabled),e.elements.input.prop("disabled",!0)):(e.state.enabled=!0,e.elements.outerWrapper.removeClass(e.classes.disabled),e.$li=e.elements.items.removeAttr("style").find("li"),e.bindEvents()),e.utils.triggerCallback("Activate",e)},getClassNames:function(){var t=this,s=t.options.customClass,i={};return e.each(n.split(" "),function(e,n){var l=s.prefix+n;i[n.toLowerCase()]=s.camelCase?l:t.utils.toDash(l)}),i.prefix=s.prefix,i},setLabel:function(){var t=this,s=t.options.labelBuilder,i=t.lookupItems[t.state.currValue];t.elements.label.html(e.isFunction(s)?s(i):t.utils.format(s,i))},populate:function(){var t=this,s=t.$element.children(),i=t.$element.find("option"),n=i.index(i.filter(":selected")),l=0;t.state.currValue=t.state.selected=~n?n:0,t.state.selectedIdx=t.state.currValue,t.items=[],t.lookupItems=[],s.length&&(s.each(function(s){var i=e(this);if(i.is("optgroup")){var n={element:i,label:i.prop("label"),groupDisabled:i.prop("disabled"),items:[]};i.children().each(function(s){var i=e(this),o=i.html();n.items[s]={index:l,element:i,value:i.val(),text:o,slug:t.utils.replaceDiacritics(o),disabled:n.groupDisabled},t.lookupItems[l]=n.items[s],l++}),t.items[s]=n}else{var o=i.html();t.items[s]={index:l,element:i,value:i.val(),text:o,slug:t.utils.replaceDiacritics(o),disabled:i.prop("disabled")},t.lookupItems[l]=t.items[s],l++}}),t.setLabel(),t.elements.items.append(t.elements.itemsScroll.html(t.getItemsMarkup(t.items))))},getItemsMarkup:function(t){var s=this,i="
      ";return e.each(t,function(t,n){void 0!==n.label?(i+=s.utils.format('
      • {3}
      • ',e.trim([s.classes.group,n.groupDisabled?"disabled":"",n.element.prop("class")].join(" ")),s.classes.grouplabel,n.element.prop("label")),e.each(n.items,function(e,t){i+=s.getItemMarkup(t.index,t)}),i+="
      "):i+=s.getItemMarkup(n.index,n)}),i+"
    "},getItemMarkup:function(t,s){var i=this,n=i.options.optionsItemBuilder;return i.utils.format('
  • {3}
  • ',t,e.trim([t===i.state.currValue?"selected":"",t===i.items.length-1?"last":"",s.disabled?"disabled":""].join(" ")),e.isFunction(n)?n(s,s.element,t):i.utils.format(n,s))},bindEvents:function(){var t=this;t.elements.wrapper.add(t.$element).add(t.elements.outerWrapper).add(t.elements.input).off(l),t.elements.outerWrapper.on("mouseenter"+l+" mouseleave"+l,function(s){e(this).toggleClass(t.classes.hover,"mouseenter"===s.type),t.options.openOnHover&&(clearTimeout(t.closeTimer),"mouseleave"===s.type?t.closeTimer=setTimeout(e.proxy(t.close,t),t.options.hoverIntentTimeout):t.open())}),t.elements.wrapper.on("click"+l,function(e){t.state.opened?t.close():t.open(e)}),t.elements.input.prop({tabindex:t.originalTabindex,disabled:!1}).on("keydown"+l,e.proxy(t.handleKeys,t)).on("focusin"+l,function(e){t.elements.outerWrapper.addClass(t.classes.focus),t.elements.input.one("blur",function(){t.elements.input.blur()}),t.options.openOnFocus&&!t.state.opened&&t.open(e)}).on("focusout"+l,function(){t.elements.outerWrapper.removeClass(t.classes.focus)}).on("input propertychange",function(){var s=t.elements.input.val();clearTimeout(t.resetStr),t.resetStr=setTimeout(function(){t.elements.input.val("")},t.options.keySearchTimeout),s.length&&e.each(t.items,function(e,i){if(RegExp("^"+t.utils.escapeRegExp(s),"i").test(i.slug)&&!i.disabled)return t.select(e),!1})}),t.$li.on({mousedown:function(e){e.preventDefault(),e.stopPropagation()},click:function(){return t.select(e(this).data("index"),!0),!1}})},handleKeys:function(t){var s=this,i=t.keyCode||t.which,n=s.options.keys,l=e.inArray(i,n.previous)>-1,o=e.inArray(i,n.next)>-1,a=e.inArray(i,n.select)>-1,r=e.inArray(i,n.open)>-1,p=s.state.selectedIdx,u=l&&0===p||o&&p+1===s.items.length,c=0;if(13!==i&&32!==i||t.preventDefault(),l||o){if(!s.options.allowWrap&&u)return;l&&(c=s.utils.previousEnabledItem(s.items,p)),o&&(c=s.utils.nextEnabledItem(s.items,p)),s.select(c)}return a&&s.state.opened?void s.select(p,!0):void(r&&!s.state.opened&&s.open())},refresh:function(){var e=this;e.populate(),e.activate(),e.utils.triggerCallback("Refresh",e)},setOptionsDimensions:function(){var e=this,t=e.elements.items.closest(":visible").children(":hidden").addClass(e.classes.tempshow),s=e.options.maxHeight,i=e.elements.items.outerWidth(),n=e.elements.wrapper.outerWidth()-(i-e.elements.items.width());!e.options.expandToItemText||n>i?e.finalWidth=n:(e.elements.items.css("overflow","scroll"),e.elements.outerWrapper.width(9e4),e.finalWidth=e.elements.items.width(),e.elements.items.css("overflow",""),e.elements.outerWrapper.width("")),e.elements.items.width(e.finalWidth).height()>s&&e.elements.items.height(s),t.removeClass(e.classes.tempshow)},isInViewport:function(){var e=this,t=s.scrollTop(),i=s.height(),n=e.elements.outerWrapper.offset().top,l=e.elements.outerWrapper.outerHeight(),o=n+l+e.itemsHeight<=t+i,a=n-e.itemsHeight>t,r=!o&&a;e.elements.outerWrapper.toggleClass(e.classes.above,r)},detectItemVisibility:function(e){var t=this,s=t.$li.eq(e).outerHeight(),i=t.$li[e].offsetTop,n=t.elements.itemsScroll.scrollTop(),l=i+2*s;t.elements.itemsScroll.scrollTop(l>n+t.itemsHeight?l-t.itemsHeight:i-s0)&&t.preventDefault()}),n.detectItemVisibility(n.state.selectedIdx),n.utils.triggerCallback("Open",n))},close:function(){var e=this;e.utils.triggerCallback("BeforeClose",e),e.change(),t.off(l),e.elements.outerWrapper.removeClass(e.classes.open),e.state.opened=!1,e.utils.triggerCallback("Close",e)},change:function(){var e=this;e.utils.triggerCallback("BeforeChange",e),e.state.currValue!==e.state.selectedIdx&&(e.$element.prop("selectedIndex",e.state.currValue=e.state.selectedIdx).data("value",e.lookupItems[e.state.selectedIdx].text),e.setLabel()),e.utils.triggerCallback("Change",e)},select:function(e,t){var s=this;void 0!==e&&(s.lookupItems[e].disabled||(s.$li.filter("[data-index]").removeClass("selected").eq(s.state.selectedIdx=e).addClass("selected"),s.detectItemVisibility(e),t&&s.close()))},destroy:function(e){var t=this;t.state&&t.state.enabled&&(t.elements.items.add(t.elements.wrapper).add(t.elements.input).remove(),e||t.$element.removeData(i).removeData("value"),t.$element.prop("tabindex",t.originalTabindex).off(l).off(t.eventTriggers).unwrap().unwrap(),t.state.enabled=!1)}},e.fn[i]=function(t){return this.each(function(){var s=e.data(this,i);s&&!s.disableOnMobile?"string"==typeof t&&s[t]?s[t]():s.init(t):e.data(this,i,new r(this,t))})},e.fn[i].hooks={add:function(e,t,s){this[e]||(this[e]={}),this[e][t]=s},remove:function(e,t){delete this[e][t]}},e.fn[i].defaults={onChange:function(t){e(t).change()},maxHeight:300,keySearchTimeout:500,arrowButtonMarkup:'',disableOnMobile:!0,openOnFocus:!0,openOnHover:!1,hoverIntentTimeout:500,expandToItemText:!1,responsive:!1,preventWindowScroll:!0,inheritOriginalWidth:!1,allowWrap:!0,optionsItemBuilder:"{text}",labelBuilder:"{text}",keys:{previous:[37,38],next:[39,40],select:[9,13,27],open:[13,32,37,38,39,40],close:[9,27]},customClass:{prefix:i,camelCase:!1}}}); \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js index abaa8cb..272ac5f 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -149,8 +149,8 @@ $(function(){ }) .always(function(){ self.loading(false); }); }; - Task.prototype.consoleMouseOver = function(){ this.autoScrollOutput = false; } - Task.prototype.consoleMouseOut = function(){ this.autoScrollOutput = true; } + Task.prototype.consoleMouseOver = function(){ this.autoScrollOutput = false; }; + Task.prototype.consoleMouseOut = function(){ this.autoScrollOutput = true; }; Task.prototype.resetOutput = function(){ this.viewOutputLine = 0; this.autoScrollOutput = true; @@ -170,7 +170,7 @@ $(function(){ self.viewOutputLine += output.length; if (self.autoScrollOutput){ var $console = $("#console_" + self.uuid); - $console.scrollTop($console[0].scrollHeight - $console.height()) + $console.scrollTop($console[0].scrollHeight - $console.height()); } } }) @@ -243,8 +243,8 @@ $(function(){ self.info({error: url + " is unreachable."}); self.stopRefreshingInfo(); }); - } - }; + }; + } Task.prototype.cancel = genApiCall("/task/cancel"); Task.prototype.restart = genApiCall("/task/restart", function(task){ task.resetOutput(); @@ -266,7 +266,8 @@ $(function(){ uploadAsync: false, uploadExtraData: function(){ return { - name: $("#taskName").val() + name: $("#taskName").val(), + options: JSON.stringify(optionsModel.getUserOptions()) }; } }); @@ -299,12 +300,22 @@ $(function(){ function Option(name, params){ this.name = name; this.params = params; + this.value = ko.observable(); } + Option.prototype.resetToDefault = function(){ + this.value(undefined); + }; function OptionsModel(){ var self = this; this.options = ko.observableArray(); + this.options.subscribe(function(){ + setTimeout(function(){ + $('#options [data-toggle="tooltip"]').tooltip(); + }, 100); + }); + this.showOptions = ko.observable(false); this.error = ko.observable(); $.get("/getOptions") @@ -314,16 +325,25 @@ $(function(){ for (var optionName in json){ self.options.push(new Option(optionName, json[optionName])); } - - $('select').selectric({ - maxHeight: 500 - }); } }) .fail(function(){ self.error("options are not available."); - }) + }); } + OptionsModel.prototype.getUserOptions = function(){ + var result = []; + for (var i = 0; i < this.options().length; i++){ + var opt = this.options()[i]; + if (opt.value() !== undefined){ + result.push({ + name: opt.name, + value: opt.value() + }); + } + } + return result; + }; var optionsModel = new OptionsModel(); ko.applyBindings(optionsModel, document.getElementById("options")); From 6e56c31ab16eefbe36df1c0d146374e1506b056b Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 28 Jul 2016 11:28:18 -0500 Subject: [PATCH 4/5] Options range checking, filtering, minor name changes --- libs/odmOptions.js | 127 ++++++++++++++++++++++++++++++++++++++++----- public/index.html | 10 ++-- public/js/main.js | 11 ++-- 3 files changed, 125 insertions(+), 23 deletions(-) diff --git a/libs/odmOptions.js b/libs/odmOptions.js index 5610f29..287ae24 100644 --- a/libs/odmOptions.js +++ b/libs/odmOptions.js @@ -17,20 +17,21 @@ along with this program. If not, see . */ "use strict"; let odmRunner = require('./odmRunner'); +let assert = require('assert'); -let options = null; +let odmOptions = null; module.exports = { getOptions: function(done){ - if (options){ - done(null, options); + if (odmOptions){ + done(null, odmOptions); return; } odmRunner.getJsonOptions((err, json) => { if (err) done(err); else{ - options = {}; + odmOptions = []; for (let option in json){ // Not all options are useful to the end user // (num cores can be set programmatically, so can gcpFile, etc.) @@ -39,8 +40,8 @@ module.exports = { "--start-with", "--odm_georeferencing-gcpFile", "--end-with"].indexOf(option) !== -1) continue; let values = json[option]; - option = option.replace(/^--/, ""); + let name = option.replace(/^--/, ""); let type = ""; let value = ""; let help = values.help || ""; @@ -80,11 +81,11 @@ module.exports = { help = help.replace(/\%\(default\)s/g, value); - options[option] = { - type, value, domain, help - }; + odmOptions.push({ + name, type, value, domain, help + }); } - done(null, options); + done(null, odmOptions); } }); }, @@ -94,14 +95,116 @@ module.exports = { // The result of filtering is passed back via callback // @param options[] filterOptions: function(options, done){ + assert(odmOptions !== null, "odmOptions is not set. Have you initialized odmOptions properly?"); + try{ if (typeof options === "string") options = JSON.parse(options); + + let result = []; + let errors = []; + function addError(opt, descr){ + errors.push({ + name: opt.name, + error: descr + }); + } - // TODO: range checks, filtering + let typeConversion = { + 'float': Number.parseFloat, + 'int': Number.parseInt, + 'bool': function(value){ + if (value === 'true') return true; + else if (value === 'false') return false; + else if (typeof value === 'boolean') return value; + else throw new Error(`Cannot convert ${value} to boolean`); + } + }; + + let domainChecks = [ + { + regex: /^(positive |negative )?(integer|float)$/, + validate: function(matches, value){ + if (matches[1] === 'positive ') return value >= 0; + else if (matches[1] === 'negative ') return value <= 0; + + else if (matches[2] === 'integer') return Number.isInteger(value); + else if (matches[2] === 'float') return Number.isFinite(value); + } + }, + { + regex: /^percent$/, + validate: function(matches, value){ + return value >= 0 && value <= 100; + } + }, + { + regex: /^(float): ([\-\+\.\d]+) <= x <= ([\-\+\.\d]+)$/, + validate: function(matches, value){ + let [str, type, lower, upper] = matches; + lower = parseFloat(lower); + upper = parseFloat(upper); + return value >= lower && value <= upper; + } + }, + { + regex: /^(float) (>=|>|<|<=) ([\-\+\.\d]+)$/, + validate: function(matches, value){ + let [str, type, oper, bound] = matches; + bound = parseFloat(bound); + switch(oper){ + case '>=': + return value >= bound; + case '>': + return value > bound; + case '<=': + return value <= bound; + case '<': + return value < bound; + default: + return false; + } + } + } + ]; - done(null, options); + function checkDomain(domain, value){ + let dc, matches; + + if (dc = domainChecks.find(dc => { return matches = domain.match(dc.regex); })){ + if (!dc.validate(matches, value)) throw new Error(`Invalid value ${value} (out of range)`); + }else{ + throw new Error(`Domain value cannot be handled: '${domain}' : '${value}'`); + } + } + + // Scan through all possible options + for (let odmOption of odmOptions){ + // Was this option selected by the user? + let opt; + if (opt = options.find(o => { return o.name === odmOption.name; })){ + try{ + // Convert to proper data type + let value = typeConversion[odmOption.type](opt.value); + + // Domain check + if (odmOption.domain){ + checkDomain(odmOption.domain, value); + } + + result.push({ + name: odmOption.name, + value: value + }); + }catch(e){ + addError(opt, e.message); + } + } + } + + if (errors.length > 0) done(new Error(JSON.stringify(errors))); + else done(null, result); }catch(e){ done(e); } } -}; +}; \ No newline at end of file diff --git a/public/index.html b/public/index.html index d830827..21a2565 100644 --- a/public/index.html +++ b/public/index.html @@ -54,18 +54,18 @@
    -
    - - +
    + + - +
    - +

    diff --git a/public/js/main.js b/public/js/main.js index 272ac5f..310efcc 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -297,9 +297,8 @@ $(function(){ }); // Load options - function Option(name, params){ - this.name = name; - this.params = params; + function Option(properties){ + this.properties = properties; this.value = ko.observable(); } Option.prototype.resetToDefault = function(){ @@ -322,8 +321,8 @@ $(function(){ .done(function(json){ if (json.error) self.error(json.error); else{ - for (var optionName in json){ - self.options.push(new Option(optionName, json[optionName])); + for (var i in json){ + self.options.push(new Option(json[i])); } } }) @@ -337,7 +336,7 @@ $(function(){ var opt = this.options()[i]; if (opt.value() !== undefined){ result.push({ - name: opt.name, + name: opt.properties.name, value: opt.value() }); } From 032e20f3904f04003becfcd42b437f5c4807a1fd Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 28 Jul 2016 16:46:28 -0500 Subject: [PATCH 5/5] Code cleanup --- index.js | 4 ++-- libs/TaskManager.js | 8 +++----- libs/odmOptions.js | 4 ++-- libs/odmRunner.js | 16 ++++------------ 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 99006b6..5003f04 100644 --- a/index.js +++ b/index.js @@ -159,7 +159,7 @@ app.get('/getOptions', (req, res) => { let gracefulShutdown = done => { async.series([ - cb => { taskManager.dumpTaskList(cb); }, + cb => taskManager.dumpTaskList(cb), cb => { console.log("Closing server"); server.close(); @@ -180,7 +180,7 @@ let taskManager; let server; async.series([ - cb => { taskManager = new TaskManager(cb); }, + cb => taskManager = new TaskManager(cb), cb => { server = app.listen(3000, err => { if (!err) console.log('Server has started on port 3000'); cb(err); diff --git a/libs/TaskManager.js b/libs/TaskManager.js index 7552852..b2e3e42 100644 --- a/libs/TaskManager.js +++ b/libs/TaskManager.js @@ -33,8 +33,8 @@ module.exports = class TaskManager{ this.runningQueue = []; async.series([ - cb => { this.restoreTaskListFromDump(cb); }, - cb => { this.removeOldTasks(cb); }, + cb => this.restoreTaskListFromDump(cb), + cb => this.removeOldTasks(cb), cb => { this.processNextTask(); cb(); @@ -136,9 +136,7 @@ module.exports = class TaskManager{ removeFromRunningQueue(task){ assert(task.constructor.name === "Task", "Must be a Task object"); - this.runningQueue = this.runningQueue.filter(t => { - return t !== task; - }); + this.runningQueue = this.runningQueue.filter(t => t !== task); } addNew(task){ diff --git a/libs/odmOptions.js b/libs/odmOptions.js index 287ae24..b8d346b 100644 --- a/libs/odmOptions.js +++ b/libs/odmOptions.js @@ -170,7 +170,7 @@ module.exports = { function checkDomain(domain, value){ let dc, matches; - if (dc = domainChecks.find(dc => { return matches = domain.match(dc.regex); })){ + if (dc = domainChecks.find(dc => matches = domain.match(dc.regex))){ if (!dc.validate(matches, value)) throw new Error(`Invalid value ${value} (out of range)`); }else{ throw new Error(`Domain value cannot be handled: '${domain}' : '${value}'`); @@ -181,7 +181,7 @@ module.exports = { for (let odmOption of odmOptions){ // Was this option selected by the user? let opt; - if (opt = options.find(o => { return o.name === odmOption.name; })){ + if (opt = options.find(o => o.name === odmOption.name)){ try{ // Convert to proper data type let value = typeConversion[odmOption.type](opt.value); diff --git a/libs/odmRunner.js b/libs/odmRunner.js index 281433c..7299c7f 100644 --- a/libs/odmRunner.js +++ b/libs/odmRunner.js @@ -31,17 +31,11 @@ module.exports = { ], {cwd: ODM_PATH}); childProcess - .on('exit', (code, signal) => { - done(null, code, signal); - }) + .on('exit', (code, signal) => done(null, code, signal)) .on('error', done); - childProcess.stdout.on('data', chunk => { - outputReceived(chunk.toString()); - }); - childProcess.stderr.on('data', chunk => { - outputReceived(chunk.toString()); - }); + childProcess.stdout.on('data', chunk => outputReceived(chunk.toString())); + childProcess.stderr.on('data', chunk => outputReceived(chunk.toString())); return childProcess; }, @@ -63,9 +57,7 @@ module.exports = { }) .on('error', done); - let processOutput = chunk => { - output.push(chunk.toString()); - }; + let processOutput = chunk => output.push(chunk.toString()); childProcess.stdout.on('data', processOutput); childProcess.stderr.on('data', processOutput);