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"));