From a769b5425f0fa617447291045a382773010b10b7 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 25 Jul 2016 20:10:18 -0500 Subject: [PATCH] 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")); });