From 1df8c044ee1e2490a3dd2c2532dc703d7baddaad Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 15 Jun 2018 11:27:24 +0100 Subject: [PATCH 01/27] Add component for auto-completing inputs. --- .../coffee/components/inputSuggestions.coffee | 49 +++++++++++++++++++ .../components/input-suggestions.less | 21 ++++++++ 2 files changed, 70 insertions(+) create mode 100644 services/web/public/coffee/components/inputSuggestions.coffee create mode 100644 services/web/public/stylesheets/components/input-suggestions.less diff --git a/services/web/public/coffee/components/inputSuggestions.coffee b/services/web/public/coffee/components/inputSuggestions.coffee new file mode 100644 index 0000000000..c5bfbbf933 --- /dev/null +++ b/services/web/public/coffee/components/inputSuggestions.coffee @@ -0,0 +1,49 @@ +define [ + "base" +], (App) -> + inputSuggestionsController = ($scope, $element, $attrs) -> + ctrl = @ + ctrl.showHint = false + ctrl.hasFocus = false + ctrl.handleFocus = () -> + ctrl.hasFocus = true + ctrl.suggestion = null + ctrl.handleBlur = () -> + ctrl.showHint = false + ctrl.hasFocus = false + ctrl.suggestion = null + ctrl.handleKeyDown = ($event) -> + if ($event.which == 9 or $event.which == 13) and ctrl.suggestion? and ctrl.suggestion != "" + $event.preventDefault() + ctrl.localNgModel += ctrl.suggestion + ctrl.suggestion = null + ctrl.showHint = false + + $scope.$watch "$ctrl.localNgModel", (newVal, oldVal) -> + if ctrl.hasFocus and newVal != oldVal + ctrl.suggestion = null + ctrl.showHint = false + ctrl.getSuggestion({ userInput: newVal }) + .then (suggestion) -> + if suggestion? and newVal == ctrl.localNgModel + ctrl.showHint = true + ctrl.suggestion = suggestion.replace newVal, "" + .catch () -> ctrl.suggestion = null + return + + App.component "inputSuggestions", { + bindings: + localNgModel: "=ngModel" + getSuggestion: "&" + inputId: "@" + inputPlaceholder: "@" + controller: inputSuggestionsController + template: """ +
+
+ +
+ +
+ """ + } diff --git a/services/web/public/stylesheets/components/input-suggestions.less b/services/web/public/stylesheets/components/input-suggestions.less new file mode 100644 index 0000000000..bf2fd4a253 --- /dev/null +++ b/services/web/public/stylesheets/components/input-suggestions.less @@ -0,0 +1,21 @@ +.input-suggestions { + position: relative; + height: @input-height-base; +} + .input-suggestions-main { + position: absolute; + top: 0; + background-color: transparent; + } + + .input-suggestions-shadow { + background-color: @input-bg; + padding-top: 4px; + } + .input-suggestions-shadow-existing { + color: transparent; + } + + .input-suggestions-shadow-suggested { + color: lighten(@input-color, 25%); + } \ No newline at end of file From a8cb126c4b817573acacff6459496f0a601f4f5a Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 15 Jun 2018 11:28:44 +0100 Subject: [PATCH 02/27] Add UI Select (enhanced select boxes) components. --- services/web/public/coffee/base.coffee | 7 +- services/web/public/coffee/libraries.coffee | 1 + services/web/public/coffee/main.coffee | 3 + services/web/public/js/libs/select/select.css | 362 +++ services/web/public/js/libs/select/select.js | 2427 +++++++++++++++++ .../web/public/js/libs/select/select.min.css | 7 + .../public/js/libs/select/select.min.css.map | 1 + .../web/public/js/libs/select/select.min.js | 9 + .../public/js/libs/select/select.min.js.map | 1 + .../public/stylesheets/_style_includes.less | 3 + .../stylesheets/components/ui-select.less | 51 + 11 files changed, 2871 insertions(+), 1 deletion(-) create mode 100755 services/web/public/js/libs/select/select.css create mode 100755 services/web/public/js/libs/select/select.js create mode 100755 services/web/public/js/libs/select/select.min.css create mode 100755 services/web/public/js/libs/select/select.min.css.map create mode 100755 services/web/public/js/libs/select/select.min.js create mode 100755 services/web/public/js/libs/select/select.min.js.map create mode 100644 services/web/public/stylesheets/components/ui-select.less diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index c3847c9342..370823703a 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -17,14 +17,19 @@ define [ "ErrorCatcher" "localStorage" "ngTagsInput" - ]).config ($qProvider, sixpackProvider, $httpProvider)-> + "ui.select" + ]).config ($qProvider, sixpackProvider, $httpProvider, uiSelectConfig) -> $qProvider.errorOnUnhandledRejections(false) + uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin' sixpackProvider.setOptions({ debug: false baseUrl: window.sharelatex.sixpackDomain client_id: window.user_id }) + App.run ($templateCache) -> + $templateCache.put "bootstrap/match.tpl.html", "
{{$select.placeholder}}
" + sl_debugging = window.location?.search?.match(/debug=true/)? window.sl_console = log: (args...) -> console.log(args...) if sl_debugging diff --git a/services/web/public/coffee/libraries.coffee b/services/web/public/coffee/libraries.coffee index e4532d7187..bddc5a2ac9 100644 --- a/services/web/public/coffee/libraries.coffee +++ b/services/web/public/coffee/libraries.coffee @@ -11,5 +11,6 @@ define [ "libs/sixpack" "libs/angular-sixpack" "libs/ng-tags-input-3.0.0" + "libs/select/select" ], () -> diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 786bf1a94b..a52a490491 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -20,6 +20,8 @@ define [ "main/subscription/team-invite-controller" "main/contact-us" "main/learn" + "main/affiliations/controllers/UserAffiliationsController" + "main/affiliations/factories/UserAffiliationsDataService" "analytics/AbTestingManager" "directives/asyncForm" "directives/stopPropagation" @@ -34,6 +36,7 @@ define [ "services/queued-http" "services/validateCaptcha" "filters/formatDate" + "components/inputSuggestions" "__MAIN_CLIENTSIDE_INCLUDES__" ], () -> angular.module('SharelatexApp').config( diff --git a/services/web/public/js/libs/select/select.css b/services/web/public/js/libs/select/select.css new file mode 100755 index 0000000000..0e713bb1b9 --- /dev/null +++ b/services/web/public/js/libs/select/select.css @@ -0,0 +1,362 @@ +/*! + * ui-select + * http://github.com/angular-ui/ui-select + * Version: 0.19.7 - 2017-04-15T14:28:36.790Z + * License: MIT + */ + + +/* Style when highlighting a search. */ +.ui-select-highlight { + font-weight: bold; +} + +.ui-select-offscreen { + clip: rect(0 0 0 0) !important; + width: 1px !important; + height: 1px !important; + border: 0 !important; + margin: 0 !important; + padding: 0 !important; + overflow: hidden !important; + position: absolute !important; + outline: 0 !important; + left: 0px !important; + top: 0px !important; +} + + +.ui-select-choices-row:hover { + background-color: #f5f5f5; +} + +/* Select2 theme */ + +/* Mark invalid Select2 */ +.ng-dirty.ng-invalid > a.select2-choice { + border-color: #D44950; +} + +.select2-result-single { + padding-left: 0; +} + +.select2-locked > .select2-search-choice-close{ + display:none; +} + +.select-locked > .ui-select-match-close{ + display:none; +} + +body > .select2-container.open { + z-index: 9999; /* The z-index Select2 applies to the select2-drop */ +} + +/* Handle up direction Select2 */ +.ui-select-container[theme="select2"].direction-up .ui-select-match, +.ui-select-container.select2.direction-up .ui-select-match { + border-radius: 4px; /* FIXME hardcoded value :-/ */ + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.ui-select-container[theme="select2"].direction-up .ui-select-dropdown, +.ui-select-container.select2.direction-up .ui-select-dropdown { + border-radius: 4px; /* FIXME hardcoded value :-/ */ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + + border-top-width: 1px; /* FIXME hardcoded value :-/ */ + border-top-style: solid; + + box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); + + margin-top: -4px; /* FIXME hardcoded value :-/ */ +} +.ui-select-container[theme="select2"].direction-up .ui-select-dropdown .select2-search, +.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search { + margin-top: 4px; /* FIXME hardcoded value :-/ */ +} +.ui-select-container[theme="select2"].direction-up.select2-dropdown-open .ui-select-match, +.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match { + border-bottom-color: #5897fb; +} + +.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden, +.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden input{ + opacity: 0; + height: 0; + min-height: 0; + padding: 0; + margin: 0; + border:0; +} + +/* Selectize theme */ + +/* Helper class to show styles when focus */ +.selectize-input.selectize-focus{ + border-color: #007FBB !important; +} + +/* Fix input width for Selectize theme */ +.selectize-control.single > .selectize-input > input { + width: 100%; +} + +/* Fix line break when there's at least one item selected with the Selectize theme */ +.selectize-control.multi > .selectize-input > input { + margin: 0 !important; +} + +/* Fix dropdown width for Selectize theme */ +.selectize-control > .selectize-dropdown { + width: 100%; +} + +/* Mark invalid Selectize */ +.ng-dirty.ng-invalid > div.selectize-input { + border-color: #D44950; +} + +/* Handle up direction Selectize */ +.ui-select-container[theme="selectize"].direction-up .ui-select-dropdown { + box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); + margin-top: -2px; /* FIXME hardcoded value :-/ */ +} + +.ui-select-container[theme="selectize"] input.ui-select-search-hidden{ + opacity: 0; + height: 0; + min-height: 0; + padding: 0; + margin: 0; + border:0; + width: 0; +} + +/* Bootstrap theme */ + +/* Helper class to show styles when focus */ +.btn-default-focus { + color: #333; + background-color: #EBEBEB; + border-color: #ADADAD; + text-decoration: none; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.ui-select-bootstrap .ui-select-toggle { + position: relative; +} + +.ui-select-bootstrap .ui-select-toggle > .caret { + position: absolute; + height: 10px; + top: 50%; + right: 10px; + margin-top: -2px; +} + +/* Fix Bootstrap dropdown position when inside a input-group */ +.input-group > .ui-select-bootstrap.dropdown { + /* Instead of relative */ + position: static; +} + +.input-group > .ui-select-bootstrap > input.ui-select-search.form-control { + border-radius: 4px; /* FIXME hardcoded value :-/ */ + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up { + border-radius: 4px !important; /* FIXME hardcoded value :-/ */ + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.ui-select-bootstrap .ui-select-search-hidden{ + opacity: 0; + height: 0; + min-height: 0; + padding: 0; + margin: 0; + border:0; +} + +.ui-select-bootstrap > .ui-select-match > .btn{ + /* Instead of center because of .btn */ + text-align: left !important; +} + +.ui-select-bootstrap > .ui-select-match > .caret { + position: absolute; + top: 45%; + right: 15px; +} + +/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */ +.ui-select-bootstrap > .ui-select-choices ,.ui-select-bootstrap > .ui-select-no-choice { + width: 100%; + height: auto; + max-height: 200px; + overflow-x: hidden; + margin-top: -1px; +} + +body > .ui-select-bootstrap.open { + z-index: 1000; /* Standard Bootstrap dropdown z-index */ +} + +.ui-select-multiple.ui-select-bootstrap { + height: auto; + padding: 3px 3px 0 3px; +} + +.ui-select-multiple.ui-select-bootstrap input.ui-select-search { + background-color: transparent !important; /* To prevent double background when disabled */ + border: none; + outline: none; + height: 1.666666em; + margin-bottom: 3px; +} + +.ui-select-multiple.ui-select-bootstrap .ui-select-match .close { + font-size: 1.6em; + line-height: 0.75; +} + +.ui-select-multiple.ui-select-bootstrap .ui-select-match-item { + outline: 0; + margin: 0 3px 3px 0; +} + +.ui-select-multiple .ui-select-match-item { + position: relative; +} + +.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close { + pointer-events: none; +} + +.ui-select-multiple:hover .ui-select-match-item.dropping-before:before { + content: ""; + position: absolute; + top: 0; + right: 100%; + height: 100%; + margin-right: 2px; + border-left: 1px solid #428bca; +} + +.ui-select-multiple:hover .ui-select-match-item.dropping-after:after { + content: ""; + position: absolute; + top: 0; + left: 100%; + height: 100%; + margin-left: 2px; + border-right: 1px solid #428bca; +} + +.ui-select-bootstrap .ui-select-choices-row>span { + cursor: pointer; + display: block; + padding: 3px 20px; + clear: both; + font-weight: 400; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} + +.ui-select-bootstrap .ui-select-choices-row>span:hover, .ui-select-bootstrap .ui-select-choices-row>span:focus { + text-decoration: none; + color: #262626; + background-color: #f5f5f5; +} + +.ui-select-bootstrap .ui-select-choices-row.active>span { + color: #fff; + text-decoration: none; + outline: 0; + background-color: #428bca; +} + +.ui-select-bootstrap .ui-select-choices-row.disabled>span, +.ui-select-bootstrap .ui-select-choices-row.active.disabled>span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} + +/* fix hide/show angular animation */ +.ui-select-match.ng-hide-add, +.ui-select-search.ng-hide-add { + display: none !important; +} + +/* Mark invalid Bootstrap */ +.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match { + border-color: #D44950; +} + +/* Handle up direction Bootstrap */ +.ui-select-container[theme="bootstrap"].direction-up .ui-select-dropdown { + box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); +} + +.ui-select-bootstrap .ui-select-match-text { + width: 100%; + padding-right: 1em; +} +.ui-select-bootstrap .ui-select-match-text span { + display: inline-block; + width: 100%; + overflow: hidden; +} +.ui-select-bootstrap .ui-select-toggle > a.btn { + position: absolute; + height: 10px; + right: 10px; + margin-top: -2px; +} + +/* Spinner */ +.ui-select-refreshing.glyphicon { + position: absolute; + right: 0; + padding: 8px 27px; + } + +@-webkit-keyframes ui-select-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes ui-select-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +.ui-select-spin { + -webkit-animation: ui-select-spin 2s infinite linear; + animation: ui-select-spin 2s infinite linear; +} + +.ui-select-refreshing.ng-animate { + -webkit-animation: none 0s; +} diff --git a/services/web/public/js/libs/select/select.js b/services/web/public/js/libs/select/select.js new file mode 100755 index 0000000000..5ac4e0b702 --- /dev/null +++ b/services/web/public/js/libs/select/select.js @@ -0,0 +1,2427 @@ +/*! + * ui-select + * http://github.com/angular-ui/ui-select + * Version: 0.19.7 - 2017-04-15T14:28:36.649Z + * License: MIT + */ + + +(function () { +"use strict"; +var KEY = { + TAB: 9, + ENTER: 13, + ESC: 27, + SPACE: 32, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + SHIFT: 16, + CTRL: 17, + ALT: 18, + PAGE_UP: 33, + PAGE_DOWN: 34, + HOME: 36, + END: 35, + BACKSPACE: 8, + DELETE: 46, + COMMAND: 91, + + MAP: { 91 : "COMMAND", 8 : "BACKSPACE" , 9 : "TAB" , 13 : "ENTER" , 16 : "SHIFT" , 17 : "CTRL" , 18 : "ALT" , 19 : "PAUSEBREAK" , 20 : "CAPSLOCK" , 27 : "ESC" , 32 : "SPACE" , 33 : "PAGE_UP", 34 : "PAGE_DOWN" , 35 : "END" , 36 : "HOME" , 37 : "LEFT" , 38 : "UP" , 39 : "RIGHT" , 40 : "DOWN" , 43 : "+" , 44 : "PRINTSCREEN" , 45 : "INSERT" , 46 : "DELETE", 48 : "0" , 49 : "1" , 50 : "2" , 51 : "3" , 52 : "4" , 53 : "5" , 54 : "6" , 55 : "7" , 56 : "8" , 57 : "9" , 59 : ";", 61 : "=" , 65 : "A" , 66 : "B" , 67 : "C" , 68 : "D" , 69 : "E" , 70 : "F" , 71 : "G" , 72 : "H" , 73 : "I" , 74 : "J" , 75 : "K" , 76 : "L", 77 : "M" , 78 : "N" , 79 : "O" , 80 : "P" , 81 : "Q" , 82 : "R" , 83 : "S" , 84 : "T" , 85 : "U" , 86 : "V" , 87 : "W" , 88 : "X" , 89 : "Y" , 90 : "Z", 96 : "0" , 97 : "1" , 98 : "2" , 99 : "3" , 100 : "4" , 101 : "5" , 102 : "6" , 103 : "7" , 104 : "8" , 105 : "9", 106 : "*" , 107 : "+" , 109 : "-" , 110 : "." , 111 : "/", 112 : "F1" , 113 : "F2" , 114 : "F3" , 115 : "F4" , 116 : "F5" , 117 : "F6" , 118 : "F7" , 119 : "F8" , 120 : "F9" , 121 : "F10" , 122 : "F11" , 123 : "F12", 144 : "NUMLOCK" , 145 : "SCROLLLOCK" , 186 : ";" , 187 : "=" , 188 : "," , 189 : "-" , 190 : "." , 191 : "/" , 192 : "`" , 219 : "[" , 220 : "\\" , 221 : "]" , 222 : "'" + }, + + isControl: function (e) { + var k = e.which; + switch (k) { + case KEY.COMMAND: + case KEY.SHIFT: + case KEY.CTRL: + case KEY.ALT: + return true; + } + + if (e.metaKey || e.ctrlKey || e.altKey) return true; + + return false; + }, + isFunctionKey: function (k) { + k = k.which ? k.which : k; + return k >= 112 && k <= 123; + }, + isVerticalMovement: function (k){ + return ~[KEY.UP, KEY.DOWN].indexOf(k); + }, + isHorizontalMovement: function (k){ + return ~[KEY.LEFT,KEY.RIGHT,KEY.BACKSPACE,KEY.DELETE].indexOf(k); + }, + toSeparator: function (k) { + var sep = {ENTER:"\n",TAB:"\t",SPACE:" "}[k]; + if (sep) return sep; + // return undefined for special keys other than enter, tab or space. + // no way to use them to cut strings. + return KEY[k] ? undefined : k; + } + }; + +function isNil(value) { + return angular.isUndefined(value) || value === null; +} + +/** + * Add querySelectorAll() to jqLite. + * + * jqLite find() is limited to lookups by tag name. + * TODO This will change with future versions of AngularJS, to be removed when this happens + * + * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586 + * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598 + */ +if (angular.element.prototype.querySelectorAll === undefined) { + angular.element.prototype.querySelectorAll = function(selector) { + return angular.element(this[0].querySelectorAll(selector)); + }; +} + +/** + * Add closest() to jqLite. + */ +if (angular.element.prototype.closest === undefined) { + angular.element.prototype.closest = function( selector) { + var elem = this[0]; + var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector; + + while (elem) { + if (matchesSelector.bind(elem)(selector)) { + return elem; + } else { + elem = elem.parentElement; + } + } + return false; + }; +} + +var latestId = 0; + +var uis = angular.module('ui.select', []) + +.constant('uiSelectConfig', { + theme: 'bootstrap', + searchEnabled: true, + sortable: false, + placeholder: '', // Empty by default, like HTML tag "); + $compile(focusser)(scope); + $select.focusser = focusser; + + //Input that will handle focus + $select.focusInput = focusser; + + element.parent().append(focusser); + focusser.bind("focus", function(){ + scope.$evalAsync(function(){ + $select.focus = true; + }); + }); + focusser.bind("blur", function(){ + scope.$evalAsync(function(){ + $select.focus = false; + }); + }); + focusser.bind("keydown", function(e){ + + if (e.which === KEY.BACKSPACE && $select.backspaceReset !== false) { + e.preventDefault(); + e.stopPropagation(); + $select.select(undefined); + scope.$apply(); + return; + } + + if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { + return; + } + + if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){ + e.preventDefault(); + e.stopPropagation(); + $select.activate(); + } + + scope.$digest(); + }); + + focusser.bind("keyup input", function(e){ + + if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) { + return; + } + + $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input + focusser.val(''); + scope.$digest(); + + }); + + + } + }; +}]); + +// Make multiple matches sortable +uis.directive('uiSelectSort', ['$timeout', 'uiSelectConfig', 'uiSelectMinErr', function($timeout, uiSelectConfig, uiSelectMinErr) { + return { + require: ['^^uiSelect', '^ngModel'], + link: function(scope, element, attrs, ctrls) { + if (scope[attrs.uiSelectSort] === null) { + throw uiSelectMinErr('sort', 'Expected a list to sort'); + } + + var $select = ctrls[0]; + var $ngModel = ctrls[1]; + + var options = angular.extend({ + axis: 'horizontal' + }, + scope.$eval(attrs.uiSelectSortOptions)); + + var axis = options.axis; + var draggingClassName = 'dragging'; + var droppingClassName = 'dropping'; + var droppingBeforeClassName = 'dropping-before'; + var droppingAfterClassName = 'dropping-after'; + + scope.$watch(function(){ + return $select.sortable; + }, function(newValue){ + if (newValue) { + element.attr('draggable', true); + } else { + element.removeAttr('draggable'); + } + }); + + element.on('dragstart', function(event) { + element.addClass(draggingClassName); + + (event.dataTransfer || event.originalEvent.dataTransfer).setData('text', scope.$index.toString()); + }); + + element.on('dragend', function() { + removeClass(draggingClassName); + }); + + var move = function(from, to) { + /*jshint validthis: true */ + this.splice(to, 0, this.splice(from, 1)[0]); + }; + + var removeClass = function(className) { + angular.forEach($select.$element.querySelectorAll('.' + className), function(el){ + angular.element(el).removeClass(className); + }); + }; + + var dragOverHandler = function(event) { + event.preventDefault(); + + var offset = axis === 'vertical' ? event.offsetY || event.layerY || (event.originalEvent ? event.originalEvent.offsetY : 0) : event.offsetX || event.layerX || (event.originalEvent ? event.originalEvent.offsetX : 0); + + if (offset < (this[axis === 'vertical' ? 'offsetHeight' : 'offsetWidth'] / 2)) { + removeClass(droppingAfterClassName); + element.addClass(droppingBeforeClassName); + + } else { + removeClass(droppingBeforeClassName); + element.addClass(droppingAfterClassName); + } + }; + + var dropTimeout; + + var dropHandler = function(event) { + event.preventDefault(); + + var droppedItemIndex = parseInt((event.dataTransfer || event.originalEvent.dataTransfer).getData('text'), 10); + + // prevent event firing multiple times in firefox + $timeout.cancel(dropTimeout); + dropTimeout = $timeout(function() { + _dropHandler(droppedItemIndex); + }, 20); + }; + + var _dropHandler = function(droppedItemIndex) { + var theList = scope.$eval(attrs.uiSelectSort); + var itemToMove = theList[droppedItemIndex]; + var newIndex = null; + + if (element.hasClass(droppingBeforeClassName)) { + if (droppedItemIndex < scope.$index) { + newIndex = scope.$index - 1; + } else { + newIndex = scope.$index; + } + } else { + if (droppedItemIndex < scope.$index) { + newIndex = scope.$index; + } else { + newIndex = scope.$index + 1; + } + } + + move.apply(theList, [droppedItemIndex, newIndex]); + + $ngModel.$setViewValue(Date.now()); + + scope.$apply(function() { + scope.$emit('uiSelectSort:change', { + array: theList, + item: itemToMove, + from: droppedItemIndex, + to: newIndex + }); + }); + + removeClass(droppingClassName); + removeClass(droppingBeforeClassName); + removeClass(droppingAfterClassName); + + element.off('drop', dropHandler); + }; + + element.on('dragenter', function() { + if (element.hasClass(draggingClassName)) { + return; + } + + element.addClass(droppingClassName); + + element.on('dragover', dragOverHandler); + element.on('drop', dropHandler); + }); + + element.on('dragleave', function(event) { + if (event.target != element) { + return; + } + + removeClass(droppingClassName); + removeClass(droppingBeforeClassName); + removeClass(droppingAfterClassName); + + element.off('dragover', dragOverHandler); + element.off('drop', dropHandler); + }); + } + }; +}]); + +uis.directive('uisOpenClose', ['$parse', '$timeout', function ($parse, $timeout) { + return { + restrict: 'A', + require: 'uiSelect', + link: function (scope, element, attrs, $select) { + $select.onOpenCloseCallback = $parse(attrs.uisOpenClose); + + scope.$watch('$select.open', function (isOpen, previousState) { + if (isOpen !== previousState) { + $timeout(function () { + $select.onOpenCloseCallback(scope, { + isOpen: isOpen + }); + }); + } + }); + } + }; +}]); + +/** + * Parses "repeat" attribute. + * + * Taken from AngularJS ngRepeat source code + * See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L211 + * + * Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat: + * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697 + */ + +uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) { + var self = this; + + /** + * Example: + * expression = "address in addresses | filter: {street: $select.search} track by $index" + * itemName = "address", + * source = "addresses | filter: {street: $select.search}", + * trackByExp = "$index", + */ + self.parse = function(expression) { + + + var match; + //var isObjectCollection = /\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)/.test(expression); + // If an array is used as collection + + // if (isObjectCollection){ + // 000000000000000000000000000000111111111000000000000000222222222222220033333333333333333333330000444444444444444444000000000000000055555555555000000000000000000000066666666600000000 + match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(\s*[\s\S]+?)?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + // 1 Alias + // 2 Item + // 3 Key on (key,value) + // 4 Value on (key,value) + // 5 Source expression (including filters) + // 6 Track by + + if (!match) { + throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + expression); + } + + var source = match[5], + filters = ''; + + // When using (key,value) ui-select requires filters to be extracted, since the object + // is converted to an array for $select.items + // (in which case the filters need to be reapplied) + if (match[3]) { + // Remove any enclosing parenthesis + source = match[5].replace(/(^\()|(\)$)/g, ''); + // match all after | but not after || + var filterMatch = match[5].match(/^\s*(?:[\s\S]+?)(?:[^\|]|\|\|)+([\s\S]*)\s*$/); + if(filterMatch && filterMatch[1].trim()) { + filters = filterMatch[1]; + source = source.replace(filters, ''); + } + } + + return { + itemName: match[4] || match[2], // (lhs) Left-hand side, + keyName: match[3], //for (key, value) syntax + source: $parse(source), + filters: filters, + trackByExp: match[6], + modelMapper: $parse(match[1] || match[4] || match[2]), + repeatExpression: function (grouped) { + var expression = this.itemName + ' in ' + (grouped ? '$group.items' : '$select.items'); + if (this.trackByExp) { + expression += ' track by ' + this.trackByExp; + } + return expression; + } + }; + + }; + + self.getGroupNgRepeatExpression = function() { + return '$group in $select.groups track by $group.name'; + }; + +}]); + +}()); +angular.module("ui.select").run(["$templateCache", function($templateCache) {$templateCache.put("bootstrap/choices.tpl.html",""); +$templateCache.put("bootstrap/match-multiple.tpl.html"," × "); +$templateCache.put("bootstrap/match.tpl.html","
{{$select.placeholder}}
"); +$templateCache.put("bootstrap/no-choice.tpl.html",""); +$templateCache.put("bootstrap/select-multiple.tpl.html","
"); +$templateCache.put("bootstrap/select.tpl.html","
"); +$templateCache.put("select2/choices.tpl.html",""); +$templateCache.put("select2/match-multiple.tpl.html","
  • "); +$templateCache.put("select2/match.tpl.html","{{$select.placeholder}} "); +$templateCache.put("select2/no-choice.tpl.html","
    "); +$templateCache.put("select2/select-multiple.tpl.html","
    "); +$templateCache.put("select2/select.tpl.html","
    "); +$templateCache.put("selectize/choices.tpl.html","
    "); +$templateCache.put("selectize/match-multiple.tpl.html","
    ×
    "); +$templateCache.put("selectize/match.tpl.html","
    {{$select.placeholder}}
    "); +$templateCache.put("selectize/no-choice.tpl.html","
    "); +$templateCache.put("selectize/select-multiple.tpl.html","
    "); +$templateCache.put("selectize/select.tpl.html","
    ");}]); \ No newline at end of file diff --git a/services/web/public/js/libs/select/select.min.css b/services/web/public/js/libs/select/select.min.css new file mode 100755 index 0000000000..39a10f4bde --- /dev/null +++ b/services/web/public/js/libs/select/select.min.css @@ -0,0 +1,7 @@ +/*! + * ui-select + * http://github.com/angular-ui/ui-select + * Version: 0.19.7 - 2017-04-15T14:28:36.790Z + * License: MIT + */.ui-select-highlight{font-weight:700}.ui-select-offscreen{clip:rect(0 0 0 0)!important;width:1px!important;height:1px!important;border:0!important;margin:0!important;padding:0!important;overflow:hidden!important;position:absolute!important;outline:0!important;left:0!important;top:0!important}.selectize-control.single>.selectize-input>input,.selectize-control>.selectize-dropdown{width:100%}.ui-select-choices-row:hover{background-color:#f5f5f5}.ng-dirty.ng-invalid>a.select2-choice{border-color:#D44950}.select2-result-single{padding-left:0}.select-locked>.ui-select-match-close,.select2-locked>.select2-search-choice-close{display:none}body>.select2-container.open{z-index:9999}.ui-select-container.select2.direction-up .ui-select-match,.ui-select-container[theme=select2].direction-up .ui-select-match{border-radius:0 0 4px 4px}.ui-select-container.select2.direction-up .ui-select-dropdown,.ui-select-container[theme=select2].direction-up .ui-select-dropdown{border-radius:4px 4px 0 0;border-top-width:1px;border-top-style:solid;box-shadow:0 -4px 8px rgba(0,0,0,.25);margin-top:-4px}.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search,.ui-select-container[theme=select2].direction-up .ui-select-dropdown .select2-search{margin-top:4px}.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match,.ui-select-container[theme=select2].direction-up.select2-dropdown-open .ui-select-match{border-bottom-color:#5897fb}.ui-select-container[theme=select2] .ui-select-dropdown .ui-select-search-hidden,.ui-select-container[theme=select2] .ui-select-dropdown .ui-select-search-hidden input{opacity:0;height:0;min-height:0;padding:0;margin:0;border:0}.selectize-input.selectize-focus{border-color:#007FBB!important}.selectize-control.multi>.selectize-input>input{margin:0!important}.ng-dirty.ng-invalid>div.selectize-input{border-color:#D44950}.ui-select-container[theme=selectize].direction-up .ui-select-dropdown{box-shadow:0 -4px 8px rgba(0,0,0,.25);margin-top:-2px}.ui-select-container[theme=selectize] input.ui-select-search-hidden{opacity:0;height:0;min-height:0;padding:0;margin:0;border:0;width:0}.btn-default-focus{color:#333;background-color:#EBEBEB;border-color:#ADADAD;text-decoration:none;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.ui-select-bootstrap .ui-select-toggle{position:relative}.ui-select-bootstrap .ui-select-toggle>.caret{position:absolute;height:10px;top:50%;right:10px;margin-top:-2px}.input-group>.ui-select-bootstrap.dropdown{position:static}.input-group>.ui-select-bootstrap>input.ui-select-search.form-control{border-radius:4px 0 0 4px}.input-group>.ui-select-bootstrap>input.ui-select-search.form-control.direction-up{border-radius:4px 0 0 4px!important}.ui-select-bootstrap .ui-select-search-hidden{opacity:0;height:0;min-height:0;padding:0;margin:0;border:0}.ui-select-bootstrap>.ui-select-match>.btn{text-align:left!important}.ui-select-bootstrap>.ui-select-match>.caret{position:absolute;top:45%;right:15px}.ui-select-bootstrap>.ui-select-choices,.ui-select-bootstrap>.ui-select-no-choice{width:100%;height:auto;max-height:200px;overflow-x:hidden;margin-top:-1px}body>.ui-select-bootstrap.open{z-index:1000}.ui-select-multiple.ui-select-bootstrap{height:auto;padding:3px 3px 0}.ui-select-multiple.ui-select-bootstrap input.ui-select-search{background-color:transparent!important;border:none;outline:0;height:1.666666em;margin-bottom:3px}.ui-select-multiple.ui-select-bootstrap .ui-select-match .close{font-size:1.6em;line-height:.75}.ui-select-multiple.ui-select-bootstrap .ui-select-match-item{outline:0;margin:0 3px 3px 0}.ui-select-multiple .ui-select-match-item{position:relative}.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close{pointer-events:none}.ui-select-multiple:hover .ui-select-match-item.dropping-before:before{content:"";position:absolute;top:0;right:100%;height:100%;margin-right:2px;border-left:1px solid #428bca}.ui-select-multiple:hover .ui-select-match-item.dropping-after:after{content:"";position:absolute;top:0;left:100%;height:100%;margin-left:2px;border-right:1px solid #428bca}.ui-select-bootstrap .ui-select-choices-row>span{cursor:pointer;display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.ui-select-bootstrap .ui-select-choices-row>span:focus,.ui-select-bootstrap .ui-select-choices-row>span:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.ui-select-bootstrap .ui-select-choices-row.active>span{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.ui-select-bootstrap .ui-select-choices-row.active.disabled>span,.ui-select-bootstrap .ui-select-choices-row.disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.ui-select-match.ng-hide-add,.ui-select-search.ng-hide-add{display:none!important}.ui-select-bootstrap.ng-dirty.ng-invalid>button.btn.ui-select-match{border-color:#D44950}.ui-select-container[theme=bootstrap].direction-up .ui-select-dropdown{box-shadow:0 -4px 8px rgba(0,0,0,.25)}.ui-select-bootstrap .ui-select-match-text{width:100%;padding-right:1em}.ui-select-bootstrap .ui-select-match-text span{display:inline-block;width:100%;overflow:hidden}.ui-select-bootstrap .ui-select-toggle>a.btn{position:absolute;height:10px;right:10px;margin-top:-2px}.ui-select-refreshing.glyphicon{position:absolute;right:0;padding:8px 27px}@-webkit-keyframes ui-select-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes ui-select-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.ui-select-spin{-webkit-animation:ui-select-spin 2s infinite linear;animation:ui-select-spin 2s infinite linear}.ui-select-refreshing.ng-animate{-webkit-animation:none 0s} +/*# sourceMappingURL=select.min.css.map */ diff --git a/services/web/public/js/libs/select/select.min.css.map b/services/web/public/js/libs/select/select.min.css.map new file mode 100755 index 0000000000..477a68739c --- /dev/null +++ b/services/web/public/js/libs/select/select.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["select.css","common.css"],"names":[],"mappings":"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","file":"select.min.css","sourcesContent":["/*!\n * ui-select\n * http://github.com/angular-ui/ui-select\n * Version: 0.19.7 - 2017-04-15T14:28:36.790Z\n * License: MIT\n */\n\n\n/* Style when highlighting a search. */\n.ui-select-highlight {\n font-weight: bold;\n}\n\n.ui-select-offscreen {\n clip: rect(0 0 0 0) !important;\n width: 1px !important;\n height: 1px !important;\n border: 0 !important;\n margin: 0 !important;\n padding: 0 !important;\n overflow: hidden !important;\n position: absolute !important;\n outline: 0 !important;\n left: 0px !important;\n top: 0px !important;\n}\n\n\n.ui-select-choices-row:hover {\n background-color: #f5f5f5;\n}\n\n/* Select2 theme */\n\n/* Mark invalid Select2 */\n.ng-dirty.ng-invalid > a.select2-choice {\n border-color: #D44950;\n}\n\n.select2-result-single {\n padding-left: 0;\n}\n\n.select2-locked > .select2-search-choice-close{\n display:none;\n}\n\n.select-locked > .ui-select-match-close{\n display:none;\n}\n\nbody > .select2-container.open {\n z-index: 9999; /* The z-index Select2 applies to the select2-drop */\n}\n\n/* Handle up direction Select2 */\n.ui-select-container[theme=\"select2\"].direction-up .ui-select-match,\n.ui-select-container.select2.direction-up .ui-select-match {\n border-radius: 4px; /* FIXME hardcoded value :-/ */\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.ui-select-container[theme=\"select2\"].direction-up .ui-select-dropdown,\n.ui-select-container.select2.direction-up .ui-select-dropdown {\n border-radius: 4px; /* FIXME hardcoded value :-/ */\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n\n border-top-width: 1px; /* FIXME hardcoded value :-/ */\n border-top-style: solid;\n\n box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);\n\n margin-top: -4px; /* FIXME hardcoded value :-/ */\n}\n.ui-select-container[theme=\"select2\"].direction-up .ui-select-dropdown .select2-search,\n.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search {\n margin-top: 4px; /* FIXME hardcoded value :-/ */\n}\n.ui-select-container[theme=\"select2\"].direction-up.select2-dropdown-open .ui-select-match,\n.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match {\n border-bottom-color: #5897fb;\n}\n\n.ui-select-container[theme=\"select2\"] .ui-select-dropdown .ui-select-search-hidden,\n.ui-select-container[theme=\"select2\"] .ui-select-dropdown .ui-select-search-hidden input{\n opacity: 0;\n height: 0;\n min-height: 0;\n padding: 0;\n margin: 0;\n border:0;\n}\n\n/* Selectize theme */\n\n/* Helper class to show styles when focus */\n.selectize-input.selectize-focus{\n border-color: #007FBB !important;\n}\n\n/* Fix input width for Selectize theme */\n.selectize-control.single > .selectize-input > input {\n width: 100%;\n}\n\n/* Fix line break when there's at least one item selected with the Selectize theme */\n.selectize-control.multi > .selectize-input > input {\n margin: 0 !important;\n}\n\n/* Fix dropdown width for Selectize theme */\n.selectize-control > .selectize-dropdown {\n width: 100%;\n}\n\n/* Mark invalid Selectize */\n.ng-dirty.ng-invalid > div.selectize-input {\n border-color: #D44950;\n}\n\n/* Handle up direction Selectize */\n.ui-select-container[theme=\"selectize\"].direction-up .ui-select-dropdown {\n box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);\n margin-top: -2px; /* FIXME hardcoded value :-/ */\n}\n\n.ui-select-container[theme=\"selectize\"] input.ui-select-search-hidden{\n opacity: 0;\n height: 0;\n min-height: 0;\n padding: 0;\n margin: 0;\n border:0;\n width: 0;\n}\n\n/* Bootstrap theme */\n\n/* Helper class to show styles when focus */\n.btn-default-focus {\n color: #333;\n background-color: #EBEBEB;\n border-color: #ADADAD;\n text-decoration: none;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n\n.ui-select-bootstrap .ui-select-toggle {\n position: relative;\n}\n\n.ui-select-bootstrap .ui-select-toggle > .caret {\n position: absolute;\n height: 10px;\n top: 50%;\n right: 10px;\n margin-top: -2px;\n}\n\n/* Fix Bootstrap dropdown position when inside a input-group */\n.input-group > .ui-select-bootstrap.dropdown {\n /* Instead of relative */\n position: static;\n}\n\n.input-group > .ui-select-bootstrap > input.ui-select-search.form-control {\n border-radius: 4px; /* FIXME hardcoded value :-/ */\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up {\n border-radius: 4px !important; /* FIXME hardcoded value :-/ */\n border-top-right-radius: 0 !important;\n border-bottom-right-radius: 0 !important;\n}\n\n.ui-select-bootstrap .ui-select-search-hidden{\n opacity: 0;\n height: 0;\n min-height: 0;\n padding: 0;\n margin: 0;\n border:0;\n}\n\n.ui-select-bootstrap > .ui-select-match > .btn{\n /* Instead of center because of .btn */\n text-align: left !important;\n}\n\n.ui-select-bootstrap > .ui-select-match > .caret {\n position: absolute;\n top: 45%;\n right: 15px;\n}\n\n/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */\n.ui-select-bootstrap > .ui-select-choices ,.ui-select-bootstrap > .ui-select-no-choice {\n width: 100%;\n height: auto;\n max-height: 200px;\n overflow-x: hidden;\n margin-top: -1px;\n}\n\nbody > .ui-select-bootstrap.open {\n z-index: 1000; /* Standard Bootstrap dropdown z-index */\n}\n\n.ui-select-multiple.ui-select-bootstrap {\n height: auto;\n padding: 3px 3px 0 3px;\n}\n\n.ui-select-multiple.ui-select-bootstrap input.ui-select-search {\n background-color: transparent !important; /* To prevent double background when disabled */\n border: none;\n outline: none;\n height: 1.666666em;\n margin-bottom: 3px;\n}\n\n.ui-select-multiple.ui-select-bootstrap .ui-select-match .close {\n font-size: 1.6em;\n line-height: 0.75;\n}\n\n.ui-select-multiple.ui-select-bootstrap .ui-select-match-item {\n outline: 0;\n margin: 0 3px 3px 0;\n}\n\n.ui-select-multiple .ui-select-match-item {\n position: relative;\n}\n\n.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close {\n pointer-events: none;\n}\n\n.ui-select-multiple:hover .ui-select-match-item.dropping-before:before {\n content: \"\";\n position: absolute;\n top: 0;\n right: 100%;\n height: 100%;\n margin-right: 2px;\n border-left: 1px solid #428bca;\n}\n\n.ui-select-multiple:hover .ui-select-match-item.dropping-after:after {\n content: \"\";\n position: absolute;\n top: 0;\n left: 100%;\n height: 100%;\n margin-left: 2px;\n border-right: 1px solid #428bca;\n}\n\n.ui-select-bootstrap .ui-select-choices-row>span {\n cursor: pointer;\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333;\n white-space: nowrap;\n}\n\n.ui-select-bootstrap .ui-select-choices-row>span:hover, .ui-select-bootstrap .ui-select-choices-row>span:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n\n.ui-select-bootstrap .ui-select-choices-row.active>span {\n color: #fff;\n text-decoration: none;\n outline: 0;\n background-color: #428bca;\n}\n\n.ui-select-bootstrap .ui-select-choices-row.disabled>span,\n.ui-select-bootstrap .ui-select-choices-row.active.disabled>span {\n color: #777;\n cursor: not-allowed;\n background-color: #fff;\n}\n\n/* fix hide/show angular animation */\n.ui-select-match.ng-hide-add,\n.ui-select-search.ng-hide-add {\n display: none !important;\n}\n\n/* Mark invalid Bootstrap */\n.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match {\n border-color: #D44950;\n}\n\n/* Handle up direction Bootstrap */\n.ui-select-container[theme=\"bootstrap\"].direction-up .ui-select-dropdown {\n box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);\n}\n\n.ui-select-bootstrap .ui-select-match-text {\n width: 100%;\n padding-right: 1em;\n}\n.ui-select-bootstrap .ui-select-match-text span {\n display: inline-block;\n width: 100%;\n overflow: hidden;\n}\n.ui-select-bootstrap .ui-select-toggle > a.btn {\n position: absolute;\n height: 10px;\n right: 10px;\n margin-top: -2px;\n}\n\n/* Spinner */\n.ui-select-refreshing.glyphicon {\n position: absolute;\n right: 0;\n padding: 8px 27px;\n }\n\n@-webkit-keyframes ui-select-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg);\n }\n}\n@keyframes ui-select-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg);\n }\n}\n\n.ui-select-spin {\n -webkit-animation: ui-select-spin 2s infinite linear;\n animation: ui-select-spin 2s infinite linear;\n}\n\n.ui-select-refreshing.ng-animate {\n -webkit-animation: none 0s;\n}\n","/* Style when highlighting a search. */\n.ui-select-highlight {\n font-weight: bold;\n}\n\n.ui-select-offscreen {\n clip: rect(0 0 0 0) !important;\n width: 1px !important;\n height: 1px !important;\n border: 0 !important;\n margin: 0 !important;\n padding: 0 !important;\n overflow: hidden !important;\n position: absolute !important;\n outline: 0 !important;\n left: 0px !important;\n top: 0px !important;\n}\n\n\n.ui-select-choices-row:hover {\n background-color: #f5f5f5;\n}\n\n/* Select2 theme */\n\n/* Mark invalid Select2 */\n.ng-dirty.ng-invalid > a.select2-choice {\n border-color: #D44950;\n}\n\n.select2-result-single {\n padding-left: 0;\n}\n\n.select2-locked > .select2-search-choice-close{\n display:none;\n}\n\n.select-locked > .ui-select-match-close{\n display:none;\n}\n\nbody > .select2-container.open {\n z-index: 9999; /* The z-index Select2 applies to the select2-drop */\n}\n\n/* Handle up direction Select2 */\n.ui-select-container[theme=\"select2\"].direction-up .ui-select-match,\n.ui-select-container.select2.direction-up .ui-select-match {\n border-radius: 4px; /* FIXME hardcoded value :-/ */\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.ui-select-container[theme=\"select2\"].direction-up .ui-select-dropdown,\n.ui-select-container.select2.direction-up .ui-select-dropdown {\n border-radius: 4px; /* FIXME hardcoded value :-/ */\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n\n border-top-width: 1px; /* FIXME hardcoded value :-/ */\n border-top-style: solid;\n\n box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);\n\n margin-top: -4px; /* FIXME hardcoded value :-/ */\n}\n.ui-select-container[theme=\"select2\"].direction-up .ui-select-dropdown .select2-search,\n.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search {\n margin-top: 4px; /* FIXME hardcoded value :-/ */\n}\n.ui-select-container[theme=\"select2\"].direction-up.select2-dropdown-open .ui-select-match,\n.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match {\n border-bottom-color: #5897fb;\n}\n\n.ui-select-container[theme=\"select2\"] .ui-select-dropdown .ui-select-search-hidden,\n.ui-select-container[theme=\"select2\"] .ui-select-dropdown .ui-select-search-hidden input{\n opacity: 0;\n height: 0;\n min-height: 0;\n padding: 0;\n margin: 0;\n border:0;\n}\n\n/* Selectize theme */\n\n/* Helper class to show styles when focus */\n.selectize-input.selectize-focus{\n border-color: #007FBB !important;\n}\n\n/* Fix input width for Selectize theme */\n.selectize-control.single > .selectize-input > input {\n width: 100%;\n}\n\n/* Fix line break when there's at least one item selected with the Selectize theme */\n.selectize-control.multi > .selectize-input > input {\n margin: 0 !important;\n}\n\n/* Fix dropdown width for Selectize theme */\n.selectize-control > .selectize-dropdown {\n width: 100%;\n}\n\n/* Mark invalid Selectize */\n.ng-dirty.ng-invalid > div.selectize-input {\n border-color: #D44950;\n}\n\n/* Handle up direction Selectize */\n.ui-select-container[theme=\"selectize\"].direction-up .ui-select-dropdown {\n box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);\n margin-top: -2px; /* FIXME hardcoded value :-/ */\n}\n\n.ui-select-container[theme=\"selectize\"] input.ui-select-search-hidden{\n opacity: 0;\n height: 0;\n min-height: 0;\n padding: 0;\n margin: 0;\n border:0;\n width: 0;\n}\n\n/* Bootstrap theme */\n\n/* Helper class to show styles when focus */\n.btn-default-focus {\n color: #333;\n background-color: #EBEBEB;\n border-color: #ADADAD;\n text-decoration: none;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n\n.ui-select-bootstrap .ui-select-toggle {\n position: relative;\n}\n\n.ui-select-bootstrap .ui-select-toggle > .caret {\n position: absolute;\n height: 10px;\n top: 50%;\n right: 10px;\n margin-top: -2px;\n}\n\n/* Fix Bootstrap dropdown position when inside a input-group */\n.input-group > .ui-select-bootstrap.dropdown {\n /* Instead of relative */\n position: static;\n}\n\n.input-group > .ui-select-bootstrap > input.ui-select-search.form-control {\n border-radius: 4px; /* FIXME hardcoded value :-/ */\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up {\n border-radius: 4px !important; /* FIXME hardcoded value :-/ */\n border-top-right-radius: 0 !important;\n border-bottom-right-radius: 0 !important;\n}\n\n.ui-select-bootstrap .ui-select-search-hidden{\n opacity: 0;\n height: 0;\n min-height: 0;\n padding: 0;\n margin: 0;\n border:0;\n}\n\n.ui-select-bootstrap > .ui-select-match > .btn{\n /* Instead of center because of .btn */\n text-align: left !important;\n}\n\n.ui-select-bootstrap > .ui-select-match > .caret {\n position: absolute;\n top: 45%;\n right: 15px;\n}\n\n/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */\n.ui-select-bootstrap > .ui-select-choices ,.ui-select-bootstrap > .ui-select-no-choice {\n width: 100%;\n height: auto;\n max-height: 200px;\n overflow-x: hidden;\n margin-top: -1px;\n}\n\nbody > .ui-select-bootstrap.open {\n z-index: 1000; /* Standard Bootstrap dropdown z-index */\n}\n\n.ui-select-multiple.ui-select-bootstrap {\n height: auto;\n padding: 3px 3px 0 3px;\n}\n\n.ui-select-multiple.ui-select-bootstrap input.ui-select-search {\n background-color: transparent !important; /* To prevent double background when disabled */\n border: none;\n outline: none;\n height: 1.666666em;\n margin-bottom: 3px;\n}\n\n.ui-select-multiple.ui-select-bootstrap .ui-select-match .close {\n font-size: 1.6em;\n line-height: 0.75;\n}\n\n.ui-select-multiple.ui-select-bootstrap .ui-select-match-item {\n outline: 0;\n margin: 0 3px 3px 0;\n}\n\n.ui-select-multiple .ui-select-match-item {\n position: relative;\n}\n\n.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close {\n pointer-events: none;\n}\n\n.ui-select-multiple:hover .ui-select-match-item.dropping-before:before {\n content: \"\";\n position: absolute;\n top: 0;\n right: 100%;\n height: 100%;\n margin-right: 2px;\n border-left: 1px solid #428bca;\n}\n\n.ui-select-multiple:hover .ui-select-match-item.dropping-after:after {\n content: \"\";\n position: absolute;\n top: 0;\n left: 100%;\n height: 100%;\n margin-left: 2px;\n border-right: 1px solid #428bca;\n}\n\n.ui-select-bootstrap .ui-select-choices-row>span {\n cursor: pointer;\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333;\n white-space: nowrap;\n}\n\n.ui-select-bootstrap .ui-select-choices-row>span:hover, .ui-select-bootstrap .ui-select-choices-row>span:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n\n.ui-select-bootstrap .ui-select-choices-row.active>span {\n color: #fff;\n text-decoration: none;\n outline: 0;\n background-color: #428bca;\n}\n\n.ui-select-bootstrap .ui-select-choices-row.disabled>span,\n.ui-select-bootstrap .ui-select-choices-row.active.disabled>span {\n color: #777;\n cursor: not-allowed;\n background-color: #fff;\n}\n\n/* fix hide/show angular animation */\n.ui-select-match.ng-hide-add,\n.ui-select-search.ng-hide-add {\n display: none !important;\n}\n\n/* Mark invalid Bootstrap */\n.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match {\n border-color: #D44950;\n}\n\n/* Handle up direction Bootstrap */\n.ui-select-container[theme=\"bootstrap\"].direction-up .ui-select-dropdown {\n box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);\n}\n\n.ui-select-bootstrap .ui-select-match-text {\n width: 100%;\n padding-right: 1em;\n}\n.ui-select-bootstrap .ui-select-match-text span {\n display: inline-block;\n width: 100%;\n overflow: hidden;\n}\n.ui-select-bootstrap .ui-select-toggle > a.btn {\n position: absolute;\n height: 10px;\n right: 10px;\n margin-top: -2px;\n}\n\n/* Spinner */\n.ui-select-refreshing.glyphicon {\n position: absolute;\n right: 0;\n padding: 8px 27px;\n }\n\n@-webkit-keyframes ui-select-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg);\n }\n}\n@keyframes ui-select-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg);\n }\n}\n\n.ui-select-spin {\n -webkit-animation: ui-select-spin 2s infinite linear;\n animation: ui-select-spin 2s infinite linear;\n}\n\n.ui-select-refreshing.ng-animate {\n -webkit-animation: none 0s;\n}\n"]} \ No newline at end of file diff --git a/services/web/public/js/libs/select/select.min.js b/services/web/public/js/libs/select/select.min.js new file mode 100755 index 0000000000..61be14d250 --- /dev/null +++ b/services/web/public/js/libs/select/select.min.js @@ -0,0 +1,9 @@ +/*! + * ui-select + * http://github.com/angular-ui/ui-select + * Version: 0.19.7 - 2017-04-15T14:28:36.649Z + * License: MIT + */ +!function(){"use strict";function e(e){return angular.isUndefined(e)||null===e}var t={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,COMMAND:91,MAP:{91:"COMMAND",8:"BACKSPACE",9:"TAB",13:"ENTER",16:"SHIFT",17:"CTRL",18:"ALT",19:"PAUSEBREAK",20:"CAPSLOCK",27:"ESC",32:"SPACE",33:"PAGE_UP",34:"PAGE_DOWN",35:"END",36:"HOME",37:"LEFT",38:"UP",39:"RIGHT",40:"DOWN",43:"+",44:"PRINTSCREEN",45:"INSERT",46:"DELETE",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",61:"=",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9",106:"*",107:"+",109:"-",110:".",111:"/",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NUMLOCK",145:"SCROLLLOCK",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},isControl:function(e){var s=e.which;switch(s){case t.COMMAND:case t.SHIFT:case t.CTRL:case t.ALT:return!0}return!!(e.metaKey||e.ctrlKey||e.altKey)},isFunctionKey:function(e){return e=e.which?e.which:e,e>=112&&e<=123},isVerticalMovement:function(e){return~[t.UP,t.DOWN].indexOf(e)},isHorizontalMovement:function(e){return~[t.LEFT,t.RIGHT,t.BACKSPACE,t.DELETE].indexOf(e)},toSeparator:function(e){var s={ENTER:"\n",TAB:"\t",SPACE:" "}[e];return s?s:t[e]?void 0:e}};void 0===angular.element.prototype.querySelectorAll&&(angular.element.prototype.querySelectorAll=function(e){return angular.element(this[0].querySelectorAll(e))}),void 0===angular.element.prototype.closest&&(angular.element.prototype.closest=function(e){for(var t=this[0],s=t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector;t;){if(s.bind(t)(e))return t;t=t.parentElement}return!1});var s=0,i=angular.module("ui.select",[]).constant("uiSelectConfig",{theme:"bootstrap",searchEnabled:!0,sortable:!1,placeholder:"",refreshDelay:1e3,closeOnSelect:!0,skipFocusser:!1,dropdownPosition:"auto",removeSelected:!0,resetSearchInput:!0,generateId:function(){return s++},appendToBody:!1,spinnerEnabled:!1,spinnerClass:"glyphicon glyphicon-refresh ui-select-spin",backspaceReset:!0}).service("uiSelectMinErr",function(){var e=angular.$$minErr("ui.select");return function(){var t=e.apply(this,arguments),s=t.message.replace(new RegExp("\nhttp://errors.angularjs.org/.*"),"");return new Error(s)}}).directive("uisTranscludeAppend",function(){return{link:function(e,t,s,i,c){c(e,function(e){t.append(e)})}}}).filter("highlight",function(){function e(e){return(""+e).replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(t,s){return s&&t?(""+t).replace(new RegExp(e(s),"gi"),'$&'):t}}).factory("uisOffset",["$document","$window",function(e,t){return function(s){var i=s[0].getBoundingClientRect();return{width:i.width||s.prop("offsetWidth"),height:i.height||s.prop("offsetHeight"),top:i.top+(t.pageYOffset||e[0].documentElement.scrollTop),left:i.left+(t.pageXOffset||e[0].documentElement.scrollLeft)}}}]);i.factory("$$uisDebounce",["$timeout",function(e){return function(t,s){var i;return function(){var c=this,n=Array.prototype.slice.call(arguments);i&&e.cancel(i),i=e(function(){t.apply(c,n)},s)}}}]),i.directive("uiSelectChoices",["uiSelectConfig","uisRepeatParser","uiSelectMinErr","$compile","$window",function(e,t,s,i,c){return{restrict:"EA",require:"^uiSelect",replace:!0,transclude:!0,templateUrl:function(t){t.addClass("ui-select-choices");var s=t.parent().attr("theme")||e.theme;return s+"/choices.tpl.html"},compile:function(i,n){if(!n.repeat)throw s("repeat","Expected 'repeat' expression.");var l=n.groupBy,a=n.groupFilter;if(l){var r=i.querySelectorAll(".ui-select-choices-group");if(1!==r.length)throw s("rows","Expected 1 .ui-select-choices-group but got '{0}'.",r.length);r.attr("ng-repeat",t.getGroupNgRepeatExpression())}var o=t.parse(n.repeat),u=i.querySelectorAll(".ui-select-choices-row");if(1!==u.length)throw s("rows","Expected 1 .ui-select-choices-row but got '{0}'.",u.length);u.attr("ng-repeat",o.repeatExpression(l)).attr("ng-if","$select.open");var d=i.querySelectorAll(".ui-select-choices-row-inner");if(1!==d.length)throw s("rows","Expected 1 .ui-select-choices-row-inner but got '{0}'.",d.length);d.attr("uis-transclude-append","");var p=c.document.addEventListener?u:d;return p.attr("ng-click","$select.select("+o.itemName+",$select.skipFocusser,$event)"),function(t,s,c,n){n.parseRepeatAttr(c.repeat,l,a),n.disableChoiceExpression=c.uiDisableChoice,n.onHighlightCallback=c.onHighlight,n.minimumInputLength=parseInt(c.minimumInputLength)||0,n.dropdownPosition=c.position?c.position.toLowerCase():e.dropdownPosition,t.$watch("$select.search",function(e){e&&!n.open&&n.multiple&&n.activate(!1,!0),n.activeIndex=n.tagging.isActivated?-1:0,!c.minimumInputLength||n.search.length>=c.minimumInputLength?n.refresh(c.refresh):n.items=[]}),c.$observe("refreshDelay",function(){var s=t.$eval(c.refreshDelay);n.refreshDelay=void 0!==s?s:e.refreshDelay}),t.$watch("$select.open",function(e){e?(i.attr("role","listbox"),n.refresh(c.refresh)):s.removeAttr("role")})}}}}]),i.controller("uiSelectCtrl",["$scope","$element","$timeout","$filter","$$uisDebounce","uisRepeatParser","uiSelectMinErr","uiSelectConfig","$parse","$injector","$window",function(s,i,c,n,l,a,r,o,u,d,p){function h(e,t,s){if(e.findIndex)return e.findIndex(t,s);for(var i,c=Object(e),n=c.length>>>0,l=0;l-1&&I.splice(s,1)}function m(e){return I.indexOf(e)>-1}function $(e){function t(e,t){var s=i.indexOf(e);t&&s===-1&&i.push(e),!t&&s>-1&&i.splice(s,1)}function s(e){return i.indexOf(e)>-1}if(e){var i=[];y.isLocked=function(e,i){var c=!1,n=y.selected[i];return n&&(e?(c=!!e.$eval(y.lockChoiceExpression),t(n,c)):c=s(n)),c}}}function b(e){var s=!0;switch(e){case t.DOWN:if(!y.open&&y.multiple)y.activate(!1,!0);else if(y.activeIndexc)for(var n=--y.activeIndex;m(y.items[n])&&n>c;)y.activeIndex=--n;break;case t.TAB:y.multiple&&!y.open||y.select(y.items[y.activeIndex],!0);break;case t.ENTER:y.open&&(y.tagging.isActivated||y.activeIndex>=0)?y.select(y.items[y.activeIndex],y.skipFocusser):y.activate(!1,!0);break;case t.ESC:y.close();break;default:s=!1}return s}function w(){var e=i.querySelectorAll(".ui-select-choices-content"),t=e.querySelectorAll(".ui-select-choices-row");if(t.length<1)throw r("choices","Expected multiple .ui-select-choices-row but got '{0}'.",t.length);if(!(y.activeIndex<0)){var s=t[y.activeIndex],c=s.offsetTop+s.clientHeight-e[0].scrollTop,n=e[0].offsetHeight;c>n?e[0].scrollTop+=c-n:c=y.items.length?0:y.activeIndex,y.activeIndex===-1&&y.taggingLabel!==!1&&(y.activeIndex=0);var n=i.querySelectorAll(".ui-select-choices-content"),l=i.querySelectorAll(".ui-select-search");if(y.$animate&&y.$animate.on&&y.$animate.enabled(n[0])){var a=function(t,s){"start"===s&&0===y.items.length?(y.$animate.off("removeClass",l[0],a),c(function(){y.focusSearchInput(e)})):"close"===s&&(y.$animate.off("enter",n[0],a),c(function(){y.focusSearchInput(e)}))};y.items.length>0?y.$animate.on("enter",n[0],a):y.$animate.on("removeClass",l[0],a)}else c(function(){y.focusSearchInput(e),!y.tagging.isActivated&&y.items.length>1&&w()})}},y.focusSearchInput=function(e){y.search=e||y.search,y.searchInput[0].focus()},y.findGroupByName=function(e){return y.groups&&y.groups.filter(function(t){return t.name===e})[0]},y.parseRepeatAttr=function(e,t,i){function c(e){var c=s.$eval(t);if(y.groups=[],angular.forEach(e,function(e){var t=angular.isFunction(c)?c(e):e[c],s=y.findGroupByName(t);s?s.items.push(e):y.groups.push({name:t,items:[e]})}),i){var n=s.$eval(i);angular.isFunction(n)?y.groups=n(y.groups):angular.isArray(n)&&(y.groups=f(y.groups,n))}y.items=[],y.groups.forEach(function(e){y.items=y.items.concat(e.items)})}function n(e){y.items=e||[]}y.setItemsFn=t?c:n,y.parserResult=a.parse(e),y.isGrouped=!!t,y.itemProperty=y.parserResult.itemName;var l=y.parserResult.source,o=function(){var e=l(s);s.$uisSource=Object.keys(e).map(function(t){var s={};return s[y.parserResult.keyName]=t,s.value=e[t],s})};y.parserResult.keyName&&(o(),y.parserResult.source=u("$uisSource"+y.parserResult.filters),s.$watch(l,function(e,t){e!==t&&o()},!0)),y.refreshItems=function(e){e=e||y.parserResult.source(s);var t=y.selected;if(y.isEmpty()||angular.isArray(t)&&!t.length||!y.multiple||!y.removeSelected)y.setItemsFn(e);else if(void 0!==e&&null!==e){var i=e.filter(function(e){return angular.isArray(t)?t.every(function(t){return!angular.equals(e,t)}):!angular.equals(e,t)});y.setItemsFn(i)}"auto"!==y.dropdownPosition&&"up"!==y.dropdownPosition||s.calculateDropdownPos(),s.$broadcast("uis:refresh")},s.$watchCollection(y.parserResult.source,function(e){if(void 0===e||null===e)y.items=[];else{if(!angular.isArray(e))throw r("items","Expected an array but got '{0}'.",e);y.refreshItems(e),angular.isDefined(y.ngModel.$modelValue)&&(y.ngModel.$modelValue=null)}})};var E;y.refresh=function(e){void 0!==e&&(E&&c.cancel(E),E=c(function(){if(s.$select.search.length>=s.$select.minimumInputLength){var t=s.$eval(e);t&&angular.isFunction(t.then)&&!y.refreshing&&(y.refreshing=!0,t["finally"](function(){y.refreshing=!1}))}},y.refreshDelay))},y.isActive=function(e){if(!y.open)return!1;var t=y.items.indexOf(e[y.itemProperty]),s=t==y.activeIndex;return!(!s||t<0)&&(s&&!angular.isUndefined(y.onHighlightCallback)&&e.$eval(y.onHighlightCallback),s)};var S=function(e){return y.selected&&angular.isArray(y.selected)&&y.selected.filter(function(t){return angular.equals(t,e)}).length>0},I=[];y.isDisabled=function(e){if(y.open){var t=e[y.itemProperty],s=y.items.indexOf(t),i=!1;if(s>=0&&(angular.isDefined(y.disableChoiceExpression)||y.multiple)){if(t.isTag)return!1;y.multiple&&(i=S(t)),!i&&angular.isDefined(y.disableChoiceExpression)&&(i=!!e.$eval(y.disableChoiceExpression)),v(t,i)}return i}},y.select=function(t,i,c){if(e(t)||!m(t)){if(!y.items&&!y.search&&!y.tagging.isActivated)return;if(!t||!m(t)){if(y.clickTriggeredSelect=!1,c&&("click"===c.type||"touchend"===c.type)&&t&&(y.clickTriggeredSelect=!0),y.tagging.isActivated&&y.clickTriggeredSelect===!1){if(y.taggingLabel===!1)if(y.activeIndex<0){if(void 0===t&&(t=void 0!==y.tagging.fct?y.tagging.fct(y.search):y.search),!t||angular.equals(y.items[0],t))return}else t=y.items[y.activeIndex];else if(0===y.activeIndex){if(void 0===t)return;if(void 0!==y.tagging.fct&&"string"==typeof t){if(t=y.tagging.fct(t),!t)return}else"string"==typeof t&&(t=t.replace(y.taggingLabel,"").trim())}if(S(t))return void y.close(i)}g(),s.$broadcast("uis:select",t),y.closeOnSelect&&y.close(i)}}},y.close=function(e){y.open&&(y.ngModel&&y.ngModel.$setTouched&&y.ngModel.$setTouched(),y.open=!1,g(),s.$broadcast("uis:close",e))},y.setFocus=function(){y.focus||y.focusInput[0].focus()},y.clear=function(e){y.select(null),e.stopPropagation(),c(function(){y.focusser[0].focus()},0,!1)},y.toggle=function(e){y.open?(y.close(),e.preventDefault(),e.stopPropagation()):y.activate()},y.isLocked=function(){return!1},s.$watch(function(){return angular.isDefined(y.lockChoiceExpression)&&""!==y.lockChoiceExpression},$);var C=null,k=!1;y.sizeSearchInput=function(){var e=y.searchInput[0],t=y.$element[0],i=function(){return t.clientWidth*!!e.offsetParent},n=function(t){if(0===t)return!1;var s=t-e.offsetLeft;return s<50&&(s=t),y.searchInput.css("width",s+"px"),!0};y.searchInput.css("width","10px"),c(function(){null!==C||n(i())||(C=s.$watch(function(){k||(k=!0,s.$$postDigest(function(){k=!1,n(i())&&(C(),C=null)}))},angular.noop))})},y.searchInput.on("keydown",function(e){var i=e.which;~[t.ENTER,t.ESC].indexOf(i)&&(e.preventDefault(),e.stopPropagation()),s.$apply(function(){var s=!1;if((y.items.length>0||y.tagging.isActivated)&&(b(i)||y.searchEnabled||(e.preventDefault(),e.stopPropagation()),y.taggingTokens.isActivated)){for(var n=0;n0&&(s=!0);s&&c(function(){y.searchInput.triggerHandler("tagged");var s=y.search.replace(t.MAP[e.keyCode],"").trim();y.tagging.fct&&(s=y.tagging.fct(s)),s&&y.select(s,!0)})}}),t.isVerticalMovement(i)&&y.items.length>0&&w(),i!==t.ENTER&&i!==t.ESC||(e.preventDefault(),e.stopPropagation())}),y.searchInput.on("paste",function(e){var s;if(s=window.clipboardData&&window.clipboardData.getData?window.clipboardData.getData("Text"):(e.originalEvent||e).clipboardData.getData("text/plain"),s=y.search+s,s&&s.length>0)if(y.taggingTokens.isActivated){for(var i=[],c=0;c-1){i=s.split(n);break}}0===i.length&&(i=[s]);var l=y.search;angular.forEach(i,function(e){var t=y.tagging.fct?y.tagging.fct(e):e;t&&y.select(t,!0)}),y.search=l||x,e.preventDefault(),e.stopPropagation()}else y.paste&&(y.paste(s),y.search=x,e.preventDefault(),e.stopPropagation())}),y.searchInput.on("tagged",function(){c(function(){g()})});var A=l(function(){y.sizeSearchInput()},50);angular.element(p).bind("resize",A),s.$on("$destroy",function(){y.searchInput.off("keyup keydown tagged blur paste"),angular.element(p).off("resize",A)}),s.$watch("$select.activeIndex",function(e){e&&i.find("input").attr("aria-activedescendant","ui-select-choices-row-"+y.generatedId+"-"+e)}),s.$watch("$select.open",function(e){e||i.find("input").removeAttr("aria-activedescendant")})}]),i.directive("uiSelect",["$document","uiSelectConfig","uiSelectMinErr","uisOffset","$compile","$parse","$timeout",function(e,t,s,i,c,n,l){return{restrict:"EA",templateUrl:function(e,s){var i=s.theme||t.theme;return i+(angular.isDefined(s.multiple)?"/select-multiple.tpl.html":"/select.tpl.html")},replace:!0,transclude:!0,require:["uiSelect","^ngModel"],scope:!0,controller:"uiSelectCtrl",controllerAs:"$select",compile:function(c,a){var r=/{(.*)}\s*{(.*)}/.exec(a.ngClass);if(r){var o="{"+r[1]+", "+r[2]+"}";a.ngClass=o,c.attr("ng-class",o)}return angular.isDefined(a.multiple)?c.append("").removeAttr("multiple"):c.append(""),a.inputId&&(c.querySelectorAll("input.ui-select-search")[0].id=a.inputId),function(c,a,r,o,u){function d(e){if(g.open){var t=!1;if(t=window.jQuery?window.jQuery.contains(a[0],e.target):a[0].contains(e.target),!t&&!g.clickTriggeredSelect){var s;if(g.skipFocusser)s=!0;else{var i=["input","button","textarea","select"],n=angular.element(e.target).controller("uiSelect");s=n&&n!==g,s||(s=~i.indexOf(e.target.tagName.toLowerCase()))}g.close(s),c.$digest()}g.clickTriggeredSelect=!1}}function p(){var t=i(a);m=angular.element('
    '),m[0].style.width=t.width+"px",m[0].style.height=t.height+"px",a.after(m),$=a[0].style.width,e.find("body").append(a),a[0].style.position="absolute",a[0].style.left=t.left+"px",a[0].style.top=t.top+"px",a[0].style.width=t.width+"px"}function h(){null!==m&&(m.replaceWith(a),m=null,a[0].style.position="",a[0].style.left="",a[0].style.top="",a[0].style.width=$,g.setFocus())}var g=o[0],f=o[1];g.generatedId=t.generateId(),g.baseTitle=r.title||"Select box",g.focusserTitle=g.baseTitle+" focus",g.focusserId="focusser-"+g.generatedId,g.closeOnSelect=function(){return angular.isDefined(r.closeOnSelect)?n(r.closeOnSelect)():t.closeOnSelect}(),c.$watch("skipFocusser",function(){var e=c.$eval(r.skipFocusser);g.skipFocusser=void 0!==e?e:t.skipFocusser}),g.onSelectCallback=n(r.onSelect),g.onRemoveCallback=n(r.onRemove),g.ngModel=f,g.choiceGrouped=function(e){return g.isGrouped&&e&&e.name},r.tabindex&&r.$observe("tabindex",function(e){g.focusInput.attr("tabindex",e),a.removeAttr("tabindex")}),c.$watch(function(){return c.$eval(r.searchEnabled)},function(e){g.searchEnabled=void 0!==e?e:t.searchEnabled}),c.$watch("sortable",function(){var e=c.$eval(r.sortable);g.sortable=void 0!==e?e:t.sortable}),r.$observe("backspaceReset",function(){var e=c.$eval(r.backspaceReset);g.backspaceReset=void 0===e||e}),r.$observe("limit",function(){g.limit=angular.isDefined(r.limit)?parseInt(r.limit,10):void 0}),c.$watch("removeSelected",function(){var e=c.$eval(r.removeSelected);g.removeSelected=void 0!==e?e:t.removeSelected}),r.$observe("disabled",function(){g.disabled=void 0!==r.disabled&&r.disabled}),r.$observe("resetSearchInput",function(){var e=c.$eval(r.resetSearchInput);g.resetSearchInput=void 0===e||e}),r.$observe("paste",function(){g.paste=c.$eval(r.paste)}),r.$observe("tagging",function(){if(void 0!==r.tagging){var e=c.$eval(r.tagging);g.tagging={isActivated:!0,fct:e!==!0?e:void 0}}else g.tagging={isActivated:!1,fct:void 0}}),r.$observe("taggingLabel",function(){void 0!==r.tagging&&("false"===r.taggingLabel?g.taggingLabel=!1:g.taggingLabel=void 0!==r.taggingLabel?r.taggingLabel:"(new)")}),r.$observe("taggingTokens",function(){if(void 0!==r.tagging){var e=void 0!==r.taggingTokens?r.taggingTokens.split("|"):[",","ENTER"];g.taggingTokens={isActivated:!0,tokens:e}}}),r.$observe("spinnerEnabled",function(){var e=c.$eval(r.spinnerEnabled);g.spinnerEnabled=void 0!==e?e:t.spinnerEnabled}),r.$observe("spinnerClass",function(){var e=r.spinnerClass;g.spinnerClass=void 0!==e?r.spinnerClass:t.spinnerClass}),angular.isDefined(r.autofocus)&&l(function(){g.setFocus()}),angular.isDefined(r.focusOn)&&c.$on(r.focusOn,function(){l(function(){g.setFocus()})}),e.on("click",d),c.$on("$destroy",function(){e.off("click",d)}),u(c,function(e){var t=angular.element("
    ").append(e),i=t.querySelectorAll(".ui-select-match");if(i.removeAttr("ui-select-match"),i.removeAttr("data-ui-select-match"),1!==i.length)throw s("transcluded","Expected 1 .ui-select-match but got '{0}'.",i.length);a.querySelectorAll(".ui-select-match").replaceWith(i);var c=t.querySelectorAll(".ui-select-choices");if(c.removeAttr("ui-select-choices"),c.removeAttr("data-ui-select-choices"),1!==c.length)throw s("transcluded","Expected 1 .ui-select-choices but got '{0}'.",c.length);a.querySelectorAll(".ui-select-choices").replaceWith(c);var n=t.querySelectorAll(".ui-select-no-choice");n.removeAttr("ui-select-no-choice"),n.removeAttr("data-ui-select-no-choice"),1==n.length&&a.querySelectorAll(".ui-select-no-choice").replaceWith(n)});var v=c.$eval(r.appendToBody);(void 0!==v?v:t.appendToBody)&&(c.$watch("$select.open",function(e){e?p():h()}),c.$on("$destroy",function(){h()}));var m=null,$="",b=null,w="direction-up";c.$watch("$select.open",function(){"auto"!==g.dropdownPosition&&"up"!==g.dropdownPosition||c.calculateDropdownPos()});var y=function(e,t){e=e||i(a),t=t||i(b),b[0].style.position="absolute",b[0].style.top=t.height*-1+"px",a.addClass(w)},x=function(e,t){a.removeClass(w),e=e||i(a),t=t||i(b),b[0].style.position="",b[0].style.top=""},E=function(){l(function(){if("up"===g.dropdownPosition)y();else{a.removeClass(w);var t=i(a),s=i(b),c=e[0].documentElement.scrollTop||e[0].body.scrollTop;t.top+t.height+s.height>c+e[0].documentElement.clientHeight?y(t,s):x(t,s)}b[0].style.opacity=1})},S=!1;c.calculateDropdownPos=function(){if(g.open){if(b=angular.element(a).querySelectorAll(".ui-select-dropdown"),0===b.length)return;if(""!==g.search||S||(b[0].style.opacity=0,S=!0),!i(b).height&&g.$animate&&g.$animate.on&&g.$animate.enabled(b)){var e=!0;g.$animate.on("enter",b,function(t,s){"close"===s&&e&&(E(),e=!1)})}else E()}else{if(null===b||0===b.length)return;b[0].style.opacity=0,b[0].style.position="",b[0].style.top="",a.removeClass(w)}}}}}}]),i.directive("uiSelectMatch",["uiSelectConfig",function(e){function t(e,t){return e[0].hasAttribute(t)?e.attr(t):e[0].hasAttribute("data-"+t)?e.attr("data-"+t):e[0].hasAttribute("x-"+t)?e.attr("x-"+t):void 0}return{restrict:"EA",require:"^uiSelect",replace:!0,transclude:!0,templateUrl:function(s){s.addClass("ui-select-match");var i=s.parent(),c=t(i,"theme")||e.theme,n=angular.isDefined(t(i,"multiple"));return c+(n?"/match-multiple.tpl.html":"/match.tpl.html")},link:function(t,s,i,c){function n(e){c.allowClear=!!angular.isDefined(e)&&(""===e||"true"===e.toLowerCase())}c.lockChoiceExpression=i.uiLockChoice,i.$observe("placeholder",function(t){c.placeholder=void 0!==t?t:e.placeholder}),i.$observe("allowClear",n),n(i.allowClear),c.multiple&&c.sizeSearchInput()}}}]),i.directive("uiSelectMultiple",["uiSelectMinErr","$timeout",function(s,i){return{restrict:"EA",require:["^uiSelect","^ngModel"],controller:["$scope","$timeout",function(e,t){var s,i=this,c=e.$select;angular.isUndefined(c.selected)&&(c.selected=[]),e.$evalAsync(function(){s=e.ngModel}),i.activeMatchIndex=-1,i.updateModel=function(){s.$setViewValue(Date.now()),i.refreshComponent()},i.refreshComponent=function(){c.refreshItems&&c.refreshItems(),c.sizeSearchInput&&c.sizeSearchInput()},i.removeChoice=function(s){if(c.isLocked(null,s))return!1;var n=c.selected[s],l={};return l[c.parserResult.itemName]=n,c.selected.splice(s,1),i.activeMatchIndex=-1,c.sizeSearchInput(),t(function(){c.onRemoveCallback(e,{$item:n,$model:c.parserResult.modelMapper(e,l)})}),i.updateModel(),!0},i.getPlaceholder=function(){if(!c.selected||!c.selected.length)return c.placeholder}}],controllerAs:"$selectMultiple",link:function(c,n,l,a){function r(e){return angular.isNumber(e.selectionStart)?e.selectionStart:e.value.length}function o(e){function s(){switch(e){case t.LEFT:return~g.activeMatchIndex?u:l;case t.RIGHT:return~g.activeMatchIndex&&a!==l?o:(p.activate(),!1);case t.BACKSPACE:return~g.activeMatchIndex?g.removeChoice(a)?u:a:l;case t.DELETE:return!!~g.activeMatchIndex&&(g.removeChoice(g.activeMatchIndex),a)}}var i=r(p.searchInput[0]),c=p.selected.length,n=0,l=c-1,a=g.activeMatchIndex,o=g.activeMatchIndex+1,u=g.activeMatchIndex-1,d=a;return!(i>0||p.search.length&&e==t.RIGHT)&&(p.close(),d=s(),p.selected.length&&d!==!1?g.activeMatchIndex=Math.min(l,Math.max(n,d)):g.activeMatchIndex=-1,!0)}function u(e){if(void 0===e||void 0===p.search)return!1;var t=e.filter(function(e){return void 0!==p.search.toUpperCase()&&void 0!==e&&e.toUpperCase()===p.search.toUpperCase()}).length>0;return t}function d(e,t){var s=-1;if(angular.isArray(e))for(var i=angular.copy(e),c=0;c=0;i--)t={},t[p.parserResult.itemName]=p.selected[i],e=p.parserResult.modelMapper(c,t),s.unshift(e);return s}),h.$formatters.unshift(function(e){var t,s=p.parserResult&&p.parserResult.source(c,{$select:{search:""}}),i={};if(!s)return e;var n=[],l=function(e,s){if(e&&e.length){for(var l=e.length-1;l>=0;l--){if(i[p.parserResult.itemName]=e[l],t=p.parserResult.modelMapper(c,i),p.parserResult.trackByExp){var a=/(\w*)\./.exec(p.parserResult.trackByExp),r=/\.([^\s]+)/.exec(p.parserResult.trackByExp);if(a&&a.length>0&&a[1]==p.parserResult.itemName&&r&&r.length>0&&t[r[1]]==s[r[1]])return n.unshift(e[l]),!0}if(angular.equals(t,s))return n.unshift(e[l]),!0}return!1}};if(!e)return n;for(var a=e.length-1;a>=0;a--)l(p.selected,e[a])||l(s,e[a])||n.unshift(e[a]);return n}),c.$watchCollection(function(){return h.$modelValue},function(e,t){t!=e&&(angular.isDefined(h.$modelValue)&&(h.$modelValue=null),g.refreshComponent())}),h.$render=function(){if(!angular.isArray(h.$viewValue)){if(!e(h.$viewValue))throw s("multiarr","Expected model value to be array but got '{0}'",h.$viewValue);h.$viewValue=[]}p.selected=h.$viewValue,g.refreshComponent(),c.$evalAsync()},c.$on("uis:select",function(e,t){if(!(p.selected.length>=p.limit)){p.selected.push(t);var s={};s[p.parserResult.itemName]=t,i(function(){p.onSelectCallback(c,{$item:t,$model:p.parserResult.modelMapper(c,s)})}),g.updateModel()}}),c.$on("uis:activate",function(){g.activeMatchIndex=-1}),c.$watch("$select.disabled",function(e,t){t&&!e&&p.sizeSearchInput()}),p.searchInput.on("keydown",function(e){var s=e.which;c.$apply(function(){var i=!1;t.isHorizontalMovement(s)&&(i=o(s)),i&&s!=t.TAB&&(e.preventDefault(),e.stopPropagation())})}),p.searchInput.on("keyup",function(e){if(t.isVerticalMovement(e.which)||c.$evalAsync(function(){p.activeIndex=p.taggingLabel===!1?-1:0}),p.tagging.isActivated&&p.search.length>0){if(e.which===t.TAB||t.isControl(e)||t.isFunctionKey(e)||e.which===t.ESC||t.isVerticalMovement(e.which))return;if(p.activeIndex=p.taggingLabel===!1?-1:0,p.taggingLabel===!1)return;var s,i,n,l,a=angular.copy(p.items),r=angular.copy(p.items),o=!1,h=-1;if(void 0!==p.tagging.fct){if(n=p.$filter("filter")(a,{isTag:!0}),n.length>0&&(l=n[0]),a.length>0&&l&&(o=!0,a=a.slice(1,a.length),r=r.slice(1,r.length)),s=p.tagging.fct(p.search),r.some(function(e){return angular.equals(e,s)})||p.selected.some(function(e){return angular.equals(e,s)}))return void c.$evalAsync(function(){p.activeIndex=0,p.items=a});s&&(s.isTag=!0)}else{if(n=p.$filter("filter")(a,function(e){return e.match(p.taggingLabel)}),n.length>0&&(l=n[0]),i=a[0],void 0!==i&&a.length>0&&l&&(o=!0,a=a.slice(1,a.length),r=r.slice(1,r.length)),s=p.search+" "+p.taggingLabel,d(p.selected,p.search)>-1)return;if(u(r.concat(p.selected)))return void(o&&(a=r,c.$evalAsync(function(){p.activeIndex=0,p.items=a})));if(u(r))return void(o&&(p.items=r.slice(1,r.length)))}o&&(h=d(p.selected,s)),h>-1?a=a.slice(h+1,a.length-1):(a=[],s&&a.push(s),a=a.concat(r)),c.$evalAsync(function(){if(p.activeIndex=0,p.items=a,p.isGrouped){var e=s?a.slice(1):a;p.setItemsFn(e),s&&(p.items.unshift(s),p.groups.unshift({name:"",items:[s],tagging:!0}))}})}}),p.searchInput.on("blur",function(){i(function(){g.activeMatchIndex=-1})})}}}]),i.directive("uiSelectNoChoice",["uiSelectConfig",function(e){return{restrict:"EA",require:"^uiSelect",replace:!0,transclude:!0,templateUrl:function(t){t.addClass("ui-select-no-choice");var s=t.parent().attr("theme")||e.theme;return s+"/no-choice.tpl.html"}}}]),i.directive("uiSelectSingle",["$timeout","$compile",function(s,i){return{restrict:"EA",require:["^uiSelect","^ngModel"],link:function(c,n,l,a){var r=a[0],o=a[1];o.$parsers.unshift(function(t){if(e(t))return t;var s,i={};return i[r.parserResult.itemName]=t,s=r.parserResult.modelMapper(c,i)}),o.$formatters.unshift(function(t){if(e(t))return t;var s,i=r.parserResult&&r.parserResult.source(c,{$select:{search:""}}),n={};if(i){var l=function(e){return n[r.parserResult.itemName]=e,s=r.parserResult.modelMapper(c,n),s===t};if(r.selected&&l(r.selected))return r.selected;for(var a=i.length-1;a>=0;a--)if(l(i[a]))return i[a]}return t}),c.$watch("$select.selected",function(e){o.$viewValue!==e&&o.$setViewValue(e)}),o.$render=function(){r.selected=o.$viewValue},c.$on("uis:select",function(t,i){r.selected=i;var n={};n[r.parserResult.itemName]=i,s(function(){r.onSelectCallback(c,{$item:i,$model:e(i)?i:r.parserResult.modelMapper(c,n)})})}),c.$on("uis:close",function(e,t){s(function(){r.focusser.prop("disabled",!1),t||r.focusser[0].focus()},0,!1)}),c.$on("uis:activate",function(){u.prop("disabled",!0)});var u=angular.element("");i(u)(c),r.focusser=u,r.focusInput=u,n.parent().append(u),u.bind("focus",function(){c.$evalAsync(function(){r.focus=!0})}),u.bind("blur",function(){c.$evalAsync(function(){r.focus=!1})}),u.bind("keydown",function(e){return e.which===t.BACKSPACE&&r.backspaceReset!==!1?(e.preventDefault(),e.stopPropagation(),r.select(void 0),void c.$apply()):void(e.which===t.TAB||t.isControl(e)||t.isFunctionKey(e)||e.which===t.ESC||(e.which!=t.DOWN&&e.which!=t.UP&&e.which!=t.ENTER&&e.which!=t.SPACE||(e.preventDefault(),e.stopPropagation(),r.activate()),c.$digest()))}),u.bind("keyup input",function(e){e.which===t.TAB||t.isControl(e)||t.isFunctionKey(e)||e.which===t.ESC||e.which==t.ENTER||e.which===t.BACKSPACE||(r.activate(u.val()),u.val(""),c.$digest())})}}}]),i.directive("uiSelectSort",["$timeout","uiSelectConfig","uiSelectMinErr",function(e,t,s){return{require:["^^uiSelect","^ngModel"],link:function(t,i,c,n){if(null===t[c.uiSelectSort])throw s("sort","Expected a list to sort");var l=n[0],a=n[1],r=angular.extend({axis:"horizontal"},t.$eval(c.uiSelectSortOptions)),o=r.axis,u="dragging",d="dropping",p="dropping-before",h="dropping-after";t.$watch(function(){return l.sortable},function(e){e?i.attr("draggable",!0):i.removeAttr("draggable")}),i.on("dragstart",function(e){i.addClass(u),(e.dataTransfer||e.originalEvent.dataTransfer).setData("text",t.$index.toString())}),i.on("dragend",function(){v(u)});var g,f=function(e,t){this.splice(t,0,this.splice(e,1)[0])},v=function(e){angular.forEach(l.$element.querySelectorAll("."+e),function(t){angular.element(t).removeClass(e)})},m=function(e){e.preventDefault();var t="vertical"===o?e.offsetY||e.layerY||(e.originalEvent?e.originalEvent.offsetY:0):e.offsetX||e.layerX||(e.originalEvent?e.originalEvent.offsetX:0);t
  • '),e.put("bootstrap/match-multiple.tpl.html",' × '),e.put("bootstrap/match.tpl.html",'
    {{$select.placeholder}}
    '),e.put("bootstrap/no-choice.tpl.html",''),e.put("bootstrap/select-multiple.tpl.html",''),e.put("bootstrap/select.tpl.html",''),e.put("select2/choices.tpl.html",'
    '),e.put("select2/match-multiple.tpl.html",'
  • '),e.put("select2/match.tpl.html",'{{$select.placeholder}} '),e.put("select2/no-choice.tpl.html",''),e.put("select2/select-multiple.tpl.html",'
    '),e.put("select2/select.tpl.html",'
    '),e.put("selectize/choices.tpl.html",'
    '),e.put("selectize/match-multiple.tpl.html",'
    ×
    '),e.put("selectize/match.tpl.html",'
    {{$select.placeholder}}
    '),e.put("selectize/no-choice.tpl.html",'
    '),e.put("selectize/select-multiple.tpl.html",'
    '),e.put("selectize/select.tpl.html",'
    ')}]); +//# sourceMappingURL=select.min.js.map diff --git a/services/web/public/js/libs/select/select.min.js.map b/services/web/public/js/libs/select/select.min.js.map new file mode 100755 index 0000000000..04891040c3 --- /dev/null +++ b/services/web/public/js/libs/select/select.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["select.js","select_without_templates.js","templates.js"],"names":["isNil","value","angular","isUndefined","KEY","TAB","ENTER","ESC","SPACE","LEFT","UP","RIGHT","DOWN","SHIFT","CTRL","ALT","PAGE_UP","PAGE_DOWN","HOME","END","BACKSPACE","DELETE","COMMAND","MAP","91","8","9","13","16","17","18","19","20","27","32","33","34","35","36","37","38","39","40","43","44","45","46","48","49","50","51","52","53","54","55","56","57","59","61","65","66","67","68","69","70","71","72","73","74","75","76","77","78","79","80","81","82","83","84","85","86","87","88","89","90","96","97","98","99","100","101","102","103","104","105","106","107","109","110","111","112","113","114","115","116","117","118","119","120","121","122","123","144","145","186","187","188","189","190","191","192","219","220","221","222","isControl","e","k","which","metaKey","ctrlKey","altKey","isFunctionKey","isVerticalMovement","indexOf","isHorizontalMovement","toSeparator","sep","undefined","element","prototype","querySelectorAll","selector","this","closest","elem","matchesSelector","matches","webkitMatchesSelector","mozMatchesSelector","msMatchesSelector","bind","parentElement","latestId","uis","module","constant","theme","searchEnabled","sortable","placeholder","refreshDelay","closeOnSelect","skipFocusser","dropdownPosition","removeSelected","resetSearchInput","generateId","appendToBody","spinnerEnabled","spinnerClass","backspaceReset","service","minErr","$$minErr","error","apply","arguments","message","replace","RegExp","Error","directive","link","scope","attrs","ctrl","transclude","clone","append","filter","escapeRegexp","queryToEscape","matchItem","query","factory","$document","$window","boundingClientRect","getBoundingClientRect","width","prop","height","top","pageYOffset","documentElement","scrollTop","left","pageXOffset","scrollLeft","$timeout","callback","debounceTime","timeoutPromise","self","args","Array","slice","call","cancel","uiSelectConfig","RepeatParser","uiSelectMinErr","$compile","restrict","require","templateUrl","tElement","addClass","parent","attr","compile","tAttrs","repeat","groupByExp","groupBy","groupFilterExp","groupFilter","groups","length","getGroupNgRepeatExpression","parserResult","parse","choices","repeatExpression","rowsInner","clickTarget","document","addEventListener","itemName","$select","parseRepeatAttr","disableChoiceExpression","uiDisableChoice","onHighlightCallback","onHighlight","minimumInputLength","parseInt","position","toLowerCase","$watch","newValue","open","multiple","activate","activeIndex","tagging","isActivated","search","refresh","items","$observe","$eval","removeAttr","controller","$scope","$element","$filter","$$uisDebounce","$parse","$injector","_findIndex","collection","predicate","thisArg","findIndex","list","Object","i","_resetSearchInput","EMPTY_SEARCH","selected","item","equals","_groupsFilter","groupNames","j","result","name","push","_updateItemDisabled","isDisabled","disabledItemIndex","disabledItems","splice","_isItemDisabled","_initaliseLockedChoices","doInitalise","_updateItemLocked","isLocked","lockedItemIndex","lockedItems","_isItemlocked","itemScope","itemIndex","lockChoiceExpression","_handleDropDownSelection","key","processed","idx","minActiveIndex","idxmin","select","close","_ensureHighlightVisible","container","highlighted","posY","offsetTop","clientHeight","offsetHeight","isGrouped","paste","refreshing","focus","disabled","focusser","fct","taggingTokens","tokens","clickTriggeredSelect","$animate","get","err","searchInput","isEmpty","initSearchValue","avoidReset","$broadcast","taggingLabel","on","enabled","animateHandler","phase","off","focusSearchInput","findGroupByName","group","repeatAttr","updateGroups","groupFn","forEach","groupName","isFunction","groupFilterFn","isArray","concat","setPlainItems","setItemsFn","itemProperty","originalSource","source","createArrayFromObject","origSrc","$uisSource","keys","map","v","keyName","filters","newVal","oldVal","refreshItems","data","selectedItems","filteredItems","every","selectedItem","calculateDropdownPos","$watchCollection","isDefined","ngModel","$modelValue","_refreshDelayPromise","refreshAttr","refreshPromise","then","isActive","_isItemSelected","selection","isTag","$event","type","trim","$setTouched","setFocus","focusInput","clear","stopPropagation","toggle","preventDefault","sizeWatch","updaterScheduled","sizeSearchInput","input","calculateContainerWidth","clientWidth","offsetParent","updateIfVisible","containerWidth","inputWidth","offsetLeft","css","$$postDigest","noop","$apply","tagged","keyCode","triggerHandler","newItem","window","clipboardData","getData","originalEvent","separator","split","oldsearch","onResize","$on","find","generatedId","uisOffset","controllerAs","match","exec","ngClass","combined","inputId","id","ctrls","transcludeFn","onDocumentClick","contains","jQuery","target","focusableControls","targetController","tagName","$digest","positionDropdown","offset","style","after","originalWidth","resetDropdown","replaceWith","baseTitle","title","focusserTitle","focusserId","onSelectCallback","onSelect","onRemoveCallback","onRemove","choiceGrouped","tabindex","limit","taggingEval","autofocus","focusOn","transcluded","transcludedMatch","transcludedChoices","transcludedNoChoice","isOpen","dropdown","directionUpClassName","setDropdownPosUp","offsetDropdown","setDropdownPosDown","removeClass","calculateDropdownPosAfterAnimation","body","opacity","opened","needsCalculated","getAttribute","attribute","hasAttribute","multi","setAllowClear","allow","allowClear","uiLockChoice","$evalAsync","activeMatchIndex","updateModel","$setViewValue","Date","now","refreshComponent","removeChoice","index","removedChoice","locals","$item","$model","modelMapper","getPlaceholder","_getCaretPosition","el","isNumber","selectionStart","_handleMatchSelection","getNewActiveMatchIndex","$selectMultiple","prev","last","curr","next","caretPosition","first","newIndex","Math","min","max","_findCaseInsensitiveDupe","arr","hasDupe","origItem","toUpperCase","_findApproxDupe","haystack","needle","dupeIndex","tempArr","copy","mockObj","isObject","$isEmpty","$parsers","unshift","resultMultiple","$formatters","inputValue","checkFnMultiple","p","trackByExp","propsItemNameMatches","oldValue","$render","$viewValue","event","tagItems","tagItem","stashArr","hasTag","some","itemsWithoutTag","checkFnSingle","d","val","uiSelectSort","$ngModel","options","extend","axis","uiSelectSortOptions","draggingClassName","droppingClassName","droppingBeforeClassName","droppingAfterClassName","dataTransfer","setData","$index","toString","dropTimeout","move","from","to","className","dragOverHandler","offsetY","layerY","offsetX","layerX","dropHandler","droppedItemIndex","_dropHandler","theList","itemToMove","hasClass","$emit","array","onOpenCloseCallback","uisOpenClose","previousState","expression","filterMatch","grouped","run","$templateCache","put"],"mappings":"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","file":"select.min.js","sourcesContent":["/*!\n * ui-select\n * http://github.com/angular-ui/ui-select\n * Version: 0.19.7 - 2017-04-15T14:28:36.649Z\n * License: MIT\n */\n\n\n(function () { \n\"use strict\";\nvar KEY = {\n TAB: 9,\n ENTER: 13,\n ESC: 27,\n SPACE: 32,\n LEFT: 37,\n UP: 38,\n RIGHT: 39,\n DOWN: 40,\n SHIFT: 16,\n CTRL: 17,\n ALT: 18,\n PAGE_UP: 33,\n PAGE_DOWN: 34,\n HOME: 36,\n END: 35,\n BACKSPACE: 8,\n DELETE: 46,\n COMMAND: 91,\n\n MAP: { 91 : \"COMMAND\", 8 : \"BACKSPACE\" , 9 : \"TAB\" , 13 : \"ENTER\" , 16 : \"SHIFT\" , 17 : \"CTRL\" , 18 : \"ALT\" , 19 : \"PAUSEBREAK\" , 20 : \"CAPSLOCK\" , 27 : \"ESC\" , 32 : \"SPACE\" , 33 : \"PAGE_UP\", 34 : \"PAGE_DOWN\" , 35 : \"END\" , 36 : \"HOME\" , 37 : \"LEFT\" , 38 : \"UP\" , 39 : \"RIGHT\" , 40 : \"DOWN\" , 43 : \"+\" , 44 : \"PRINTSCREEN\" , 45 : \"INSERT\" , 46 : \"DELETE\", 48 : \"0\" , 49 : \"1\" , 50 : \"2\" , 51 : \"3\" , 52 : \"4\" , 53 : \"5\" , 54 : \"6\" , 55 : \"7\" , 56 : \"8\" , 57 : \"9\" , 59 : \";\", 61 : \"=\" , 65 : \"A\" , 66 : \"B\" , 67 : \"C\" , 68 : \"D\" , 69 : \"E\" , 70 : \"F\" , 71 : \"G\" , 72 : \"H\" , 73 : \"I\" , 74 : \"J\" , 75 : \"K\" , 76 : \"L\", 77 : \"M\" , 78 : \"N\" , 79 : \"O\" , 80 : \"P\" , 81 : \"Q\" , 82 : \"R\" , 83 : \"S\" , 84 : \"T\" , 85 : \"U\" , 86 : \"V\" , 87 : \"W\" , 88 : \"X\" , 89 : \"Y\" , 90 : \"Z\", 96 : \"0\" , 97 : \"1\" , 98 : \"2\" , 99 : \"3\" , 100 : \"4\" , 101 : \"5\" , 102 : \"6\" , 103 : \"7\" , 104 : \"8\" , 105 : \"9\", 106 : \"*\" , 107 : \"+\" , 109 : \"-\" , 110 : \".\" , 111 : \"/\", 112 : \"F1\" , 113 : \"F2\" , 114 : \"F3\" , 115 : \"F4\" , 116 : \"F5\" , 117 : \"F6\" , 118 : \"F7\" , 119 : \"F8\" , 120 : \"F9\" , 121 : \"F10\" , 122 : \"F11\" , 123 : \"F12\", 144 : \"NUMLOCK\" , 145 : \"SCROLLLOCK\" , 186 : \";\" , 187 : \"=\" , 188 : \",\" , 189 : \"-\" , 190 : \".\" , 191 : \"/\" , 192 : \"`\" , 219 : \"[\" , 220 : \"\\\\\" , 221 : \"]\" , 222 : \"'\"\n },\n\n isControl: function (e) {\n var k = e.which;\n switch (k) {\n case KEY.COMMAND:\n case KEY.SHIFT:\n case KEY.CTRL:\n case KEY.ALT:\n return true;\n }\n\n if (e.metaKey || e.ctrlKey || e.altKey) return true;\n\n return false;\n },\n isFunctionKey: function (k) {\n k = k.which ? k.which : k;\n return k >= 112 && k <= 123;\n },\n isVerticalMovement: function (k){\n return ~[KEY.UP, KEY.DOWN].indexOf(k);\n },\n isHorizontalMovement: function (k){\n return ~[KEY.LEFT,KEY.RIGHT,KEY.BACKSPACE,KEY.DELETE].indexOf(k);\n },\n toSeparator: function (k) {\n var sep = {ENTER:\"\\n\",TAB:\"\\t\",SPACE:\" \"}[k];\n if (sep) return sep;\n // return undefined for special keys other than enter, tab or space.\n // no way to use them to cut strings.\n return KEY[k] ? undefined : k;\n }\n };\n\nfunction isNil(value) {\n return angular.isUndefined(value) || value === null;\n}\n\n/**\n * Add querySelectorAll() to jqLite.\n *\n * jqLite find() is limited to lookups by tag name.\n * TODO This will change with future versions of AngularJS, to be removed when this happens\n *\n * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586\n * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598\n */\nif (angular.element.prototype.querySelectorAll === undefined) {\n angular.element.prototype.querySelectorAll = function(selector) {\n return angular.element(this[0].querySelectorAll(selector));\n };\n}\n\n/**\n * Add closest() to jqLite.\n */\nif (angular.element.prototype.closest === undefined) {\n angular.element.prototype.closest = function( selector) {\n var elem = this[0];\n var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;\n\n while (elem) {\n if (matchesSelector.bind(elem)(selector)) {\n return elem;\n } else {\n elem = elem.parentElement;\n }\n }\n return false;\n };\n}\n\nvar latestId = 0;\n\nvar uis = angular.module('ui.select', [])\n\n.constant('uiSelectConfig', {\n theme: 'bootstrap',\n searchEnabled: true,\n sortable: false,\n placeholder: '', // Empty by default, like HTML tag \");\n $compile(focusser)(scope);\n $select.focusser = focusser;\n\n //Input that will handle focus\n $select.focusInput = focusser;\n\n element.parent().append(focusser);\n focusser.bind(\"focus\", function(){\n scope.$evalAsync(function(){\n $select.focus = true;\n });\n });\n focusser.bind(\"blur\", function(){\n scope.$evalAsync(function(){\n $select.focus = false;\n });\n });\n focusser.bind(\"keydown\", function(e){\n\n if (e.which === KEY.BACKSPACE && $select.backspaceReset !== false) {\n e.preventDefault();\n e.stopPropagation();\n $select.select(undefined);\n scope.$apply();\n return;\n }\n\n if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {\n return;\n }\n\n if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){\n e.preventDefault();\n e.stopPropagation();\n $select.activate();\n }\n\n scope.$digest();\n });\n\n focusser.bind(\"keyup input\", function(e){\n\n if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) {\n return;\n }\n\n $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input\n focusser.val('');\n scope.$digest();\n\n });\n\n\n }\n };\n}]);\n\n// Make multiple matches sortable\nuis.directive('uiSelectSort', ['$timeout', 'uiSelectConfig', 'uiSelectMinErr', function($timeout, uiSelectConfig, uiSelectMinErr) {\n return {\n require: ['^^uiSelect', '^ngModel'],\n link: function(scope, element, attrs, ctrls) {\n if (scope[attrs.uiSelectSort] === null) {\n throw uiSelectMinErr('sort', 'Expected a list to sort');\n }\n\n var $select = ctrls[0];\n var $ngModel = ctrls[1];\n\n var options = angular.extend({\n axis: 'horizontal'\n },\n scope.$eval(attrs.uiSelectSortOptions));\n\n var axis = options.axis;\n var draggingClassName = 'dragging';\n var droppingClassName = 'dropping';\n var droppingBeforeClassName = 'dropping-before';\n var droppingAfterClassName = 'dropping-after';\n\n scope.$watch(function(){\n return $select.sortable;\n }, function(newValue){\n if (newValue) {\n element.attr('draggable', true);\n } else {\n element.removeAttr('draggable');\n }\n });\n\n element.on('dragstart', function(event) {\n element.addClass(draggingClassName);\n\n (event.dataTransfer || event.originalEvent.dataTransfer).setData('text', scope.$index.toString());\n });\n\n element.on('dragend', function() {\n removeClass(draggingClassName);\n });\n\n var move = function(from, to) {\n /*jshint validthis: true */\n this.splice(to, 0, this.splice(from, 1)[0]);\n };\n\n var removeClass = function(className) {\n angular.forEach($select.$element.querySelectorAll('.' + className), function(el){\n angular.element(el).removeClass(className);\n });\n };\n\n var dragOverHandler = function(event) {\n event.preventDefault();\n\n var offset = axis === 'vertical' ? event.offsetY || event.layerY || (event.originalEvent ? event.originalEvent.offsetY : 0) : event.offsetX || event.layerX || (event.originalEvent ? event.originalEvent.offsetX : 0);\n\n if (offset < (this[axis === 'vertical' ? 'offsetHeight' : 'offsetWidth'] / 2)) {\n removeClass(droppingAfterClassName);\n element.addClass(droppingBeforeClassName);\n\n } else {\n removeClass(droppingBeforeClassName);\n element.addClass(droppingAfterClassName);\n }\n };\n\n var dropTimeout;\n\n var dropHandler = function(event) {\n event.preventDefault();\n\n var droppedItemIndex = parseInt((event.dataTransfer || event.originalEvent.dataTransfer).getData('text'), 10);\n\n // prevent event firing multiple times in firefox\n $timeout.cancel(dropTimeout);\n dropTimeout = $timeout(function() {\n _dropHandler(droppedItemIndex);\n }, 20);\n };\n\n var _dropHandler = function(droppedItemIndex) {\n var theList = scope.$eval(attrs.uiSelectSort);\n var itemToMove = theList[droppedItemIndex];\n var newIndex = null;\n\n if (element.hasClass(droppingBeforeClassName)) {\n if (droppedItemIndex < scope.$index) {\n newIndex = scope.$index - 1;\n } else {\n newIndex = scope.$index;\n }\n } else {\n if (droppedItemIndex < scope.$index) {\n newIndex = scope.$index;\n } else {\n newIndex = scope.$index + 1;\n }\n }\n\n move.apply(theList, [droppedItemIndex, newIndex]);\n\n $ngModel.$setViewValue(Date.now());\n\n scope.$apply(function() {\n scope.$emit('uiSelectSort:change', {\n array: theList,\n item: itemToMove,\n from: droppedItemIndex,\n to: newIndex\n });\n });\n\n removeClass(droppingClassName);\n removeClass(droppingBeforeClassName);\n removeClass(droppingAfterClassName);\n\n element.off('drop', dropHandler);\n };\n\n element.on('dragenter', function() {\n if (element.hasClass(draggingClassName)) {\n return;\n }\n\n element.addClass(droppingClassName);\n\n element.on('dragover', dragOverHandler);\n element.on('drop', dropHandler);\n });\n\n element.on('dragleave', function(event) {\n if (event.target != element) {\n return;\n }\n\n removeClass(droppingClassName);\n removeClass(droppingBeforeClassName);\n removeClass(droppingAfterClassName);\n\n element.off('dragover', dragOverHandler);\n element.off('drop', dropHandler);\n });\n }\n };\n}]);\n\nuis.directive('uisOpenClose', ['$parse', '$timeout', function ($parse, $timeout) {\n return {\n restrict: 'A',\n require: 'uiSelect',\n link: function (scope, element, attrs, $select) {\n $select.onOpenCloseCallback = $parse(attrs.uisOpenClose);\n\n scope.$watch('$select.open', function (isOpen, previousState) {\n if (isOpen !== previousState) {\n $timeout(function () {\n $select.onOpenCloseCallback(scope, {\n isOpen: isOpen\n });\n });\n }\n });\n }\n };\n}]);\n\n/**\n * Parses \"repeat\" attribute.\n *\n * Taken from AngularJS ngRepeat source code\n * See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L211\n *\n * Original discussion about parsing \"repeat\" attribute instead of fully relying on ng-repeat:\n * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697\n */\n\nuis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) {\n var self = this;\n\n /**\n * Example:\n * expression = \"address in addresses | filter: {street: $select.search} track by $index\"\n * itemName = \"address\",\n * source = \"addresses | filter: {street: $select.search}\",\n * trackByExp = \"$index\",\n */\n self.parse = function(expression) {\n\n\n var match;\n //var isObjectCollection = /\\(\\s*([\\$\\w][\\$\\w]*)\\s*,\\s*([\\$\\w][\\$\\w]*)\\s*\\)/.test(expression);\n // If an array is used as collection\n\n // if (isObjectCollection){\n // 000000000000000000000000000000111111111000000000000000222222222222220033333333333333333333330000444444444444444444000000000000000055555555555000000000000000000000066666666600000000\n match = expression.match(/^\\s*(?:([\\s\\S]+?)\\s+as\\s+)?(?:([\\$\\w][\\$\\w]*)|(?:\\(\\s*([\\$\\w][\\$\\w]*)\\s*,\\s*([\\$\\w][\\$\\w]*)\\s*\\)))\\s+in\\s+(\\s*[\\s\\S]+?)?(?:\\s+track\\s+by\\s+([\\s\\S]+?))?\\s*$/);\n\n // 1 Alias\n // 2 Item\n // 3 Key on (key,value)\n // 4 Value on (key,value)\n // 5 Source expression (including filters)\n // 6 Track by\n\n if (!match) {\n throw uiSelectMinErr('iexp', \"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.\",\n expression);\n }\n \n var source = match[5], \n filters = '';\n\n // When using (key,value) ui-select requires filters to be extracted, since the object\n // is converted to an array for $select.items \n // (in which case the filters need to be reapplied)\n if (match[3]) {\n // Remove any enclosing parenthesis\n source = match[5].replace(/(^\\()|(\\)$)/g, '');\n // match all after | but not after ||\n var filterMatch = match[5].match(/^\\s*(?:[\\s\\S]+?)(?:[^\\|]|\\|\\|)+([\\s\\S]*)\\s*$/);\n if(filterMatch && filterMatch[1].trim()) {\n filters = filterMatch[1];\n source = source.replace(filters, '');\n } \n }\n\n return {\n itemName: match[4] || match[2], // (lhs) Left-hand side,\n keyName: match[3], //for (key, value) syntax\n source: $parse(source),\n filters: filters,\n trackByExp: match[6],\n modelMapper: $parse(match[1] || match[4] || match[2]),\n repeatExpression: function (grouped) {\n var expression = this.itemName + ' in ' + (grouped ? '$group.items' : '$select.items');\n if (this.trackByExp) {\n expression += ' track by ' + this.trackByExp;\n }\n return expression;\n } \n };\n\n };\n\n self.getGroupNgRepeatExpression = function() {\n return '$group in $select.groups track by $group.name';\n };\n\n}]);\n\n}());\nangular.module(\"ui.select\").run([\"$templateCache\", function($templateCache) {$templateCache.put(\"bootstrap/choices.tpl.html\",\"
      0\\\">
    • 0\\\">
    \");\n$templateCache.put(\"bootstrap/match-multiple.tpl.html\",\" × \");\n$templateCache.put(\"bootstrap/match.tpl.html\",\"
    {{$select.placeholder}}
    \");\n$templateCache.put(\"bootstrap/no-choice.tpl.html\",\"
    \");\n$templateCache.put(\"bootstrap/select-multiple.tpl.html\",\"
    \");\n$templateCache.put(\"bootstrap/select.tpl.html\",\"
    \");\n$templateCache.put(\"select2/choices.tpl.html\",\"
    \");\n$templateCache.put(\"select2/match-multiple.tpl.html\",\"
  • \");\n$templateCache.put(\"select2/match.tpl.html\",\"{{$select.placeholder}} \");\n$templateCache.put(\"select2/no-choice.tpl.html\",\"
    \");\n$templateCache.put(\"select2/select-multiple.tpl.html\",\"
    \");\n$templateCache.put(\"select2/select.tpl.html\",\"
    \");\n$templateCache.put(\"selectize/choices.tpl.html\",\"
    \");\n$templateCache.put(\"selectize/match-multiple.tpl.html\",\"
    ×
    \");\n$templateCache.put(\"selectize/match.tpl.html\",\"
    {{$select.placeholder}}
    \");\n$templateCache.put(\"selectize/no-choice.tpl.html\",\"
    \");\n$templateCache.put(\"selectize/select-multiple.tpl.html\",\"
    \");\n$templateCache.put(\"selectize/select.tpl.html\",\"
    \");}]);",null,null]} \ No newline at end of file diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less index bbe3463cba..e09d0d56d6 100644 --- a/services/web/public/stylesheets/_style_includes.less +++ b/services/web/public/stylesheets/_style_includes.less @@ -41,6 +41,8 @@ @import "components/close.less"; @import "components/fineupload.less"; @import "components/hover.less"; +@import "components/ui-select.less"; +@import "components/input-suggestions.less"; // Components w/ JavaScript @import "components/modals.less"; @@ -87,4 +89,5 @@ @import "../js/libs/pdfListView/TextLayer.css"; @import "../js/libs/pdfListView/AnnotationsLayer.css"; @import "../js/libs/pdfListView/HighlightsLayer.css"; +@import "../js/libs/select/select.css"; @import "vendor/codemirror.css"; diff --git a/services/web/public/stylesheets/components/ui-select.less b/services/web/public/stylesheets/components/ui-select.less new file mode 100644 index 0000000000..a1ce7ecc8f --- /dev/null +++ b/services/web/public/stylesheets/components/ui-select.less @@ -0,0 +1,51 @@ +.ui-select-bootstrap > .ui-select-choices, +.ui-select-bootstrap > .ui-select-no-choice { + width: auto; + max-width: 400px; +} + +.dropdown-menu .ui-select-choices-row { + padding: 4px 0; + + > .ui-select-choices-row-inner { + overflow: hidden; + text-overflow: ellipsis; + } +} + +.ui-select-placeholder, +.ui-select-match-text { + overflow: hidden; + text-overflow: ellipsis; + font-weight: normal; +} + +.ui-select-bootstrap { + &:focus { + outline: none; + } + > .ui-select-match { + &:focus { + outline: none; + } + &.btn-default-focus { + outline: 0; + box-shadow: none; + background-color: transparent; + > .btn { + border-color: @input-border-focus; + box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px fade(@input-border-focus, 60%); + } + } + > .btn { + color: @input-color; + background-color: @input-bg; + border: 1px solid @input-border; + &[disabled] { + cursor: not-allowed; + background-color: @input-bg-disabled; + opacity: 1; + } + } + } +} \ No newline at end of file From 5ada231d15409f861735f51cc582268e65f8b99c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 15 Jun 2018 11:30:15 +0100 Subject: [PATCH 03/27] Add basic affiliations styles, data services and controller. --- .../UserAffiliationsController.coffee | 53 +++++++++++++++++++ .../UserAffiliationsDataService.coffee | 39 ++++++++++++++ .../stylesheets/app/account-settings.less | 37 +++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee create mode 100644 services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee new file mode 100644 index 0000000000..d07b656624 --- /dev/null +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -0,0 +1,53 @@ +define [ + "base" +], (App) -> + App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", ($scope, UserAffiliationsDataService, $q) -> + $scope.countries = [] + $scope.universities = [] + $scope.newAffiliation = + email: "" + country: null + university: null + role: null + department: null + autoDetectMode: true + + EMAIL_REGEXP = /([^@]+)@(.+)/ + + _matchEmail = (email) -> + match = email.match EMAIL_REGEXP + if match? + { local: match[1], domain: match[2] } + else + { local: null, domain: null } + + + $scope.addUniversityToSelection = (universityName) -> + { name: universityName, country_code: $scope.newAffiliation.country.code } + + $scope.getEmailSuggestion = (userInput) -> + matchedEmail = _matchEmail(userInput) + if matchedEmail.domain? + UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(matchedEmail.domain) + .then (universityDomain) -> + $scope.newAffiliation.university = universityDomain.university.name + $scope.newAffiliation.department = universityDomain.department + $q.resolve "#{matchedEmail.local}@#{universityDomain.hostname}" + .catch () -> + $scope.newAffiliation.university = null + $scope.newAffiliation.department = null + $q.reject null + else + $q.resolve null + + UserAffiliationsDataService + .getCountries() + .then (countries) -> $scope.countries = countries + + $scope.$watch "newAffiliation.country", (newSelectedCountry, prevSelectedCountry) -> + if newSelectedCountry? and newSelectedCountry != prevSelectedCountry + $scope.newAffiliation.university = null + UserAffiliationsDataService + .getUniversitiesFromCountry(newSelectedCountry) + .then (universities) -> $scope.universities = universities + ] \ No newline at end of file diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee new file mode 100644 index 0000000000..c90ff65baf --- /dev/null +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -0,0 +1,39 @@ +define [ + "base" +], (App) -> + # TODO Get actual countries list from the DB + countriesList = [{ code : "af", name: "Afghanistan" }, { code : "ax", name: "Åland Islands" }, { code : "al", name: "Albania" }, { code : "dz", name: "Algeria" }, { code : "as", name: "American Samoa" }, { code : "ad", name: "Andorra" }, { code : "ao", name: "Angola" }, { code : "ai", name: "Anguilla" }, { code : "aq", name: "Antarctica" }, { code : "ag", name: "Antigua and Barbuda" }, { code : "ar", name: "Argentina" }, { code : "am", name: "Armenia" }, { code : "aw", name: "Aruba" }, { code : "au", name: "Australia" }, { code : "at", name: "Austria" }, { code : "az", name: "Azerbaijan" }, { code : "bs", name: "Bahamas" }, { code : "bh", name: "Bahrain" }, { code : "bd", name: "Bangladesh" }, { code : "bb", name: "Barbados" }, { code : "by", name: "Belarus" }, { code : "be", name: "Belgium" }, { code : "bz", name: "Belize" }, { code : "bj", name: "Benin" }, { code : "bm", name: "Bermuda" }, { code : "bt", name: "Bhutan" }, { code : "bo", name: "Bolivia" }, { code : "bq", name: "Bonaire, Saint Eustatius and Saba" }, { code : "ba", name: "Bosnia and Herzegovina" }, { code : "bw", name: "Botswana" }, { code : "bv", name: "Bouvet Island" }, { code : "br", name: "Brazil" }, { code : "io", name: "British Indian Ocean Territory" }, { code : "vg", name: "British Virgin Islands" }, { code : "bn", name: "Brunei" }, { code : "bg", name: "Bulgaria" }, { code : "bf", name: "Burkina Faso" }, { code : "bi", name: "Burundi" }, { code : "kh", name: "Cambodia" }, { code : "cm", name: "Cameroon" }, { code : "ca", name: "Canada" }, { code : "cv", name: "Cabo Verde" }, { code : "ky", name: "Cayman Islands" }, { code : "cf", name: "Central African Republic" }, { code : "td", name: "Chad" }, { code : "cl", name: "Chile" }, { code : "cn", name: "China" }, { code : "cx", name: "Christmas Island" }, { code : "cc", name: "Cocos (Keeling) Islands" }, { code : "co", name: "Colombia" }, { code : "km", name: "Comoros" }, { code : "cg", name: "Congo" }, { code : "ck", name: "Cook Islands" }, { code : "cr", name: "Costa Rica" }, { code : "ci", name: "Côte d'Ivoire" }, { code : "hr", name: "Croatia" }, { code : "cu", name: "Cuba" }, { code : "cw", name: "Curaçao" }, { code : "cy", name: "Cyprus" }, { code : "cz", name: "Czech Republic" }, { code : "kp", name: "Democratic People's Republic of Korea" }, { code : "cd", name: "Democratic Republic of the Congo" }, { code : "dk", name: "Denmark" }, { code : "dj", name: "Djibouti" }, { code : "dm", name: "Dominica" }, { code : "do", name: "Dominican Republic" }, { code : "ec", name: "Ecuador" }, { code : "eg", name: "Egypt" }, { code : "sv", name: "El Salvador" }, { code : "gq", name: "Equatorial Guinea" }, { code : "er", name: "Eritrea" }, { code : "ee", name: "Estonia" }, { code : "et", name: "Ethiopia" }, { code : "fk", name: "Falkland Islands (Malvinas)" }, { code : "fo", name: "Faroe Islands" }, { code : "fj", name: "Fiji" }, { code : "fi", name: "Finland" }, { code : "fr", name: "France" }, { code : "gf", name: "French Guiana" }, { code : "pf", name: "French Polynesia" }, { code : "tf", name: "French Southern Territories" }, { code : "ga", name: "Gabon" }, { code : "gm", name: "Gambia" }, { code : "ge", name: "Georgia" }, { code : "de", name: "Germany" }, { code : "gh", name: "Ghana" }, { code : "gi", name: "Gibraltar" }, { code : "gr", name: "Greece" }, { code : "gl", name: "Greenland" }, { code : "gd", name: "Grenada" }, { code : "gp", name: "Guadeloupe" }, { code : "gu", name: "Guam" }, { code : "gt", name: "Guatemala" }, { code : "gg", name: "Guernsey" }, { code : "gn", name: "Guinea" }, { code : "gw", name: "Guinea-Bissau" }, { code : "gy", name: "Guyana" }, { code : "ht", name: "Haiti" }, { code : "hm", name: "Heard Island and McDonald Islands" }, { code : "va", name: "Holy See (Vatican City)" }, { code : "hn", name: "Honduras" }, { code : "hk", name: "Hong Kong" }, { code : "hu", name: "Hungary" }, { code : "is", name: "Iceland" }, { code : "in", name: "India" }, { code : "id", name: "Indonesia" }, { code : "ir", name: "Iran" }, { code : "iq", name: "Iraq" }, { code : "ie", name: "Ireland" }, { code : "im", name: "Isle of Man" }, { code : "il", name: "Israel" }, { code : "it", name: "Italy" }, { code : "jm", name: "Jamaica" }, { code : "jp", name: "Japan" }, { code : "je", name: "Jersey" }, { code : "jo", name: "Jordan" }, { code : "kz", name: "Kazakhstan" }, { code : "ke", name: "Kenya" }, { code : "ki", name: "Kiribati" }, { code : "xk", name: "Kosovo" }, { code : "kw", name: "Kuwait" }, { code : "kg", name: "Kyrgyzstan" }, { code : "la", name: "Laos" }, { code : "lv", name: "Latvia" }, { code : "lb", name: "Lebanon" }, { code : "ls", name: "Lesotho" }, { code : "lr", name: "Liberia" }, { code : "ly", name: "Libya" }, { code : "li", name: "Liechtenstein" }, { code : "lt", name: "Lithuania" }, { code : "lu", name: "Luxembourg" }, { code : "mo", name: "Macao" }, { code : "mk", name: "Macedonia" }, { code : "mg", name: "Madagascar" }, { code : "mw", name: "Malawi" }, { code : "my", name: "Malaysia" }, { code : "mv", name: "Maldives" }, { code : "ml", name: "Mali" }, { code : "mt", name: "Malta" }, { code : "mh", name: "Marshall Islands" }, { code : "mq", name: "Martinique" }, { code : "mr", name: "Mauritania" }, { code : "mu", name: "Mauritius" }, { code : "yt", name: "Mayotte" }, { code : "mx", name: "Mexico" }, { code : "fm", name: "Micronesia" }, { code : "md", name: "Moldova" }, { code : "mc", name: "Monaco" }, { code : "mn", name: "Mongolia" }, { code : "me", name: "Montenegro" }, { code : "ms", name: "Montserrat" }, { code : "ma", name: "Morocco" }, { code : "mz", name: "Mozambique" }, { code : "mm", name: "Myanmar" }, { code : "na", name: "Namibia" }, { code : "nr", name: "Nauru" }, { code : "np", name: "Nepal" }, { code : "nl", name: "Netherlands" }, { code : "an", name: "Netherlands Antilles" }, { code : "nc", name: "New Caledonia" }, { code : "nz", name: "New Zealand" }, { code : "ni", name: "Nicaragua" }, { code : "ne", name: "Niger" }, { code : "ng", name: "Nigeria" }, { code : "nu", name: "Niue" }, { code : "nf", name: "Norfolk Island" }, { code : "mp", name: "Northern Mariana Islands" }, { code : "no", name: "Norway" }, { code : "om", name: "Oman" }, { code : "pk", name: "Pakistan" }, { code : "pw", name: "Palau" }, { code : "ps", name: "Palestine" }, { code : "pa", name: "Panama" }, { code : "pg", name: "Papua New Guinea" }, { code : "py", name: "Paraguay" }, { code : "pe", name: "Peru" }, { code : "ph", name: "Philippines" }, { code : "pn", name: "Pitcairn" }, { code : "pl", name: "Poland" }, { code : "pt", name: "Portugal" }, { code : "pr", name: "Puerto Rico" }, { code : "qa", name: "Qatar" }, { code : "kr", name: "Republic of Korea" }, { code : "re", name: "Réunion" }, { code : "ro", name: "Romania" }, { code : "ru", name: "Russia" }, { code : "rw", name: "Rwanda" }, { code : "bl", name: "Saint Barthélemy" }, { code : "sh", name: "Saint Helena, Ascension and Tristan da Cunha" }, { code : "kn", name: "Saint Kitts and Nevis" }, { code : "lc", name: "Saint Lucia" }, { code : "mf", name: "Saint Martin" }, { code : "pm", name: "Saint Pierre and Miquelon" }, { code : "vc", name: "Saint Vincent and the Grenadines" }, { code : "ws", name: "Samoa" }, { code : "sm", name: "San Marino" }, { code : "st", name: "Sao Tome and Principe" }, { code : "sa", name: "Saudi Arabia" }, { code : "sn", name: "Senegal" }, { code : "rs", name: "Serbia" }, { code : "sc", name: "Seychelles" }, { code : "sl", name: "Sierra Leone" }, { code : "sg", name: "Singapore" }, { code : "sx", name: "Sint Maarten" }, { code : "sk", name: "Slovakia" }, { code : "si", name: "Slovenia" }, { code : "sb", name: "Solomon Islands" }, { code : "so", name: "Somalia" }, { code : "za", name: "South Africa" }, { code : "gs", name: "South Georgia and the South Sandwich Islands" }, { code : "ss", name: "South Sudan" }, { code : "es", name: "Spain" }, { code : "lk", name: "Sri Lanka" }, { code : "sd", name: "Sudan" }, { code : "sr", name: "Suriname" }, { code : "sj", name: "Svalbard and Jan Mayen" }, { code : "sz", name: "Swaziland" }, { code : "se", name: "Sweden" }, { code : "ch", name: "Switzerland" }, { code : "sy", name: "Syria" }, { code : "tw", name: "Taiwan" }, { code : "tj", name: "Tajikistan" }, { code : "tz", name: "Tanzania" }, { code : "th", name: "Thailand" }, { code : "tl", name: "Timor-Leste" }, { code : "tg", name: "Togo" }, { code : "tk", name: "Tokelau" }, { code : "to", name: "Tonga" }, { code : "tt", name: "Trinidad and Tobago" }, { code : "tn", name: "Tunisia" }, { code : "tr", name: "Turkey" }, { code : "tm", name: "Turkmenistan" }, { code : "tc", name: "Turks and Caicos Islands" }, { code : "tv", name: "Tuvalu" }, { code : "vi", name: "U.S. Virgin Islands" }, { code : "ug", name: "Uganda" }, { code : "ua", name: "Ukraine" }, { code : "ae", name: "United Arab Emirates" }, { code : "gb", name: "United Kingdom" }, { code : "us", name: "United States of America" }, { code : "um", name: "United States Minor Outlying Islands" }, { code : "uy", name: "Uruguay" }, { code : "uz", name: "Uzbekistan" }, { code : "vu", name: "Vanuatu" }, { code : "ve", name: "Venezuela" }, { code : "vn", name: "Vietnam" }, { code : "wf", name: "Wallis and Futuna" }, { code : "eh", name: "Western Sahara" }, { code : "ye", name: "Yemen" }, { code : "zm", name: "Zambia" }, { code : "zw", name: "Zimbabwe" } ] + universitiesByDomain = {} + + App.factory "UserAffiliationsDataService", ["$http", "$q", "_", ($http, $q, _) -> + getCountries = () -> + $q.resolve(countriesList) + + getUniversitiesFromCountry = (country) -> + if universities[country.code]? + universitiesFromCountry = universities[country.code] + else + universitiesFromCountry = $http + .get "http://www.overleaf.test:5000/universities/list.json" , { params: { country_code: country.code } } + .then (response) -> universities[country.code] = response.data + $q.resolve(universitiesFromCountry) + + getUniversityDomainFromPartialDomainInput = (partialDomainInput) -> + if universitiesByDomain[partialDomainInput]? + $q.resolve universitiesByDomain[partialDomainInput] + else + $http.get "http://www.overleaf.test:5000/university/domains.json" , { params: { hostname: partialDomainInput, limit: 1 } } + .then (response) -> + university = response.data[0] + if university? + universitiesByDomain[university.hostname] = university + $q.resolve university + else + $q.reject null + + return { + getCountries + getUniversitiesFromCountry + getUniversityDomainFromPartialDomainInput + } + ] \ No newline at end of file diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index c232e36fab..6fdf398d8b 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -10,3 +10,40 @@ margin-bottom: 4px; } } + + +.affiliations-form-row { + display: flex; + flex-wrap: wrap; + margin-left: -(@grid-gutter-width / 2); + margin-right: -(@grid-gutter-width / 2); + @media (min-width: @screen-sm-min) { + align-items: flex-end; + } +} + .affiliation-col-4, + .affiliation-col-8 { + flex: 0 0 100%; + padding-left: (@grid-gutter-width / 2); + padding-right: (@grid-gutter-width / 2); + margin-bottom: (@grid-gutter-width / 2); + } + + .affiliation-col-4 { + @media (min-width: @screen-sm-min) { + flex-basis: (4 / 12) * 100%;; + } + } + + .affiliation-col-8 { + @media (min-width: @screen-sm-min) { + flex-basis: (8 / 12) * 100%; + } + } + + .affiliation-col-align-right { + @media (min-width: @screen-sm-min) { + display: flex; + justify-content: flex-end; + } + } From 3ddb8d0bef5ba820e53699461fefc481942b7119 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 15 Jun 2018 11:30:34 +0100 Subject: [PATCH 04/27] Add affiliations UI. --- services/web/app/views/user/settings.pug | 102 ++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 317214f4d7..9ff94f4714 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -4,7 +4,7 @@ block content .content.content-alt .container .row - .col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + .col-md-12.col-lg-10.col-lg-offset-1 .card .page-header h1 #{translate("account_settings")} @@ -116,6 +116,106 @@ block content ng-disabled="changePasswordForm.$invalid" ) #{translate("change")} + hr + div( + ng-controller="UserAffiliationsController" + ) + h3 Affiliations + + //- p Do you use multiple e-mail addresses? If so, add those here. + + //- p Why should I do this? + + //- p Others users will be able to find you under different e-mail address when sharing projects. + + //- p If your e-mail is an institutional e-mail, your account will be associated with that institution (and gain acess to any benefits the institution may have). + + .affiliations-form-row + .affiliation-col-4 + label(for="affiliations-email") #{translate("email")} + input-suggestions( + ng-model="newAffiliation.email" + get-suggestion="getEmailSuggestion(userInput)" + input-id="affilitations-email" + input-placeholder="e.g. johndoe@mit.edu" + ) + .affiliation-col-8( + ng-if="newAffiliation.autoDetected" + ) + p(ng-if="newAffiliation.university") {{ newAffiliation.university }} + p(ng-if="!newAffiliation.university") Start by adding your institutional email address. + + .affiliation-col-4( + ng-if="!newAffiliation.autoDetected" + ) + label Institution + ui-select( + ng-model="newAffiliation.country" + ) + ui-select-match( + placeholder="Select your country" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="country in countries | filter: $select.search" + ) + span( + ng-bind="country.name" + ) + .affiliation-col-4(ng-if="!newAffiliation.autoDetected") + ui-select( + ng-model="newAffiliation.university" + ng-disabled="!newAffiliation.country" + tagging="addUniversityToSelection" + tagging-label="false" + ) + ui-select-match( + placeholder="Select your university" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="university in universities | filter: $select.search" + ) + span( + ng-bind="university.name" + ) + .affiliation-col-4 + label(for="affiliations-role") Role + input.form-control( + type="text" + id="affiliations-role" + placeholder="e.g. Professor" + ng-model="newAffiliation.role" + ) + .affiliation-col-4 + label(for="affiliations-department") Department + input.form-control( + type="text" + id="affiliations-department" + placeholder="e.g. Mathematics" + ng-model="newAffiliation.department" + ) + .affiliation-col-4.affiliation-col-align-right + button.btn.btn-primary( + ) Add new affiliation + + //- p Your current affiliations + + table.table + thead + tr + th Institution and role + th Affiliated e-mail + th + tbody + tr + td Universidade de Aveiro + td paulojreis@ua.pt + td Remove + tr + td Universidade do Porto + td paulojreis@fe.up.pt + td Remove + + | !{moduleIncludes("userSettings", locals)} //- The beta program doesn't make much sense to include while v2 is going From 82f359e73507184aa4c672ace3e5e0af39d7d384 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 19 Jun 2018 17:10:02 +0100 Subject: [PATCH 05/27] Support more input configuration attributes in the input suggestions component. --- .../coffee/components/inputSuggestions.coffee | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/services/web/public/coffee/components/inputSuggestions.coffee b/services/web/public/coffee/components/inputSuggestions.coffee index c5bfbbf933..c9907fe26e 100644 --- a/services/web/public/coffee/components/inputSuggestions.coffee +++ b/services/web/public/coffee/components/inputSuggestions.coffee @@ -12,13 +12,13 @@ define [ ctrl.showHint = false ctrl.hasFocus = false ctrl.suggestion = null + ctrl.onBlur() ctrl.handleKeyDown = ($event) -> if ($event.which == 9 or $event.which == 13) and ctrl.suggestion? and ctrl.suggestion != "" $event.preventDefault() ctrl.localNgModel += ctrl.suggestion - ctrl.suggestion = null - ctrl.showHint = false - + ctrl.suggestion = null + ctrl.showHint = false $scope.$watch "$ctrl.localNgModel", (newVal, oldVal) -> if ctrl.hasFocus and newVal != oldVal ctrl.suggestion = null @@ -34,16 +34,21 @@ define [ App.component "inputSuggestions", { bindings: localNgModel: "=ngModel" + localNgModelOptions: "=?ngModelOptions" getSuggestion: "&" - inputId: "@" - inputPlaceholder: "@" + onBlur: "&?" + inputId: "@?" + inputName: "@?" + inputPlaceholder: "@?" + inputType: "@?" + inputRequired: "=?" controller: inputSuggestionsController template: """
    - +
    """ } From c0241df15c74bab24954f31f1d3e2a92676c2545 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 19 Jun 2018 17:10:55 +0100 Subject: [PATCH 06/27] Rewrite service and controller code to send the correct payload according to use case (no uni; known uni; new uni). --- services/web/app/views/user/settings.pug | 63 +++++++++++------ .../UserAffiliationsController.coffee | 70 +++++++++++++++---- .../UserAffiliationsDataService.coffee | 46 ++++++++++++ .../stylesheets/app/account-settings.less | 17 +++-- 4 files changed, 159 insertions(+), 37 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 9ff94f4714..0c4b8ce81d 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -117,36 +117,51 @@ block content ) #{translate("change")} hr - div( + form( ng-controller="UserAffiliationsController" + novalidate + name="affiliationsForm" ) - h3 Affiliations + h3 Emails and affiliations + p Do you use multiple e-mail addresses? If so, add those here. + ul + li You'll retain access to your account even if you lose access to one of your emails + li Others users will be able to find you under different email addresses when sharing projects + li If your e-mail is affiliated with an institution with a group license, your account will be associated with that institution (and you'll gain acess to any benefits the institution may have). - //- p Do you use multiple e-mail addresses? If so, add those here. - - //- p Why should I do this? - - //- p Others users will be able to find you under different e-mail address when sharing projects. - - //- p If your e-mail is an institutional e-mail, your account will be associated with that institution (and gain acess to any benefits the institution may have). - - .affiliations-form-row + pre(style="font-size: 0.8em; position: fixed; left: 80px; top: 80px; color: black;") {{ newAffiliation | json }} + .affiliations-row .affiliation-col-4 label(for="affiliations-email") #{translate("email")} input-suggestions( ng-model="newAffiliation.email" + ng-model-options="{ allowInvalid: true }" get-suggestion="getEmailSuggestion(userInput)" + on-blur="handleEmailInputBlur()" input-id="affilitations-email" + input-name="affilitationsEmail" input-placeholder="e.g. johndoe@mit.edu" + input-type="email" + input-required="true" ) .affiliation-col-8( - ng-if="newAffiliation.autoDetected" + ng-show="newAffiliation.autoDetectMode" ) - p(ng-if="newAffiliation.university") {{ newAffiliation.university }} - p(ng-if="!newAffiliation.university") Start by adding your institutional email address. - + p.affiliation-input-feedback( + ng-if="newAffiliation.university" + ) + | {{ newAffiliation.university.name }} ( + a( + href + ng-click="selectUniversityManually();" + ) change + | ) + p.affiliation-input-feedback( + ng-if="!newAffiliation.university" + ) Start by adding your email address. + .affiliations-row .affiliation-col-4( - ng-if="!newAffiliation.autoDetected" + ng-if="!newAffiliation.autoDetectMode" ) label Institution ui-select( @@ -161,7 +176,7 @@ block content span( ng-bind="country.name" ) - .affiliation-col-4(ng-if="!newAffiliation.autoDetected") + .affiliation-col-4(ng-if="!newAffiliation.autoDetectMode") ui-select( ng-model="newAffiliation.university" ng-disabled="!newAffiliation.country" @@ -177,6 +192,9 @@ block content span( ng-bind="university.name" ) + .affiliations-row( + ng-if="newAffiliation.university" + ) .affiliation-col-4 label(for="affiliations-role") Role input.form-control( @@ -193,9 +211,14 @@ block content placeholder="e.g. Mathematics" ng-model="newAffiliation.department" ) - .affiliation-col-4.affiliation-col-align-right - button.btn.btn-primary( - ) Add new affiliation + .affiliations-row + .affiliation-col-4 + input.btn.btn-primary( + type="submit" + ng-disabled="affiliationsForm.$invalid" + ng-click="handleAffiliationFormSubmit()" + value="Add new email" + ) //- p Your current affiliations diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index d07b656624..b26122f523 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -12,34 +12,80 @@ define [ department: null autoDetectMode: true - EMAIL_REGEXP = /([^@]+)@(.+)/ + LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/ + EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z]+)$/ - _matchEmail = (email) -> - match = email.match EMAIL_REGEXP + _matchLocalAndDomain = (userEmailInput) -> + match = userEmailInput?.match LOCAL_AND_DOMAIN_REGEX if match? { local: match[1], domain: match[2] } else { local: null, domain: null } + UserAffiliationsDataService.getUserEmails() $scope.addUniversityToSelection = (universityName) -> - { name: universityName, country_code: $scope.newAffiliation.country.code } + { name: universityName, isUserSuggested: true } $scope.getEmailSuggestion = (userInput) -> - matchedEmail = _matchEmail(userInput) - if matchedEmail.domain? - UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(matchedEmail.domain) - .then (universityDomain) -> - $scope.newAffiliation.university = universityDomain.university.name - $scope.newAffiliation.department = universityDomain.department - $q.resolve "#{matchedEmail.local}@#{universityDomain.hostname}" + userInputLocalAndDomain = _matchLocalAndDomain(userInput) + if userInputLocalAndDomain.domain? + UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain) + .then (universityDomain) -> + $scope.newAffiliation.autoDetectMode = true + if userInputLocalAndDomain.domain == universityDomain.hostname + $scope.newAffiliation.university = universityDomain.university + $scope.newAffiliation.department = universityDomain.department + else + $scope.newAffiliation.university = null + $scope.newAffiliation.department = null + $q.resolve "#{userInputLocalAndDomain.local}@#{universityDomain.hostname}" .catch () -> $scope.newAffiliation.university = null $scope.newAffiliation.department = null + # If the input is already a full e-mail and we have no suggestions, then the user + # will need to manually select his institution. + if userInput.match EMAIL_REGEX + $scope.newAffiliation.autoDetectMode = false $q.reject null - else + else + $scope.newAffiliation.university = null + $scope.newAffiliation.department = null $q.resolve null + $scope.handleEmailInputBlur = () -> + if $scope.newAffiliation.autoDetectMode and !$scope.newAffiliation.university and $scope.newAffiliation.email?.match EMAIL_REGEX + $scope.newAffiliation.autoDetectMode = false + + $scope.selectUniversityManually = () -> + $scope.newAffiliation.university = null + $scope.newAffiliation.department = null + $scope.newAffiliation.autoDetectMode = false + + $scope.handleAffiliationFormSubmit = () -> + if !$scope.newAffiliation.university? + UserAffiliationsDataService.addUserEmail( + $scope.newAffiliation.email, + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) + else + if $scope.newAffiliation.university.isUserSuggested + UserAffiliationsDataService.addUserAffiliationWithUnknownUniversity( + $scope.newAffiliation.email, + $scope.newAffiliation.university.name, + $scope.newAffiliation.country.code, + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) + else + UserAffiliationsDataService.addUserAffiliation( + $scope.newAffiliation.email, + $scope.newAffiliation.university.id + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) + UserAffiliationsDataService .getCountries() .then (countries) -> $scope.countries = countries diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index c90ff65baf..76827273c0 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -3,12 +3,26 @@ define [ ], (App) -> # TODO Get actual countries list from the DB countriesList = [{ code : "af", name: "Afghanistan" }, { code : "ax", name: "Åland Islands" }, { code : "al", name: "Albania" }, { code : "dz", name: "Algeria" }, { code : "as", name: "American Samoa" }, { code : "ad", name: "Andorra" }, { code : "ao", name: "Angola" }, { code : "ai", name: "Anguilla" }, { code : "aq", name: "Antarctica" }, { code : "ag", name: "Antigua and Barbuda" }, { code : "ar", name: "Argentina" }, { code : "am", name: "Armenia" }, { code : "aw", name: "Aruba" }, { code : "au", name: "Australia" }, { code : "at", name: "Austria" }, { code : "az", name: "Azerbaijan" }, { code : "bs", name: "Bahamas" }, { code : "bh", name: "Bahrain" }, { code : "bd", name: "Bangladesh" }, { code : "bb", name: "Barbados" }, { code : "by", name: "Belarus" }, { code : "be", name: "Belgium" }, { code : "bz", name: "Belize" }, { code : "bj", name: "Benin" }, { code : "bm", name: "Bermuda" }, { code : "bt", name: "Bhutan" }, { code : "bo", name: "Bolivia" }, { code : "bq", name: "Bonaire, Saint Eustatius and Saba" }, { code : "ba", name: "Bosnia and Herzegovina" }, { code : "bw", name: "Botswana" }, { code : "bv", name: "Bouvet Island" }, { code : "br", name: "Brazil" }, { code : "io", name: "British Indian Ocean Territory" }, { code : "vg", name: "British Virgin Islands" }, { code : "bn", name: "Brunei" }, { code : "bg", name: "Bulgaria" }, { code : "bf", name: "Burkina Faso" }, { code : "bi", name: "Burundi" }, { code : "kh", name: "Cambodia" }, { code : "cm", name: "Cameroon" }, { code : "ca", name: "Canada" }, { code : "cv", name: "Cabo Verde" }, { code : "ky", name: "Cayman Islands" }, { code : "cf", name: "Central African Republic" }, { code : "td", name: "Chad" }, { code : "cl", name: "Chile" }, { code : "cn", name: "China" }, { code : "cx", name: "Christmas Island" }, { code : "cc", name: "Cocos (Keeling) Islands" }, { code : "co", name: "Colombia" }, { code : "km", name: "Comoros" }, { code : "cg", name: "Congo" }, { code : "ck", name: "Cook Islands" }, { code : "cr", name: "Costa Rica" }, { code : "ci", name: "Côte d'Ivoire" }, { code : "hr", name: "Croatia" }, { code : "cu", name: "Cuba" }, { code : "cw", name: "Curaçao" }, { code : "cy", name: "Cyprus" }, { code : "cz", name: "Czech Republic" }, { code : "kp", name: "Democratic People's Republic of Korea" }, { code : "cd", name: "Democratic Republic of the Congo" }, { code : "dk", name: "Denmark" }, { code : "dj", name: "Djibouti" }, { code : "dm", name: "Dominica" }, { code : "do", name: "Dominican Republic" }, { code : "ec", name: "Ecuador" }, { code : "eg", name: "Egypt" }, { code : "sv", name: "El Salvador" }, { code : "gq", name: "Equatorial Guinea" }, { code : "er", name: "Eritrea" }, { code : "ee", name: "Estonia" }, { code : "et", name: "Ethiopia" }, { code : "fk", name: "Falkland Islands (Malvinas)" }, { code : "fo", name: "Faroe Islands" }, { code : "fj", name: "Fiji" }, { code : "fi", name: "Finland" }, { code : "fr", name: "France" }, { code : "gf", name: "French Guiana" }, { code : "pf", name: "French Polynesia" }, { code : "tf", name: "French Southern Territories" }, { code : "ga", name: "Gabon" }, { code : "gm", name: "Gambia" }, { code : "ge", name: "Georgia" }, { code : "de", name: "Germany" }, { code : "gh", name: "Ghana" }, { code : "gi", name: "Gibraltar" }, { code : "gr", name: "Greece" }, { code : "gl", name: "Greenland" }, { code : "gd", name: "Grenada" }, { code : "gp", name: "Guadeloupe" }, { code : "gu", name: "Guam" }, { code : "gt", name: "Guatemala" }, { code : "gg", name: "Guernsey" }, { code : "gn", name: "Guinea" }, { code : "gw", name: "Guinea-Bissau" }, { code : "gy", name: "Guyana" }, { code : "ht", name: "Haiti" }, { code : "hm", name: "Heard Island and McDonald Islands" }, { code : "va", name: "Holy See (Vatican City)" }, { code : "hn", name: "Honduras" }, { code : "hk", name: "Hong Kong" }, { code : "hu", name: "Hungary" }, { code : "is", name: "Iceland" }, { code : "in", name: "India" }, { code : "id", name: "Indonesia" }, { code : "ir", name: "Iran" }, { code : "iq", name: "Iraq" }, { code : "ie", name: "Ireland" }, { code : "im", name: "Isle of Man" }, { code : "il", name: "Israel" }, { code : "it", name: "Italy" }, { code : "jm", name: "Jamaica" }, { code : "jp", name: "Japan" }, { code : "je", name: "Jersey" }, { code : "jo", name: "Jordan" }, { code : "kz", name: "Kazakhstan" }, { code : "ke", name: "Kenya" }, { code : "ki", name: "Kiribati" }, { code : "xk", name: "Kosovo" }, { code : "kw", name: "Kuwait" }, { code : "kg", name: "Kyrgyzstan" }, { code : "la", name: "Laos" }, { code : "lv", name: "Latvia" }, { code : "lb", name: "Lebanon" }, { code : "ls", name: "Lesotho" }, { code : "lr", name: "Liberia" }, { code : "ly", name: "Libya" }, { code : "li", name: "Liechtenstein" }, { code : "lt", name: "Lithuania" }, { code : "lu", name: "Luxembourg" }, { code : "mo", name: "Macao" }, { code : "mk", name: "Macedonia" }, { code : "mg", name: "Madagascar" }, { code : "mw", name: "Malawi" }, { code : "my", name: "Malaysia" }, { code : "mv", name: "Maldives" }, { code : "ml", name: "Mali" }, { code : "mt", name: "Malta" }, { code : "mh", name: "Marshall Islands" }, { code : "mq", name: "Martinique" }, { code : "mr", name: "Mauritania" }, { code : "mu", name: "Mauritius" }, { code : "yt", name: "Mayotte" }, { code : "mx", name: "Mexico" }, { code : "fm", name: "Micronesia" }, { code : "md", name: "Moldova" }, { code : "mc", name: "Monaco" }, { code : "mn", name: "Mongolia" }, { code : "me", name: "Montenegro" }, { code : "ms", name: "Montserrat" }, { code : "ma", name: "Morocco" }, { code : "mz", name: "Mozambique" }, { code : "mm", name: "Myanmar" }, { code : "na", name: "Namibia" }, { code : "nr", name: "Nauru" }, { code : "np", name: "Nepal" }, { code : "nl", name: "Netherlands" }, { code : "an", name: "Netherlands Antilles" }, { code : "nc", name: "New Caledonia" }, { code : "nz", name: "New Zealand" }, { code : "ni", name: "Nicaragua" }, { code : "ne", name: "Niger" }, { code : "ng", name: "Nigeria" }, { code : "nu", name: "Niue" }, { code : "nf", name: "Norfolk Island" }, { code : "mp", name: "Northern Mariana Islands" }, { code : "no", name: "Norway" }, { code : "om", name: "Oman" }, { code : "pk", name: "Pakistan" }, { code : "pw", name: "Palau" }, { code : "ps", name: "Palestine" }, { code : "pa", name: "Panama" }, { code : "pg", name: "Papua New Guinea" }, { code : "py", name: "Paraguay" }, { code : "pe", name: "Peru" }, { code : "ph", name: "Philippines" }, { code : "pn", name: "Pitcairn" }, { code : "pl", name: "Poland" }, { code : "pt", name: "Portugal" }, { code : "pr", name: "Puerto Rico" }, { code : "qa", name: "Qatar" }, { code : "kr", name: "Republic of Korea" }, { code : "re", name: "Réunion" }, { code : "ro", name: "Romania" }, { code : "ru", name: "Russia" }, { code : "rw", name: "Rwanda" }, { code : "bl", name: "Saint Barthélemy" }, { code : "sh", name: "Saint Helena, Ascension and Tristan da Cunha" }, { code : "kn", name: "Saint Kitts and Nevis" }, { code : "lc", name: "Saint Lucia" }, { code : "mf", name: "Saint Martin" }, { code : "pm", name: "Saint Pierre and Miquelon" }, { code : "vc", name: "Saint Vincent and the Grenadines" }, { code : "ws", name: "Samoa" }, { code : "sm", name: "San Marino" }, { code : "st", name: "Sao Tome and Principe" }, { code : "sa", name: "Saudi Arabia" }, { code : "sn", name: "Senegal" }, { code : "rs", name: "Serbia" }, { code : "sc", name: "Seychelles" }, { code : "sl", name: "Sierra Leone" }, { code : "sg", name: "Singapore" }, { code : "sx", name: "Sint Maarten" }, { code : "sk", name: "Slovakia" }, { code : "si", name: "Slovenia" }, { code : "sb", name: "Solomon Islands" }, { code : "so", name: "Somalia" }, { code : "za", name: "South Africa" }, { code : "gs", name: "South Georgia and the South Sandwich Islands" }, { code : "ss", name: "South Sudan" }, { code : "es", name: "Spain" }, { code : "lk", name: "Sri Lanka" }, { code : "sd", name: "Sudan" }, { code : "sr", name: "Suriname" }, { code : "sj", name: "Svalbard and Jan Mayen" }, { code : "sz", name: "Swaziland" }, { code : "se", name: "Sweden" }, { code : "ch", name: "Switzerland" }, { code : "sy", name: "Syria" }, { code : "tw", name: "Taiwan" }, { code : "tj", name: "Tajikistan" }, { code : "tz", name: "Tanzania" }, { code : "th", name: "Thailand" }, { code : "tl", name: "Timor-Leste" }, { code : "tg", name: "Togo" }, { code : "tk", name: "Tokelau" }, { code : "to", name: "Tonga" }, { code : "tt", name: "Trinidad and Tobago" }, { code : "tn", name: "Tunisia" }, { code : "tr", name: "Turkey" }, { code : "tm", name: "Turkmenistan" }, { code : "tc", name: "Turks and Caicos Islands" }, { code : "tv", name: "Tuvalu" }, { code : "vi", name: "U.S. Virgin Islands" }, { code : "ug", name: "Uganda" }, { code : "ua", name: "Ukraine" }, { code : "ae", name: "United Arab Emirates" }, { code : "gb", name: "United Kingdom" }, { code : "us", name: "United States of America" }, { code : "um", name: "United States Minor Outlying Islands" }, { code : "uy", name: "Uruguay" }, { code : "uz", name: "Uzbekistan" }, { code : "vu", name: "Vanuatu" }, { code : "ve", name: "Venezuela" }, { code : "vn", name: "Vietnam" }, { code : "wf", name: "Wallis and Futuna" }, { code : "eh", name: "Western Sahara" }, { code : "ye", name: "Yemen" }, { code : "zm", name: "Zambia" }, { code : "zw", name: "Zimbabwe" } ] + universities = {} universitiesByDomain = {} + + domainsBlackList = { "overleaf.com" : true } + commonTLDs = [ "br", "cn", "co", "co.jp", "co.uk", "com", "com.au", "de","fr", "in", "info", "io", "net", "no", "ru", "se", "us", "com.tw", "com.br", "pl", "it", "co.in", "com.mx" ] + commonDomains = [ "gmail", "googlemail", "icloud", "me", "yahoo", "ymail", "yahoomail", "hotmail", "live", "msn", "outlook", "gmx", "mail", "aol", "163", "mac", "qq", "o2", "libero", "126" ] + + for domain in commonDomains + for tld in commonTLDs + domainsBlackList["#{domain}.#{tld}"] = true + App.factory "UserAffiliationsDataService", ["$http", "$q", "_", ($http, $q, _) -> getCountries = () -> $q.resolve(countriesList) + getUserEmails = () -> + $http.get "/user/emails" + .then (response) -> response.data + getUniversitiesFromCountry = (country) -> if universities[country.code]? universitiesFromCountry = universities[country.code] @@ -31,9 +45,41 @@ define [ else $q.reject null + addUserEmail = (email) -> + $http.post "/user/emails", { + email + } + + addUserAffiliationWithUnknownUniversity = (email, unknownUniversityName, unknownUniversityCountryCode, role, department) -> + $http.post "/user/emails", { + email, + university: + name: unknownUniversityName + country_code: unknownUniversityCountryCode + role, + department + } + + addUserAffiliation = (email, universityId, role, department) -> + $http.post "/user/emails", { + email, + university: + id: universityId + role, + department + } + + isDomainBlacklisted = (domain) -> + domain.toLowerCase() of domainsBlackList + return { getCountries + getUserEmails getUniversitiesFromCountry getUniversityDomainFromPartialDomainInput + addUserEmail + addUserAffiliationWithUnknownUniversity + addUserAffiliation + isDomainBlacklisted } ] \ No newline at end of file diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 6fdf398d8b..95f92aa97a 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -12,9 +12,8 @@ } -.affiliations-form-row { +.affiliations-row { display: flex; - flex-wrap: wrap; margin-left: -(@grid-gutter-width / 2); margin-right: -(@grid-gutter-width / 2); @media (min-width: @screen-sm-min) { @@ -23,7 +22,9 @@ } .affiliation-col-4, .affiliation-col-8 { - flex: 0 0 100%; + width: 100%; + flex-grow: 0; + flex-shrink: 0; padding-left: (@grid-gutter-width / 2); padding-right: (@grid-gutter-width / 2); margin-bottom: (@grid-gutter-width / 2); @@ -31,13 +32,13 @@ .affiliation-col-4 { @media (min-width: @screen-sm-min) { - flex-basis: (4 / 12) * 100%;; + width: (4 / 12) * 100%; } } .affiliation-col-8 { @media (min-width: @screen-sm-min) { - flex-basis: (8 / 12) * 100%; + width: (8 / 12) * 100%; } } @@ -47,3 +48,9 @@ justify-content: flex-end; } } + + .affiliation-input-feedback { + height: @input-height-base; + padding: @padding-base-vertical; + margin: 0; + } \ No newline at end of file From f3806faeb22a309f4be754ff2a499594dfac05ae Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 20 Jun 2018 11:37:34 +0100 Subject: [PATCH 07/27] Make git ignore compiled Coffeescript Angular components. --- services/web/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/.gitignore b/services/web/.gitignore index 57ebc26c5a..26e9f30502 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -56,6 +56,7 @@ public/js/*.map public/js/libs/sharejs.js public/js/analytics/ public/js/directives/ +public/js/components/ public/js/es/ public/js/filters/ public/js/ide/ From 917776aede8b88a69e489674c8f5446be8f6278d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 20 Jun 2018 11:37:55 +0100 Subject: [PATCH 08/27] Change the UI to gradually show needed fields for affiliations. --- services/web/app/views/user/settings.pug | 35 +++++++++++-------- .../UserAffiliationsController.coffee | 28 +++++++-------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 0c4b8ce81d..02545b3a6c 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -129,7 +129,6 @@ block content li Others users will be able to find you under different email addresses when sharing projects li If your e-mail is affiliated with an institution with a group license, your account will be associated with that institution (and you'll gain acess to any benefits the institution may have). - pre(style="font-size: 0.8em; position: fixed; left: 80px; top: 80px; color: black;") {{ newAffiliation | json }} .affiliations-row .affiliation-col-4 label(for="affiliations-email") #{translate("email")} @@ -144,11 +143,9 @@ block content input-type="email" input-required="true" ) - .affiliation-col-8( - ng-show="newAffiliation.autoDetectMode" - ) + .affiliation-col-8 p.affiliation-input-feedback( - ng-if="newAffiliation.university" + ng-if="newAffiliation.university && !showManualUniversitySelectionUI" ) | {{ newAffiliation.university.name }} ( a( @@ -157,12 +154,20 @@ block content ) change | ) p.affiliation-input-feedback( - ng-if="!newAffiliation.university" + ng-if="!newAffiliation.university && !isValidEmail && !showManualUniversitySelectionUI" ) Start by adding your email address. - .affiliations-row - .affiliation-col-4( - ng-if="!newAffiliation.autoDetectMode" - ) + p.affiliation-input-feedback( + ng-if="!newAffiliation.university && isValidEmail && !isBlacklistedEmail && !showManualUniversitySelectionUI" + ) + | Is your email affiliated with an institution? + a( + href + ng-click="selectUniversityManually();" + ) Let us know + .affiliations-row( + ng-if="isValidEmail && showManualUniversitySelectionUI" + ) + .affiliation-col-4 label Institution ui-select( ng-model="newAffiliation.country" @@ -176,7 +181,7 @@ block content span( ng-bind="country.name" ) - .affiliation-col-4(ng-if="!newAffiliation.autoDetectMode") + .affiliation-col-4 ui-select( ng-model="newAffiliation.university" ng-disabled="!newAffiliation.country" @@ -193,7 +198,7 @@ block content ng-bind="university.name" ) .affiliations-row( - ng-if="newAffiliation.university" + ng-if="isValidEmail && newAffiliation.university" ) .affiliation-col-4 label(for="affiliations-role") Role @@ -225,17 +230,17 @@ block content table.table thead tr + th Email th Institution and role - th Affiliated e-mail th tbody tr - td Universidade de Aveiro td paulojreis@ua.pt + td Universidade de Aveiro td Remove tr - td Universidade do Porto td paulojreis@fe.up.pt + td Universidade do Porto td Remove diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index b26122f523..4c550bb103 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -10,7 +10,9 @@ define [ university: null role: null department: null - autoDetectMode: true + $scope.showManualUniversitySelectionUI = false + $scope.isValidEmail = false + $scope.isBlacklistedEmail = false LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/ EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z]+)$/ @@ -29,10 +31,14 @@ define [ $scope.getEmailSuggestion = (userInput) -> userInputLocalAndDomain = _matchLocalAndDomain(userInput) + $scope.isValidEmail = EMAIL_REGEX.test userInput   + $scope.isBlacklistedEmail = false if userInputLocalAndDomain.domain? + $scope.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted userInputLocalAndDomain.domain + UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain) .then (universityDomain) -> - $scope.newAffiliation.autoDetectMode = true + $scope.showManualUniversitySelectionUI = false if userInputLocalAndDomain.domain == universityDomain.hostname $scope.newAffiliation.university = universityDomain.university $scope.newAffiliation.department = universityDomain.department @@ -43,10 +49,6 @@ define [ .catch () -> $scope.newAffiliation.university = null $scope.newAffiliation.department = null - # If the input is already a full e-mail and we have no suggestions, then the user - # will need to manually select his institution. - if userInput.match EMAIL_REGEX - $scope.newAffiliation.autoDetectMode = false $q.reject null else $scope.newAffiliation.university = null @@ -54,21 +56,17 @@ define [ $q.resolve null $scope.handleEmailInputBlur = () -> - if $scope.newAffiliation.autoDetectMode and !$scope.newAffiliation.university and $scope.newAffiliation.email?.match EMAIL_REGEX - $scope.newAffiliation.autoDetectMode = false + # if $scope.newAffiliation.autoDetectMode and !$scope.newAffiliation.university and $scope.newAffiliation.email?.match EMAIL_REGEX + # $scope.newAffiliation.autoDetectMode = false $scope.selectUniversityManually = () -> $scope.newAffiliation.university = null $scope.newAffiliation.department = null - $scope.newAffiliation.autoDetectMode = false + $scope.showManualUniversitySelectionUI = true $scope.handleAffiliationFormSubmit = () -> if !$scope.newAffiliation.university? - UserAffiliationsDataService.addUserEmail( - $scope.newAffiliation.email, - $scope.newAffiliation.role, - $scope.newAffiliation.department - ) + UserAffiliationsDataService.addUserEmail $scope.newAffiliation.email else if $scope.newAffiliation.university.isUserSuggested UserAffiliationsDataService.addUserAffiliationWithUnknownUniversity( @@ -86,10 +84,12 @@ define [ $scope.newAffiliation.department ) + # Populates the countries dropdown UserAffiliationsDataService .getCountries() .then (countries) -> $scope.countries = countries + # Populates the universities dropdown (after selecting a country) $scope.$watch "newAffiliation.country", (newSelectedCountry, prevSelectedCountry) -> if newSelectedCountry? and newSelectedCountry != prevSelectedCountry $scope.newAffiliation.university = null From 147d74d92a6708f5091bfb8d961730e5fef85a89 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 20 Jun 2018 14:22:06 +0100 Subject: [PATCH 09/27] Integrate e-mails table with backend. --- services/web/app/views/user/settings.pug | 12 +++--- .../UserAffiliationsController.coffee | 42 ++++++++++++------- .../UserAffiliationsDataService.coffee | 9 ++-- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 02545b3a6c..100c6fd293 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -234,13 +234,11 @@ block content th Institution and role th tbody - tr - td paulojreis@ua.pt - td Universidade de Aveiro - td Remove - tr - td paulojreis@fe.up.pt - td Universidade do Porto + tr( + ng-repeat="userEmail in userEmails" + ) + td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} + td {{ userEmail.institution }} td Remove diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 4c550bb103..8e7caa6727 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -2,6 +2,7 @@ define [ "base" ], (App) -> App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", ($scope, UserAffiliationsDataService, $q) -> + $scope.userEmails = [] $scope.countries = [] $scope.universities = [] $scope.newAffiliation = @@ -24,8 +25,6 @@ define [ else { local: null, domain: null } - UserAffiliationsDataService.getUserEmails() - $scope.addUniversityToSelection = (universityName) -> { name: universityName, isUserSuggested: true } @@ -66,23 +65,34 @@ define [ $scope.handleAffiliationFormSubmit = () -> if !$scope.newAffiliation.university? - UserAffiliationsDataService.addUserEmail $scope.newAffiliation.email + addEmailPromise = UserAffiliationsDataService + .addUserEmail $scope.newAffiliation.email else if $scope.newAffiliation.university.isUserSuggested - UserAffiliationsDataService.addUserAffiliationWithUnknownUniversity( - $scope.newAffiliation.email, - $scope.newAffiliation.university.name, - $scope.newAffiliation.country.code, - $scope.newAffiliation.role, - $scope.newAffiliation.department - ) + addEmailPromise = UserAffiliationsDataService + .addUserAffiliationWithUnknownUniversity( + $scope.newAffiliation.email, + $scope.newAffiliation.university.name, + $scope.newAffiliation.country.code, + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) else - UserAffiliationsDataService.addUserAffiliation( - $scope.newAffiliation.email, - $scope.newAffiliation.university.id - $scope.newAffiliation.role, - $scope.newAffiliation.department - ) + addEmailPromise = UserAffiliationsDataService + .addUserAffiliation( + $scope.newAffiliation.email, + $scope.newAffiliation.university.id + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) + addEmailPromise.then () -> getUserEmails() + + # Populates the emails table + getUserEmails = () -> + UserAffiliationsDataService + .getUserEmails() + .then (emails) -> $scope.userEmails = emails + getUserEmails() # Populates the countries dropdown UserAffiliationsDataService diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 76827273c0..e344909792 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -47,7 +47,8 @@ define [ addUserEmail = (email) -> $http.post "/user/emails", { - email + email, + _csrf: window.csrfToken } addUserAffiliationWithUnknownUniversity = (email, unknownUniversityName, unknownUniversityCountryCode, role, department) -> @@ -57,7 +58,8 @@ define [ name: unknownUniversityName country_code: unknownUniversityCountryCode role, - department + department, + _csrf: window.csrfToken } addUserAffiliation = (email, universityId, role, department) -> @@ -66,7 +68,8 @@ define [ university: id: universityId role, - department + department, + _csrf: window.csrfToken } isDomainBlacklisted = (domain) -> From b16ebc1843c4aa7aa95f844fe514c6d71bae7749 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 21 Jun 2018 17:16:42 +0100 Subject: [PATCH 10/27] Read user email from the URL in DELETE requests. --- .../web/app/coffee/Features/User/UserEmailsController.coffee | 3 ++- services/web/app/coffee/router.coffee | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index 07433d2dde..8f49dce82b 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -25,7 +25,8 @@ module.exports = UserEmailsController = remove: (req, res) -> userId = AuthenticationController.getLoggedInUserId(req) - email = EmailHelper.parseEmail(req.body.email) + logger.log req.params + email = EmailHelper.parseEmail(req.params.email) return res.sendStatus 422 unless email? UserUpdater.removeEmailAddress userId, email, (error)-> diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 339b4abe1c..bfe1c60ffe 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -114,7 +114,7 @@ module.exports = class Router webRouter.post '/user/emails', AuthenticationController.requireLogin(), UserEmailsController.add - webRouter.delete '/user/emails', + webRouter.delete '/user/emails/:email', AuthenticationController.requireLogin(), UserEmailsController.remove webRouter.post '/user/emails/default', From 62ab9dbb984a45f2e145530b076ba3808c4a4b51 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 21 Jun 2018 17:18:01 +0100 Subject: [PATCH 11/27] Rearrange layout to have the new affiliation form within the affiliations table. --- services/web/app/views/user/settings.pug | 276 ++++++++++-------- .../UserAffiliationsController.coffee | 68 +++-- .../UserAffiliationsDataService.coffee | 14 + .../stylesheets/app/account-settings.less | 48 +-- 4 files changed, 221 insertions(+), 185 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 100c6fd293..15bab80385 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -14,6 +14,156 @@ block content | #{translate("thanks_settings_updated")} form-messages(for="changePasswordForm") .container-fluid + form.row( + ng-controller="UserAffiliationsController" + name="affiliationsForm" + ) + .col-md-12 + h3 Emails and Affiliations + p.small Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account. + + table.table.affiliations-table + thead + tr + th.affiliations-table-email Email + th.affiliations-table-institution Institution and role + th.affiliations-table-inline-actions + tbody + tr( + ng-repeat="userEmail in userEmails" + ) + td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} + td {{ userEmail.institution }} + td + a( + href + ng-if="!userEmail.default" + ng-click="setDefaultUserEmail(userEmail.email)" + ) Make default + br + a( + href + ng-if="!userEmail.default" + ng-click="removeUserEmail(userEmail.email)" + ) Remove + tr( + ng-if="ui.isLoadingEmails" + ) + td.text-center(colspan="3") + i.fa.fa-fw.fa-spin.fa-refresh + |  Loading... + + tr( + ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails" + ) + td(colspan="3") + a( + href + ng-click="showAddEmailForm()" + ) Add new email + + tr( + ng-if="ui.showAddEmailUI" + ) + td + p + input-suggestions( + ng-model="newAffiliation.email" + ng-model-options="{ allowInvalid: true }" + get-suggestion="getEmailSuggestion(userInput)" + on-blur="handleEmailInputBlur()" + input-id="affilitations-email" + input-name="affilitationsEmail" + input-placeholder="e.g. johndoe@mit.edu" + input-type="email" + input-required="true" + ) + td + p( + ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI" + ) + | {{ newAffiliation.university.name }} ( + a( + href + ng-click="selectUniversityManually();" + ) change + | ) + p( + ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" + ) Start by adding your email address. + p( + ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI" + ) + | Is your email affiliated with an institution? + br + a( + href + ng-click="selectUniversityManually();" + ) Let us know + div( + ng-if="ui.showManualUniversitySelectionUI" + ) + p + ui-select( + ng-model="newAffiliation.country" + ) + ui-select-match( + placeholder="Select your country" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="country in countries | filter: $select.search" + ) + span( + ng-bind="country.name" + ) + p + ui-select( + ng-model="newAffiliation.university" + ng-disabled="!newAffiliation.country" + tagging="addUniversityToSelection" + tagging-label="false" + ) + ui-select-match( + placeholder="Select your university" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="university in universities | filter: $select.search" + ) + span( + ng-bind="university.name" + ) + div( + ng-if="ui.isValidEmail && newAffiliation.university" + ) + p + input.form-control( + type="text" + id="affiliations-role" + placeholder="Role" + ng-model="newAffiliation.role" + ) + p + input.form-control( + type="text" + id="affiliations-department" + placeholder="Department" + ng-model="newAffiliation.department" + ) + td + button.btn.btn-primary( + ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" + ng-click="addNewEmail()" + ) + span( + ng-if="!ui.isAddingNewEmail" + ) Add new email + span( + ng-if="ui.isAddingNewEmail" + ) + i.fa.fa-fw.fa-spin.fa-refresh + |  Adding... + hr + .row .col-md-5 h3 #{translate("update_account_info")} @@ -116,131 +266,7 @@ block content ng-disabled="changePasswordForm.$invalid" ) #{translate("change")} - hr - form( - ng-controller="UserAffiliationsController" - novalidate - name="affiliationsForm" - ) - h3 Emails and affiliations - p Do you use multiple e-mail addresses? If so, add those here. - ul - li You'll retain access to your account even if you lose access to one of your emails - li Others users will be able to find you under different email addresses when sharing projects - li If your e-mail is affiliated with an institution with a group license, your account will be associated with that institution (and you'll gain acess to any benefits the institution may have). - - .affiliations-row - .affiliation-col-4 - label(for="affiliations-email") #{translate("email")} - input-suggestions( - ng-model="newAffiliation.email" - ng-model-options="{ allowInvalid: true }" - get-suggestion="getEmailSuggestion(userInput)" - on-blur="handleEmailInputBlur()" - input-id="affilitations-email" - input-name="affilitationsEmail" - input-placeholder="e.g. johndoe@mit.edu" - input-type="email" - input-required="true" - ) - .affiliation-col-8 - p.affiliation-input-feedback( - ng-if="newAffiliation.university && !showManualUniversitySelectionUI" - ) - | {{ newAffiliation.university.name }} ( - a( - href - ng-click="selectUniversityManually();" - ) change - | ) - p.affiliation-input-feedback( - ng-if="!newAffiliation.university && !isValidEmail && !showManualUniversitySelectionUI" - ) Start by adding your email address. - p.affiliation-input-feedback( - ng-if="!newAffiliation.university && isValidEmail && !isBlacklistedEmail && !showManualUniversitySelectionUI" - ) - | Is your email affiliated with an institution? - a( - href - ng-click="selectUniversityManually();" - ) Let us know - .affiliations-row( - ng-if="isValidEmail && showManualUniversitySelectionUI" - ) - .affiliation-col-4 - label Institution - ui-select( - ng-model="newAffiliation.country" - ) - ui-select-match( - placeholder="Select your country" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="country in countries | filter: $select.search" - ) - span( - ng-bind="country.name" - ) - .affiliation-col-4 - ui-select( - ng-model="newAffiliation.university" - ng-disabled="!newAffiliation.country" - tagging="addUniversityToSelection" - tagging-label="false" - ) - ui-select-match( - placeholder="Select your university" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="university in universities | filter: $select.search" - ) - span( - ng-bind="university.name" - ) - .affiliations-row( - ng-if="isValidEmail && newAffiliation.university" - ) - .affiliation-col-4 - label(for="affiliations-role") Role - input.form-control( - type="text" - id="affiliations-role" - placeholder="e.g. Professor" - ng-model="newAffiliation.role" - ) - .affiliation-col-4 - label(for="affiliations-department") Department - input.form-control( - type="text" - id="affiliations-department" - placeholder="e.g. Mathematics" - ng-model="newAffiliation.department" - ) - .affiliations-row - .affiliation-col-4 - input.btn.btn-primary( - type="submit" - ng-disabled="affiliationsForm.$invalid" - ng-click="handleAffiliationFormSubmit()" - value="Add new email" - ) - - //- p Your current affiliations - - table.table - thead - tr - th Email - th Institution and role - th - tbody - tr( - ng-repeat="userEmail in userEmails" - ) - td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} - td {{ userEmail.institution }} - td Remove - + | !{moduleIncludes("userSettings", locals)} diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 8e7caa6727..7fcfaa6360 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -5,18 +5,9 @@ define [ $scope.userEmails = [] $scope.countries = [] $scope.universities = [] - $scope.newAffiliation = - email: "" - country: null - university: null - role: null - department: null - $scope.showManualUniversitySelectionUI = false - $scope.isValidEmail = false - $scope.isBlacklistedEmail = false LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/ - EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z]+)$/ + EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z0-9_\-\.]+)([^\.])$/ _matchLocalAndDomain = (userEmailInput) -> match = userEmailInput?.match LOCAL_AND_DOMAIN_REGEX @@ -30,14 +21,14 @@ define [ $scope.getEmailSuggestion = (userInput) -> userInputLocalAndDomain = _matchLocalAndDomain(userInput) - $scope.isValidEmail = EMAIL_REGEX.test userInput   - $scope.isBlacklistedEmail = false + $scope.ui.isValidEmail = EMAIL_REGEX.test userInput + $scope.ui.isBlacklistedEmail = false + $scope.ui.showManualUniversitySelectionUI = false if userInputLocalAndDomain.domain? - $scope.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted userInputLocalAndDomain.domain + $scope.ui.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted userInputLocalAndDomain.domain UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain) .then (universityDomain) -> - $scope.showManualUniversitySelectionUI = false if userInputLocalAndDomain.domain == universityDomain.hostname $scope.newAffiliation.university = universityDomain.university $scope.newAffiliation.department = universityDomain.department @@ -61,9 +52,13 @@ define [ $scope.selectUniversityManually = () -> $scope.newAffiliation.university = null $scope.newAffiliation.department = null - $scope.showManualUniversitySelectionUI = true + $scope.ui.showManualUniversitySelectionUI = true - $scope.handleAffiliationFormSubmit = () -> + $scope.showAddEmailForm = () -> + $scope.ui.showAddEmailUI = true + + $scope.addNewEmail = () -> + $scope.ui.isAddingNewEmail = true if !$scope.newAffiliation.university? addEmailPromise = UserAffiliationsDataService .addUserEmail $scope.newAffiliation.email @@ -85,14 +80,45 @@ define [ $scope.newAffiliation.role, $scope.newAffiliation.department ) - addEmailPromise.then () -> getUserEmails() - + addEmailPromise.then () -> + _reset() + _getUserEmails() + + $scope.setDefaultUserEmail = (email) -> + UserAffiliationsDataService + .setDefaultUserEmail email + .then () -> _getUserEmails() + + $scope.removeUserEmail = (email) -> + UserAffiliationsDataService + .removeUserEmail email + .then () -> _getUserEmails() + + _reset = () -> + $scope.newAffiliation = + email: "" + country: null + university: null + role: null + department: null + $scope.ui = + showManualUniversitySelectionUI: false + isLoadingEmails: false + isAddingNewEmail: false + showAddEmailUI: false + isValidEmail: false + isBlacklistedEmail: false + _reset() + # Populates the emails table - getUserEmails = () -> + _getUserEmails = () -> + $scope.ui.isLoadingEmails = true UserAffiliationsDataService .getUserEmails() - .then (emails) -> $scope.userEmails = emails - getUserEmails() + .then (emails) -> + $scope.userEmails = emails + $scope.ui.isLoadingEmails = false + _getUserEmails() # Populates the countries dropdown UserAffiliationsDataService diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index e344909792..563687e3d9 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -72,6 +72,18 @@ define [ _csrf: window.csrfToken } + setDefaultUserEmail = (email) -> + $http.post "/user/emails/default", { + email, + _csrf: window.csrfToken + } + + removeUserEmail = (email) -> + $http.delete "/user/emails/#{ encodeURIComponent(email) }", { + headers: + "X-CSRF-Token": window.csrfToken + } + isDomainBlacklisted = (domain) -> domain.toLowerCase() of domainsBlackList @@ -83,6 +95,8 @@ define [ addUserEmail addUserAffiliationWithUnknownUniversity addUserAffiliation + setDefaultUserEmail + removeUserEmail isDomainBlacklisted } ] \ No newline at end of file diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 95f92aa97a..1cd57d3291 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -11,46 +11,16 @@ } } - -.affiliations-row { - display: flex; - margin-left: -(@grid-gutter-width / 2); - margin-right: -(@grid-gutter-width / 2); - @media (min-width: @screen-sm-min) { - align-items: flex-end; - } +.affiliations-table { + table-layout: fixed; } - .affiliation-col-4, - .affiliation-col-8 { - width: 100%; - flex-grow: 0; - flex-shrink: 0; - padding-left: (@grid-gutter-width / 2); - padding-right: (@grid-gutter-width / 2); - margin-bottom: (@grid-gutter-width / 2); + .affiliations-table-email { + width: 40%; } - - .affiliation-col-4 { - @media (min-width: @screen-sm-min) { - width: (4 / 12) * 100%; - } + .affiliations-table-institution { + width: 40%; } - - .affiliation-col-8 { - @media (min-width: @screen-sm-min) { - width: (8 / 12) * 100%; - } + .affiliations-table-inline-actions { + width: 20%; } - - .affiliation-col-align-right { - @media (min-width: @screen-sm-min) { - display: flex; - justify-content: flex-end; - } - } - - .affiliation-input-feedback { - height: @input-height-base; - padding: @padding-base-vertical; - margin: 0; - } \ No newline at end of file + \ No newline at end of file From 9bc70d479c7f5c3ce2c8952e78dc72f4be048df2 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 22 Jun 2018 11:24:14 +0100 Subject: [PATCH 12/27] Use university-provided list of departments when available. --- services/web/app/views/user/settings.pug | 16 ++++++++++++++++ .../UserAffiliationsController.coffee | 7 ++++++- .../factories/UserAffiliationsDataService.coffee | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 15bab80385..595fb64a29 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -148,7 +148,23 @@ block content id="affiliations-department" placeholder="Department" ng-model="newAffiliation.department" + ng-if="!newAffiliation.university.departments || newAffiliation.university.departments.length === 0" ) + ui-select( + ng-model="newAffiliation.department" + ng-if="newAffiliation.university.departments.length > 0" + tagging + tagging-label="false" + ) + ui-select-match( + placeholder="Department" + ) {{ $select.selected }} + ui-select-choices( + repeat="department in getUniqueUniversityDepartments() | filter: $select.search" + ) + span( + ng-bind="department" + ) td button.btn.btn-primary( ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 7fcfaa6360..c130b6068a 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", ($scope, UserAffiliationsDataService, $q) -> + App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", "_", ($scope, UserAffiliationsDataService, $q, _) -> $scope.userEmails = [] $scope.countries = [] $scope.universities = [] @@ -94,6 +94,9 @@ define [ .removeUserEmail email .then () -> _getUserEmails() + $scope.getUniqueUniversityDepartments = () -> + _.uniq $scope.newAffiliation.university.departments + _reset = () -> $scope.newAffiliation = email: "" @@ -129,6 +132,8 @@ define [ $scope.$watch "newAffiliation.country", (newSelectedCountry, prevSelectedCountry) -> if newSelectedCountry? and newSelectedCountry != prevSelectedCountry $scope.newAffiliation.university = null + $scope.newAffiliation.role = null + $scope.newAffiliation.department = null UserAffiliationsDataService .getUniversitiesFromCountry(newSelectedCountry) .then (universities) -> $scope.universities = universities diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 563687e3d9..48547565c4 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -8,7 +8,7 @@ define [ domainsBlackList = { "overleaf.com" : true } commonTLDs = [ "br", "cn", "co", "co.jp", "co.uk", "com", "com.au", "de","fr", "in", "info", "io", "net", "no", "ru", "se", "us", "com.tw", "com.br", "pl", "it", "co.in", "com.mx" ] - commonDomains = [ "gmail", "googlemail", "icloud", "me", "yahoo", "ymail", "yahoomail", "hotmail", "live", "msn", "outlook", "gmx", "mail", "aol", "163", "mac", "qq", "o2", "libero", "126" ] + commonDomains = [ "gmail", "googlemail", "icloud", "me", "yahoo", "ymail", "yahoomail", "hotmail", "live", "msn", "outlook", "gmx", "mail", "aol", "163", "mac", "qq", "o2", "libero", "126" ] for domain in commonDomains for tld in commonTLDs From a1da24a86d678e6fc1940f1e5ded6e729d1616a5 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 22 Jun 2018 16:07:54 +0100 Subject: [PATCH 13/27] Styling adjustments. --- services/web/app/views/user/settings.pug | 39 ++++++++++--------- .../stylesheets/app/account-settings.less | 11 +++++- .../components/input-suggestions.less | 2 +- .../stylesheets/components/ui-select.less | 9 +++++ .../public/stylesheets/core/ol-variables.less | 2 + 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 595fb64a29..0daff32865 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -46,7 +46,7 @@ block content ng-if="!userEmail.default" ng-click="removeUserEmail(userEmail.email)" ) Remove - tr( + tr.affiliations-table-highlighted-row( ng-if="ui.isLoadingEmails" ) td.text-center(colspan="3") @@ -60,13 +60,13 @@ block content a( href ng-click="showAddEmailForm()" - ) Add new email + ) Add another email - tr( + tr.affiliations-table-highlighted-row( ng-if="ui.showAddEmailUI" ) td - p + .affiliations-form-group input-suggestions( ng-model="newAffiliation.email" ng-model-options="{ allowInvalid: true }" @@ -79,7 +79,7 @@ block content input-required="true" ) td - p( + div( ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI" ) | {{ newAffiliation.university.name }} ( @@ -88,10 +88,10 @@ block content ng-click="selectUniversityManually();" ) change | ) - p( + div( ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" ) Start by adding your email address. - p( + div( ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI" ) | Is your email affiliated with an institution? @@ -102,21 +102,21 @@ block content ) Let us know div( ng-if="ui.showManualUniversitySelectionUI" - ) - p + ss) + .affiliations-form-group ui-select( ng-model="newAffiliation.country" ) ui-select-match( - placeholder="Select your country" + placeholder="Country" ) {{ $select.selected.name }} ui-select-choices( repeat="country in countries | filter: $select.search" ) span( ng-bind="country.name" - ) - p + s) + .affiliations-form-group ui-select( ng-model="newAffiliation.university" ng-disabled="!newAffiliation.country" @@ -124,7 +124,7 @@ block content tagging-label="false" ) ui-select-match( - placeholder="Select your university" + placeholder="Institution" ) {{ $select.selected.name }} ui-select-choices( repeat="university in universities | filter: $select.search" @@ -132,17 +132,18 @@ block content span( ng-bind="university.name" ) - div( - ng-if="ui.isValidEmail && newAffiliation.university" - ) - p + .affiliations-form-group( + ng-if="ui.isValidEmail && newAffiliation.university" + ) input.form-control( type="text" id="affiliations-role" placeholder="Role" ng-model="newAffiliation.role" - ) - p + s) + .affiliations-form-group( + ng-if="ui.isValidEmail && newAffiliation.university" + ) input.form-control( type="text" id="affiliations-department" diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 1cd57d3291..f89d0a5a2b 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -23,4 +23,13 @@ .affiliations-table-inline-actions { width: 20%; } - \ No newline at end of file + .affiliations-table-highlighted-row { + background-color: tint(@content-alt-bg-color, 6%); + } + + .affiliations-form-group { + margin-bottom: @table-cell-padding; + :last-of-type { + margin-bottom: 0; + } + } \ No newline at end of file diff --git a/services/web/public/stylesheets/components/input-suggestions.less b/services/web/public/stylesheets/components/input-suggestions.less index bf2fd4a253..93afe4cfa4 100644 --- a/services/web/public/stylesheets/components/input-suggestions.less +++ b/services/web/public/stylesheets/components/input-suggestions.less @@ -5,7 +5,7 @@ .input-suggestions-main { position: absolute; top: 0; - background-color: transparent; + // background-color: transparent; } .input-suggestions-shadow { diff --git a/services/web/public/stylesheets/components/ui-select.less b/services/web/public/stylesheets/components/ui-select.less index a1ce7ecc8f..5513378629 100644 --- a/services/web/public/stylesheets/components/ui-select.less +++ b/services/web/public/stylesheets/components/ui-select.less @@ -48,4 +48,13 @@ } } } +} + +.ui-select-container[tagging] { + .ui-select-toggle { + cursor: text; + > i.caret.pull-right { + display: none; + } + } } \ No newline at end of file diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 114d07a5b1..9f4eb29c95 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -65,6 +65,8 @@ @btn-info-bg : @ol-blue; @btn-info-border : transparent; +@padding-xs-horizontal : 8px; + // Alerts @alert-padding : 15px; @alert-border-radius : @border-radius-base; From 00dfcf66c6ae3a47779261ccd89949d1913b4a63 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 25 Jun 2018 13:44:43 +0100 Subject: [PATCH 14/27] More styling adjustments. --- services/web/app/views/user/settings.pug | 10 +++++----- .../controllers/UserAffiliationsController.coffee | 3 +++ .../factories/UserAffiliationsDataService.coffee | 1 - .../web/public/stylesheets/app/account-settings.less | 7 +++++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 0daff32865..69401b9569 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -13,7 +13,7 @@ block content .alert.alert-success(ng-show="settingsForm.response.success") | #{translate("thanks_settings_updated")} form-messages(for="changePasswordForm") - .container-fluid + div form.row( ng-controller="UserAffiliationsController" name="affiliationsForm" @@ -79,7 +79,7 @@ block content input-required="true" ) td - div( + .affiliations-table-label( ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI" ) | {{ newAffiliation.university.name }} ( @@ -88,10 +88,10 @@ block content ng-click="selectUniversityManually();" ) change | ) - div( + .affiliations-table-label( ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" ) Start by adding your email address. - div( + .affiliations-table-label( ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI" ) | Is your email affiliated with an institution? @@ -102,7 +102,7 @@ block content ) Let us know div( ng-if="ui.showManualUniversitySelectionUI" - ss) + ) .affiliations-form-group ui-select( ng-model="newAffiliation.country" diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index c130b6068a..b8b867d411 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -85,11 +85,14 @@ define [ _getUserEmails() $scope.setDefaultUserEmail = (email) -> + $scope.ui.isLoadingEmails = true UserAffiliationsDataService .setDefaultUserEmail email .then () -> _getUserEmails() + .catch () -> console.log "fodeu" $scope.removeUserEmail = (email) -> + $scope.ui.isLoadingEmails = true UserAffiliationsDataService .removeUserEmail email .then () -> _getUserEmails() diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 48547565c4..7ee1323f4e 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -74,7 +74,6 @@ define [ setDefaultUserEmail = (email) -> $http.post "/user/emails/default", { - email, _csrf: window.csrfToken } diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index f89d0a5a2b..424ab84995 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -29,7 +29,10 @@ .affiliations-form-group { margin-bottom: @table-cell-padding; - :last-of-type { + &:last-of-type { margin-bottom: 0; } - } \ No newline at end of file + } + .affiliations-table-label { + padding-top: 4px; + } From 8ac6f4d6f40fe19e121f0c4c402e4b1ef4369fb1 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 25 Jun 2018 16:13:43 +0100 Subject: [PATCH 15/27] Use v1 proxy; even more style adjustments. --- services/web/app/views/user/settings.pug | 128 +++++++++--------- .../coffee/components/inputSuggestions.coffee | 4 +- .../UserAffiliationsController.coffee | 2 +- .../UserAffiliationsDataService.coffee | 4 +- .../stylesheets/app/account-settings.less | 6 +- .../components/input-suggestions.less | 2 +- 6 files changed, 75 insertions(+), 71 deletions(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 69401b9569..5efcee86e6 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -21,7 +21,6 @@ block content .col-md-12 h3 Emails and Affiliations p.small Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account. - table.table.affiliations-table thead tr @@ -33,7 +32,11 @@ block content ng-repeat="userEmail in userEmails" ) td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} - td {{ userEmail.institution }} + td + div(ng-if="userEmail.affiliation.institution") {{ userEmail.affiliation.institution.name }} + div(ng-if="userEmail.affiliation.role || userEmail.affiliation.department") + span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role + ", " }} + span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} td a( href @@ -53,7 +56,7 @@ block content i.fa.fa-fw.fa-spin.fa-refresh |  Loading... - tr( + tr.affiliations-table-highlighted-row( ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails" ) td(colspan="3") @@ -100,72 +103,73 @@ block content href ng-click="selectUniversityManually();" ) Let us know - div( + .affiliations-form-group( ng-if="ui.showManualUniversitySelectionUI" ) - .affiliations-form-group - ui-select( - ng-model="newAffiliation.country" - ) - ui-select-match( - placeholder="Country" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="country in countries | filter: $select.search" - ) - span( - ng-bind="country.name" - s) - .affiliations-form-group - ui-select( - ng-model="newAffiliation.university" - ng-disabled="!newAffiliation.country" - tagging="addUniversityToSelection" - tagging-label="false" - ) - ui-select-match( - placeholder="Institution" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="university in universities | filter: $select.search" - ) - span( - ng-bind="university.name" - ) - .affiliations-form-group( - ng-if="ui.isValidEmail && newAffiliation.university" + ui-select( + ng-model="newAffiliation.country" ) - input.form-control( - type="text" - id="affiliations-role" - placeholder="Role" - ng-model="newAffiliation.role" - s) - .affiliations-form-group( - ng-if="ui.isValidEmail && newAffiliation.university" + ui-select-match( + placeholder="Country" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="country in countries | filter: $select.search" + ) + span( + ng-bind="country.name" + s) + .affiliations-form-group( + ng-if="ui.showManualUniversitySelectionUI" + ) + ui-select( + ng-model="newAffiliation.university" + ng-disabled="!newAffiliation.country" + tagging="addUniversityToSelection" + tagging-label="false" ) - input.form-control( - type="text" - id="affiliations-department" + ui-select-match( + placeholder="Institution" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="university in universities | filter: $select.search" + ) + span( + ng-bind="university.name" + ) + .affiliations-form-group( + ng-if="ui.isValidEmail && newAffiliation.university" + ) + input.form-control( + type="text" + id="affiliations-role" + placeholder="Role" + ng-model="newAffiliation.role" + ) + .affiliations-form-group( + ng-if="ui.isValidEmail && newAffiliation.university" + ) + input.form-control( + type="text" + id="affiliations-department" + placeholder="Department" + ng-model="newAffiliation.department" + ng-if="!newAffiliation.university.departments || newAffiliation.university.departments.length === 0" + ) + ui-select( + ng-model="newAffiliation.department" + ng-if="newAffiliation.university.departments.length > 0" + tagging + tagging-label="false" + ) + ui-select-match( placeholder="Department" - ng-model="newAffiliation.department" - ng-if="!newAffiliation.university.departments || newAffiliation.university.departments.length === 0" + ) {{ $select.selected }} + ui-select-choices( + repeat="department in getUniqueUniversityDepartments() | filter: $select.search" ) - ui-select( - ng-model="newAffiliation.department" - ng-if="newAffiliation.university.departments.length > 0" - tagging - tagging-label="false" - ) - ui-select-match( - placeholder="Department" - ) {{ $select.selected }} - ui-select-choices( - repeat="department in getUniqueUniversityDepartments() | filter: $select.search" + span( + ng-bind="department" ) - span( - ng-bind="department" - ) td button.btn.btn-primary( ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" diff --git a/services/web/public/coffee/components/inputSuggestions.coffee b/services/web/public/coffee/components/inputSuggestions.coffee index c9907fe26e..b48df8391c 100644 --- a/services/web/public/coffee/components/inputSuggestions.coffee +++ b/services/web/public/coffee/components/inputSuggestions.coffee @@ -45,8 +45,8 @@ define [ controller: inputSuggestionsController template: """
    -
    - +
    +
    diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index b8b867d411..d212a44bdf 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -28,7 +28,7 @@ define [ $scope.ui.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted userInputLocalAndDomain.domain UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain) - .then (universityDomain) -> + .then (universityDomain) -> if userInputLocalAndDomain.domain == universityDomain.hostname $scope.newAffiliation.university = universityDomain.university $scope.newAffiliation.department = universityDomain.department diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 7ee1323f4e..e51eaed703 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -28,7 +28,7 @@ define [ universitiesFromCountry = universities[country.code] else universitiesFromCountry = $http - .get "http://www.overleaf.test:5000/universities/list.json" , { params: { country_code: country.code } } + .get "/institutions/list" , { params: { country_code: country.code } } .then (response) -> universities[country.code] = response.data $q.resolve(universitiesFromCountry) @@ -36,7 +36,7 @@ define [ if universitiesByDomain[partialDomainInput]? $q.resolve universitiesByDomain[partialDomainInput] else - $http.get "http://www.overleaf.test:5000/university/domains.json" , { params: { hostname: partialDomainInput, limit: 1 } } + $http.get "/institutions/domains" , { params: { hostname: partialDomainInput, limit: 1 } } .then (response) -> university = response.data[0] if university? diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 424ab84995..bf399b316c 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -28,9 +28,9 @@ } .affiliations-form-group { - margin-bottom: @table-cell-padding; - &:last-of-type { - margin-bottom: 0; + margin-top: @table-cell-padding; + &:first-child { + margin-top: 0; } } .affiliations-table-label { diff --git a/services/web/public/stylesheets/components/input-suggestions.less b/services/web/public/stylesheets/components/input-suggestions.less index 93afe4cfa4..bf2fd4a253 100644 --- a/services/web/public/stylesheets/components/input-suggestions.less +++ b/services/web/public/stylesheets/components/input-suggestions.less @@ -5,7 +5,7 @@ .input-suggestions-main { position: absolute; top: 0; - // background-color: transparent; + background-color: transparent; } .input-suggestions-shadow { From e7f819fbddaf07cb56fe7c02f684f26014da9273 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 25 Jun 2018 17:06:23 +0100 Subject: [PATCH 16/27] Hide affiliations UI behind feature flag. --- .../Features/User/UserPagesController.coffee | 1 + services/web/app/views/user/settings.pug | 212 +++++++++--------- .../stylesheets/app/account-settings.less | 3 + 3 files changed, 110 insertions(+), 106 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 5e6ea7d62b..2a7ed62d02 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -68,6 +68,7 @@ module.exports = shouldAllowEditingDetails: shouldAllowEditingDetails languages: Settings.languages, accountSettingsTabActive: true + showAffiliationsUI: (req.query?.aff == "true") or false sessionsPage: (req, res, next) -> user = AuthenticationController.getSessionUser(req) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 5efcee86e6..05d0dd5183 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -9,11 +9,7 @@ block content .page-header h1 #{translate("account_settings")} .account-settings(ng-controller="AccountSettingsController", ng-cloak) - form-messages(for="settingsForm") - .alert.alert-success(ng-show="settingsForm.response.success") - | #{translate("thanks_settings_updated")} - form-messages(for="changePasswordForm") - div + if locals.showAffiliationsUI form.row( ng-controller="UserAffiliationsController" name="affiliationsForm" @@ -184,110 +180,114 @@ block content i.fa.fa-fw.fa-spin.fa-refresh |  Adding... hr + form-messages(for="settingsForm") + .alert.alert-success(ng-show="settingsForm.response.success") + | #{translate("thanks_settings_updated")} + form-messages(for="changePasswordForm") - .row - .col-md-5 - h3 #{translate("update_account_info")} - form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) + .row + .col-md-5 + h3 #{translate("update_account_info")} + form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) + input(type="hidden", name="_csrf", value=csrfToken) + if !externalAuthenticationSystemUsed() + .form-group + label(for='email') #{translate("email")} + input.form-control( + type='email', + name='email', + placeholder="email@example.com" + required, + ng-model="email", + ng-init="email = "+JSON.stringify(user.email), + ng-model-options="{ updateOn: 'blur' }" + ) + span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty") + | #{translate("must_be_email_address")} + else + // show the email, non-editable + .form-group + label.control-label #{translate("email")} + div.form-control(readonly="true") #{user.email} + + if shouldAllowEditingDetails + .form-group + label(for='firstName').control-label #{translate("first_name")} + input.form-control( + type='text', + name='first_name', + value=user.first_name + ng-non-bindable + ) + .form-group + label(for='lastName').control-label #{translate("last_name")} + input.form-control( + type='text', + name='last_name', + value=user.last_name + ng-non-bindable + ) + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="settingsForm.$invalid" + ) #{translate("update")} + else + .form-group + label.control-label #{translate("first_name")} + div.form-control(readonly="true") #{user.first_name} + .form-group + label.control-label #{translate("last_name")} + div.form-control(readonly="true") #{user.last_name} + + if !externalAuthenticationSystemUsed() + .col-md-5.col-md-offset-1 + h3 #{translate("change_password")} + form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate) input(type="hidden", name="_csrf", value=csrfToken) - if !externalAuthenticationSystemUsed() - .form-group - label(for='email') #{translate("email")} - input.form-control( - type='email', - name='email', - placeholder="email@example.com" - required, - ng-model="email", - ng-init="email = "+JSON.stringify(user.email), - ng-model-options="{ updateOn: 'blur' }" - ) - span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty") - | #{translate("must_be_email_address")} - else - // show the email, non-editable - .form-group - label.control-label #{translate("email")} - div.form-control(readonly="true") #{user.email} + .form-group + label(for='currentPassword') #{translate("current_password")} + input.form-control( + type='password', + name='currentPassword', + placeholder='*********', + ng-model="currentPassword", + required + ) + span.small.text-primary(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty") + | #{translate("required")} + .form-group + label(for='newPassword1') #{translate("new_password")} + input.form-control( + id='passwordField', + type='password', + name='newPassword1', + placeholder='*********', + ng-model="newPassword1", + required, + complex-password + ) + span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage") + .form-group + label(for='newPassword2') #{translate("confirm_new_password")} + input.form-control( + type='password', + name='newPassword2', + placeholder='*********', + ng-model="newPassword2", + equals="passwordField" + ) + span.small.text-primary(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty") + | #{translate("doesnt_match")} + span.small.text-primary(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty") + | #{translate("invalid_password")} + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="changePasswordForm.$invalid" + ) #{translate("change")} - if shouldAllowEditingDetails - .form-group - label(for='firstName').control-label #{translate("first_name")} - input.form-control( - type='text', - name='first_name', - value=user.first_name - ng-non-bindable - ) - .form-group - label(for='lastName').control-label #{translate("last_name")} - input.form-control( - type='text', - name='last_name', - value=user.last_name - ng-non-bindable - ) - .actions - button.btn.btn-primary( - type='submit', - ng-disabled="settingsForm.$invalid" - ) #{translate("update")} - else - .form-group - label.control-label #{translate("first_name")} - div.form-control(readonly="true") #{user.first_name} - .form-group - label.control-label #{translate("last_name")} - div.form-control(readonly="true") #{user.last_name} - - if !externalAuthenticationSystemUsed() - .col-md-5.col-md-offset-1 - h3 #{translate("change_password")} - form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate) - input(type="hidden", name="_csrf", value=csrfToken) - .form-group - label(for='currentPassword') #{translate("current_password")} - input.form-control( - type='password', - name='currentPassword', - placeholder='*********', - ng-model="currentPassword", - required - ) - span.small.text-primary(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty") - | #{translate("required")} - .form-group - label(for='newPassword1') #{translate("new_password")} - input.form-control( - id='passwordField', - type='password', - name='newPassword1', - placeholder='*********', - ng-model="newPassword1", - required, - complex-password - ) - span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage") - .form-group - label(for='newPassword2') #{translate("confirm_new_password")} - input.form-control( - type='password', - name='newPassword2', - placeholder='*********', - ng-model="newPassword2", - equals="passwordField" - ) - span.small.text-primary(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty") - | #{translate("doesnt_match")} - span.small.text-primary(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty") - | #{translate("invalid_password")} - .actions - button.btn.btn-primary( - type='submit', - ng-disabled="changePasswordForm.$invalid" - ) #{translate("change")} - - + | !{moduleIncludes("userSettings", locals)} diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index bf399b316c..f8663f7b5f 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -2,6 +2,9 @@ .alert { margin-bottom: 0; } + h3 { + margin-top: 0; + } } #delete-account-modal { From 70b3df5a0eb4775fdbd078733534e3ddfdfc0127 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 25 Jun 2018 17:32:39 +0100 Subject: [PATCH 17/27] Remove logs. --- .../web/app/coffee/Features/User/UserEmailsController.coffee | 1 - .../affiliations/controllers/UserAffiliationsController.coffee | 1 - 2 files changed, 2 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index 3c574d6088..bc53af4098 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -33,7 +33,6 @@ module.exports = UserEmailsController = remove: (req, res, next) -> userId = AuthenticationController.getLoggedInUserId(req) - logger.log req.params email = EmailHelper.parseEmail(req.params.email) return res.sendStatus 422 unless email? diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index d212a44bdf..bc3890081a 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -89,7 +89,6 @@ define [ UserAffiliationsDataService .setDefaultUserEmail email .then () -> _getUserEmails() - .catch () -> console.log "fodeu" $scope.removeUserEmail = (email) -> $scope.ui.isLoadingEmails = true From 76d8ab11e559b525cacd62f8d8883d4e40a38050 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 26 Jun 2018 16:18:53 +0100 Subject: [PATCH 18/27] Make sure we infer the university when the user finishes typing his email. --- services/web/public/coffee/components/inputSuggestions.coffee | 2 +- .../affiliations/controllers/UserAffiliationsController.coffee | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/components/inputSuggestions.coffee b/services/web/public/coffee/components/inputSuggestions.coffee index b48df8391c..a7f36a22a3 100644 --- a/services/web/public/coffee/components/inputSuggestions.coffee +++ b/services/web/public/coffee/components/inputSuggestions.coffee @@ -45,7 +45,7 @@ define [ controller: inputSuggestionsController template: """
    -
    +
    diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index bc3890081a..d503fb850f 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -29,7 +29,8 @@ define [ UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain) .then (universityDomain) -> - if userInputLocalAndDomain.domain == universityDomain.hostname + currentUserInputLocalAndDomain = _matchLocalAndDomain $scope.newAffiliation.email + if currentUserInputLocalAndDomain.domain == universityDomain.hostname $scope.newAffiliation.university = universityDomain.university $scope.newAffiliation.department = universityDomain.department else From 3e0ad478731e590ba41ff54841957483092ba1fd Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 26 Jun 2018 16:50:55 +0100 Subject: [PATCH 19/27] Avoid suggesting blacklisted emails as university domains. --- .../affiliations/controllers/UserAffiliationsController.coffee | 3 +-- .../affiliations/factories/UserAffiliationsDataService.coffee | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index d503fb850f..9a4595491f 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -26,7 +26,6 @@ define [ $scope.ui.showManualUniversitySelectionUI = false if userInputLocalAndDomain.domain? $scope.ui.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted userInputLocalAndDomain.domain - UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain) .then (universityDomain) -> currentUserInputLocalAndDomain = _matchLocalAndDomain $scope.newAffiliation.email @@ -44,7 +43,7 @@ define [ else $scope.newAffiliation.university = null $scope.newAffiliation.department = null - $q.resolve null + $q.reject null $scope.handleEmailInputBlur = () -> # if $scope.newAffiliation.autoDetectMode and !$scope.newAffiliation.university and $scope.newAffiliation.email?.match EMAIL_REGEX diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index e51eaed703..6bc5e8eb9f 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -39,7 +39,7 @@ define [ $http.get "/institutions/domains" , { params: { hostname: partialDomainInput, limit: 1 } } .then (response) -> university = response.data[0] - if university? + if university? and !isDomainBlacklisted university.hostname universitiesByDomain[university.hostname] = university $q.resolve university else From 60009c63469dfb5cc1ed8dbcd40a1ca4ca723941 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Tue, 26 Jun 2018 19:50:12 +0200 Subject: [PATCH 20/27] fix delete endpoint --- .../app/coffee/Features/User/UserEmailsController.coffee | 2 +- services/web/app/coffee/router.coffee | 2 +- .../factories/UserAffiliationsDataService.coffee | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index bc53af4098..a6335521d6 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -33,7 +33,7 @@ module.exports = UserEmailsController = remove: (req, res, next) -> userId = AuthenticationController.getLoggedInUserId(req) - email = EmailHelper.parseEmail(req.params.email) + email = EmailHelper.parseEmail(req.body.email) return res.sendStatus 422 unless email? UserUpdater.removeEmailAddress userId, email, (error)-> diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 4e7c3d9ddb..8993bb45dc 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -114,7 +114,7 @@ module.exports = class Router webRouter.post '/user/emails', AuthenticationController.requireLogin(), UserEmailsController.add - webRouter.delete '/user/emails/:email', + webRouter.post '/user/emails/delete', AuthenticationController.requireLogin(), UserEmailsController.remove webRouter.post '/user/emails/default', diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 6bc5e8eb9f..f89c6410e1 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -78,9 +78,9 @@ define [ } removeUserEmail = (email) -> - $http.delete "/user/emails/#{ encodeURIComponent(email) }", { - headers: - "X-CSRF-Token": window.csrfToken + $http.post "/user/emails/delete", { + email, + _csrf: window.csrfToken } isDomainBlacklisted = (domain) -> From bebbc433bfb3fee2839ca646301c80d757d31586 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Wed, 27 Jun 2018 09:58:54 +0200 Subject: [PATCH 21/27] proxy static institutions URLs in dev env --- services/web/config/settings.defaults.coffee | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 7078a9f64c..9d471e6642 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -15,6 +15,11 @@ httpAuthUsers[httpAuthUser] = httpAuthPass sessionSecret = "secret-please-change" +v1Api = + url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000" + user: 'overleaf' + pass: 'password' + module.exports = settings = allowAnonymousReadAndWriteSharing: @@ -157,9 +162,9 @@ module.exports = settings = thirdpartyreferences: url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046" v1: - url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000" - user: 'overleaf' - pass: 'password' + url: v1Api.url + user: v1Api.user + pass: v1Api.pass templates: user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2" @@ -420,7 +425,9 @@ module.exports = settings = redirects: "/templates/index": "/templates/" - proxyUrls: {} + proxyUrls: + '/institutions/list': { baseUrl: v1Api.url, path: '/universities/list' } + '/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' } reloadModuleViewsOnEachRequest: true From 522084a504bae98a0002694b11797a984f72c777 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 27 Jun 2018 12:21:26 +0100 Subject: [PATCH 22/27] Add Keys enum; use it when detecting keystrokes in the input suggestions component. --- .../coffee/components/inputSuggestions.coffee | 4 ++-- services/web/public/coffee/main.coffee | 1 + services/web/public/coffee/main/keys.coffee | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 services/web/public/coffee/main/keys.coffee diff --git a/services/web/public/coffee/components/inputSuggestions.coffee b/services/web/public/coffee/components/inputSuggestions.coffee index a7f36a22a3..69b414264a 100644 --- a/services/web/public/coffee/components/inputSuggestions.coffee +++ b/services/web/public/coffee/components/inputSuggestions.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - inputSuggestionsController = ($scope, $element, $attrs) -> + inputSuggestionsController = ($scope, $element, $attrs, Keys) -> ctrl = @ ctrl.showHint = false ctrl.hasFocus = false @@ -14,7 +14,7 @@ define [ ctrl.suggestion = null ctrl.onBlur() ctrl.handleKeyDown = ($event) -> - if ($event.which == 9 or $event.which == 13) and ctrl.suggestion? and ctrl.suggestion != "" + if ($event.which == Keys.TAB or $event.which == Keys.ENTER) and ctrl.suggestion? and ctrl.suggestion != "" $event.preventDefault() ctrl.localNgModel += ctrl.suggestion ctrl.suggestion = null diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index a52a490491..80e637d705 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -22,6 +22,7 @@ define [ "main/learn" "main/affiliations/controllers/UserAffiliationsController" "main/affiliations/factories/UserAffiliationsDataService" + "main/keys" "analytics/AbTestingManager" "directives/asyncForm" "directives/stopPropagation" diff --git a/services/web/public/coffee/main/keys.coffee b/services/web/public/coffee/main/keys.coffee new file mode 100644 index 0000000000..987c41fd1d --- /dev/null +++ b/services/web/public/coffee/main/keys.coffee @@ -0,0 +1,16 @@ +define [ + "base" +], (App) -> + App.constant "Keys", + ENTER : 13 + TAB : 9 + ESCAPE : 27 + SPACE : 32 + BACKSPACE : 8 + UP : 38 + DOWN : 40 + LEFT : 37 + RIGHT : 39 + PERIOD : 190 + COMMA : 188 + END : 35 From 82392978a7906a380cf8726dbc2faca165f85888 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 27 Jun 2018 13:47:49 +0100 Subject: [PATCH 23/27] Improve input suggestions template readability. --- .../coffee/components/inputSuggestions.coffee | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/services/web/public/coffee/components/inputSuggestions.coffee b/services/web/public/coffee/components/inputSuggestions.coffee index 69b414264a..9153fef602 100644 --- a/services/web/public/coffee/components/inputSuggestions.coffee +++ b/services/web/public/coffee/components/inputSuggestions.coffee @@ -43,12 +43,32 @@ define [ inputType: "@?" inputRequired: "=?" controller: inputSuggestionsController - template: """ -
    -
    - -
    - -
    - """ + template: [ + '
    ', + '
    ', + '', + '', + '', + '', + '
    ', + '', + '
    ' + ].join "" + } From d1467784bb9f926c011a21d41e67194af9cfa902 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 27 Jun 2018 14:21:03 +0100 Subject: [PATCH 24/27] Avoid showing a comma when the user only has department info. --- services/web/app/views/user/settings.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 05d0dd5183..39f69a4f78 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -31,7 +31,8 @@ block content td div(ng-if="userEmail.affiliation.institution") {{ userEmail.affiliation.institution.name }} div(ng-if="userEmail.affiliation.role || userEmail.affiliation.department") - span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role + ", " }} + span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }} + span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") , span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} td a( From 270c67df5fa3944dcaa2efbccaded2e79e9ddfda Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 27 Jun 2018 17:08:25 +0100 Subject: [PATCH 25/27] Move affiliations form and table into specific file. --- services/web/app/views/user/settings.pug | 173 +----------------- .../views/user/settings/user-affiliations.pug | 171 +++++++++++++++++ 2 files changed, 173 insertions(+), 171 deletions(-) create mode 100644 services/web/app/views/user/settings/user-affiliations.pug diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 39f69a4f78..a781cd708a 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -10,177 +10,8 @@ block content h1 #{translate("account_settings")} .account-settings(ng-controller="AccountSettingsController", ng-cloak) if locals.showAffiliationsUI - form.row( - ng-controller="UserAffiliationsController" - name="affiliationsForm" - ) - .col-md-12 - h3 Emails and Affiliations - p.small Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account. - table.table.affiliations-table - thead - tr - th.affiliations-table-email Email - th.affiliations-table-institution Institution and role - th.affiliations-table-inline-actions - tbody - tr( - ng-repeat="userEmail in userEmails" - ) - td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} - td - div(ng-if="userEmail.affiliation.institution") {{ userEmail.affiliation.institution.name }} - div(ng-if="userEmail.affiliation.role || userEmail.affiliation.department") - span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }} - span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") , - span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} - td - a( - href - ng-if="!userEmail.default" - ng-click="setDefaultUserEmail(userEmail.email)" - ) Make default - br - a( - href - ng-if="!userEmail.default" - ng-click="removeUserEmail(userEmail.email)" - ) Remove - tr.affiliations-table-highlighted-row( - ng-if="ui.isLoadingEmails" - ) - td.text-center(colspan="3") - i.fa.fa-fw.fa-spin.fa-refresh - |  Loading... - - tr.affiliations-table-highlighted-row( - ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails" - ) - td(colspan="3") - a( - href - ng-click="showAddEmailForm()" - ) Add another email - - tr.affiliations-table-highlighted-row( - ng-if="ui.showAddEmailUI" - ) - td - .affiliations-form-group - input-suggestions( - ng-model="newAffiliation.email" - ng-model-options="{ allowInvalid: true }" - get-suggestion="getEmailSuggestion(userInput)" - on-blur="handleEmailInputBlur()" - input-id="affilitations-email" - input-name="affilitationsEmail" - input-placeholder="e.g. johndoe@mit.edu" - input-type="email" - input-required="true" - ) - td - .affiliations-table-label( - ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI" - ) - | {{ newAffiliation.university.name }} ( - a( - href - ng-click="selectUniversityManually();" - ) change - | ) - .affiliations-table-label( - ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" - ) Start by adding your email address. - .affiliations-table-label( - ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI" - ) - | Is your email affiliated with an institution? - br - a( - href - ng-click="selectUniversityManually();" - ) Let us know - .affiliations-form-group( - ng-if="ui.showManualUniversitySelectionUI" - ) - ui-select( - ng-model="newAffiliation.country" - ) - ui-select-match( - placeholder="Country" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="country in countries | filter: $select.search" - ) - span( - ng-bind="country.name" - s) - .affiliations-form-group( - ng-if="ui.showManualUniversitySelectionUI" - ) - ui-select( - ng-model="newAffiliation.university" - ng-disabled="!newAffiliation.country" - tagging="addUniversityToSelection" - tagging-label="false" - ) - ui-select-match( - placeholder="Institution" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="university in universities | filter: $select.search" - ) - span( - ng-bind="university.name" - ) - .affiliations-form-group( - ng-if="ui.isValidEmail && newAffiliation.university" - ) - input.form-control( - type="text" - id="affiliations-role" - placeholder="Role" - ng-model="newAffiliation.role" - ) - .affiliations-form-group( - ng-if="ui.isValidEmail && newAffiliation.university" - ) - input.form-control( - type="text" - id="affiliations-department" - placeholder="Department" - ng-model="newAffiliation.department" - ng-if="!newAffiliation.university.departments || newAffiliation.university.departments.length === 0" - ) - ui-select( - ng-model="newAffiliation.department" - ng-if="newAffiliation.university.departments.length > 0" - tagging - tagging-label="false" - ) - ui-select-match( - placeholder="Department" - ) {{ $select.selected }} - ui-select-choices( - repeat="department in getUniqueUniversityDepartments() | filter: $select.search" - ) - span( - ng-bind="department" - ) - td - button.btn.btn-primary( - ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" - ng-click="addNewEmail()" - ) - span( - ng-if="!ui.isAddingNewEmail" - ) Add new email - span( - ng-if="ui.isAddingNewEmail" - ) - i.fa.fa-fw.fa-spin.fa-refresh - |  Adding... - hr + include settings/user-affiliations + form-messages(for="settingsForm") .alert.alert-success(ng-show="settingsForm.response.success") | #{translate("thanks_settings_updated")} diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug new file mode 100644 index 0000000000..8d0413cba1 --- /dev/null +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -0,0 +1,171 @@ +form.row( + ng-controller="UserAffiliationsController" + name="affiliationsForm" +) + .col-md-12 + h3 Emails and Affiliations + p.small Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account. + table.table.affiliations-table + thead + tr + th.affiliations-table-email Email + th.affiliations-table-institution Institution and role + th.affiliations-table-inline-actions + tbody + tr( + ng-repeat="userEmail in userEmails" + ) + td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} + td + div(ng-if="userEmail.affiliation.institution") {{ userEmail.affiliation.institution.name }} + div(ng-if="userEmail.affiliation.role || userEmail.affiliation.department") + span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }} + span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") , + span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} + td + a( + href + ng-if="!userEmail.default" + ng-click="setDefaultUserEmail(userEmail.email)" + ) Make default + br + a( + href + ng-if="!userEmail.default" + ng-click="removeUserEmail(userEmail.email)" + ) Remove + tr.affiliations-table-highlighted-row( + ng-if="ui.isLoadingEmails" + ) + td.text-center(colspan="3") + i.fa.fa-fw.fa-spin.fa-refresh + |  Loading... + + tr.affiliations-table-highlighted-row( + ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails" + ) + td(colspan="3") + a( + href + ng-click="showAddEmailForm()" + ) Add another email + + tr.affiliations-table-highlighted-row( + ng-if="ui.showAddEmailUI" + ) + td + .affiliations-form-group + input-suggestions( + ng-model="newAffiliation.email" + ng-model-options="{ allowInvalid: true }" + get-suggestion="getEmailSuggestion(userInput)" + on-blur="handleEmailInputBlur()" + input-id="affilitations-email" + input-name="affilitationsEmail" + input-placeholder="e.g. johndoe@mit.edu" + input-type="email" + input-required="true" + ) + td + .affiliations-table-label( + ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI" + ) + | {{ newAffiliation.university.name }} ( + a( + href + ng-click="selectUniversityManually();" + ) change + | ) + .affiliations-table-label( + ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" + ) Start by adding your email address. + .affiliations-table-label( + ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI" + ) + | Is your email affiliated with an institution? + br + a( + href + ng-click="selectUniversityManually();" + ) Let us know + .affiliations-form-group( + ng-if="ui.showManualUniversitySelectionUI" + ) + ui-select( + ng-model="newAffiliation.country" + ) + ui-select-match( + placeholder="Country" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="country in countries | filter: $select.search" + ) + span( + ng-bind="country.name" + s) + .affiliations-form-group( + ng-if="ui.showManualUniversitySelectionUI" + ) + ui-select( + ng-model="newAffiliation.university" + ng-disabled="!newAffiliation.country" + tagging="addUniversityToSelection" + tagging-label="false" + ) + ui-select-match( + placeholder="Institution" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="university in universities | filter: $select.search" + ) + span( + ng-bind="university.name" + ) + .affiliations-form-group( + ng-if="ui.isValidEmail && newAffiliation.university" + ) + input.form-control( + type="text" + id="affiliations-role" + placeholder="Role" + ng-model="newAffiliation.role" + ) + .affiliations-form-group( + ng-if="ui.isValidEmail && newAffiliation.university" + ) + input.form-control( + type="text" + id="affiliations-department" + placeholder="Department" + ng-model="newAffiliation.department" + ng-if="!newAffiliation.university.departments || newAffiliation.university.departments.length === 0" + ) + ui-select( + ng-model="newAffiliation.department" + ng-if="newAffiliation.university.departments.length > 0" + tagging + tagging-label="false" + ) + ui-select-match( + placeholder="Department" + ) {{ $select.selected }} + ui-select-choices( + repeat="department in getUniqueUniversityDepartments() | filter: $select.search" + ) + span( + ng-bind="department" + ) + td + button.btn.btn-primary( + ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" + ng-click="addNewEmail()" + ) + span( + ng-if="!ui.isAddingNewEmail" + ) Add new email + span( + ng-if="ui.isAddingNewEmail" + ) + i.fa.fa-fw.fa-spin.fa-refresh + |  Adding... + hr \ No newline at end of file From d021cd8e3f2ddddb2e921eca9a0c4ae30192e67d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 28 Jun 2018 16:37:36 +0100 Subject: [PATCH 26/27] Add role hints and deparment hints (when not provided by the uni). --- .../views/user/settings/user-affiliations.pug | 27 ++++++++------- .../UserAffiliationsController.coffee | 34 ++++++++++++++++--- .../UserAffiliationsDataService.coffee | 14 ++++++-- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 8d0413cba1..6b08b09346 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -124,25 +124,26 @@ form.row( .affiliations-form-group( ng-if="ui.isValidEmail && newAffiliation.university" ) - input.form-control( - type="text" - id="affiliations-role" - placeholder="Role" + ui-select( ng-model="newAffiliation.role" + tagging + tagging-label="false" ) + ui-select-match( + placeholder="Role" + ) {{ $select.selected }} + ui-select-choices( + repeat="role in roles | filter: $select.search" + ) + span( + ng-bind="role" + ) + .affiliations-form-group( ng-if="ui.isValidEmail && newAffiliation.university" ) - input.form-control( - type="text" - id="affiliations-department" - placeholder="Department" - ng-model="newAffiliation.department" - ng-if="!newAffiliation.university.departments || newAffiliation.university.departments.length === 0" - ) ui-select( ng-model="newAffiliation.department" - ng-if="newAffiliation.university.departments.length > 0" tagging tagging-label="false" ) @@ -150,7 +151,7 @@ form.row( placeholder="Department" ) {{ $select.selected }} ui-select-choices( - repeat="department in getUniqueUniversityDepartments() | filter: $select.search" + repeat="department in departments | filter: $select.search" ) span( ng-bind="department" diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 9a4595491f..a8cf710afc 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -5,6 +5,10 @@ define [ $scope.userEmails = [] $scope.countries = [] $scope.universities = [] + $scope.roles = [] + $scope.departments = [] + + _defaultDepartments = [] LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/ EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z0-9_\-\.]+)([^\.])$/ @@ -45,9 +49,6 @@ define [ $scope.newAffiliation.department = null $q.reject null - $scope.handleEmailInputBlur = () -> - # if $scope.newAffiliation.autoDetectMode and !$scope.newAffiliation.university and $scope.newAffiliation.email?.match EMAIL_REGEX - # $scope.newAffiliation.autoDetectMode = false $scope.selectUniversityManually = () -> $scope.newAffiliation.university = null @@ -96,8 +97,11 @@ define [ .removeUserEmail email .then () -> _getUserEmails() - $scope.getUniqueUniversityDepartments = () -> - _.uniq $scope.newAffiliation.university.departments + $scope.getDepartments = () -> + if $scope.newAffiliation.university?.departments.length > 0 + _.uniq $scope.newAffiliation.university.departments + else + UserAffiliationsDataService.getDefaultDepartmentHints() _reset = () -> $scope.newAffiliation = @@ -130,6 +134,17 @@ define [ .getCountries() .then (countries) -> $scope.countries = countries + # Populates the roles dropdown + UserAffiliationsDataService + .getDefaultRoleHints() + .then (roles) -> $scope.roles = roles + + # Fetches the default department hints + UserAffiliationsDataService + .getDefaultDepartmentHints() + .then (departments) -> + _defaultDepartments = departments + # Populates the universities dropdown (after selecting a country) $scope.$watch "newAffiliation.country", (newSelectedCountry, prevSelectedCountry) -> if newSelectedCountry? and newSelectedCountry != prevSelectedCountry @@ -139,4 +154,13 @@ define [ UserAffiliationsDataService .getUniversitiesFromCountry(newSelectedCountry) .then (universities) -> $scope.universities = universities + + # Populates the departments dropdown (after selecting a university) + $scope.$watch "newAffiliation.university", (newSelectedUniversity, prevSelectedUniversity) -> + if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity + if newSelectedUniversity.departments?.length > 0 + $scope.departments = _.uniq newSelectedUniversity.departments + else + $scope.departments = _defaultDepartments + ] \ No newline at end of file diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index f89c6410e1..ba558eb595 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -1,10 +1,12 @@ define [ "base" ], (App) -> - # TODO Get actual countries list from the DB countriesList = [{ code : "af", name: "Afghanistan" }, { code : "ax", name: "Åland Islands" }, { code : "al", name: "Albania" }, { code : "dz", name: "Algeria" }, { code : "as", name: "American Samoa" }, { code : "ad", name: "Andorra" }, { code : "ao", name: "Angola" }, { code : "ai", name: "Anguilla" }, { code : "aq", name: "Antarctica" }, { code : "ag", name: "Antigua and Barbuda" }, { code : "ar", name: "Argentina" }, { code : "am", name: "Armenia" }, { code : "aw", name: "Aruba" }, { code : "au", name: "Australia" }, { code : "at", name: "Austria" }, { code : "az", name: "Azerbaijan" }, { code : "bs", name: "Bahamas" }, { code : "bh", name: "Bahrain" }, { code : "bd", name: "Bangladesh" }, { code : "bb", name: "Barbados" }, { code : "by", name: "Belarus" }, { code : "be", name: "Belgium" }, { code : "bz", name: "Belize" }, { code : "bj", name: "Benin" }, { code : "bm", name: "Bermuda" }, { code : "bt", name: "Bhutan" }, { code : "bo", name: "Bolivia" }, { code : "bq", name: "Bonaire, Saint Eustatius and Saba" }, { code : "ba", name: "Bosnia and Herzegovina" }, { code : "bw", name: "Botswana" }, { code : "bv", name: "Bouvet Island" }, { code : "br", name: "Brazil" }, { code : "io", name: "British Indian Ocean Territory" }, { code : "vg", name: "British Virgin Islands" }, { code : "bn", name: "Brunei" }, { code : "bg", name: "Bulgaria" }, { code : "bf", name: "Burkina Faso" }, { code : "bi", name: "Burundi" }, { code : "kh", name: "Cambodia" }, { code : "cm", name: "Cameroon" }, { code : "ca", name: "Canada" }, { code : "cv", name: "Cabo Verde" }, { code : "ky", name: "Cayman Islands" }, { code : "cf", name: "Central African Republic" }, { code : "td", name: "Chad" }, { code : "cl", name: "Chile" }, { code : "cn", name: "China" }, { code : "cx", name: "Christmas Island" }, { code : "cc", name: "Cocos (Keeling) Islands" }, { code : "co", name: "Colombia" }, { code : "km", name: "Comoros" }, { code : "cg", name: "Congo" }, { code : "ck", name: "Cook Islands" }, { code : "cr", name: "Costa Rica" }, { code : "ci", name: "Côte d'Ivoire" }, { code : "hr", name: "Croatia" }, { code : "cu", name: "Cuba" }, { code : "cw", name: "Curaçao" }, { code : "cy", name: "Cyprus" }, { code : "cz", name: "Czech Republic" }, { code : "kp", name: "Democratic People's Republic of Korea" }, { code : "cd", name: "Democratic Republic of the Congo" }, { code : "dk", name: "Denmark" }, { code : "dj", name: "Djibouti" }, { code : "dm", name: "Dominica" }, { code : "do", name: "Dominican Republic" }, { code : "ec", name: "Ecuador" }, { code : "eg", name: "Egypt" }, { code : "sv", name: "El Salvador" }, { code : "gq", name: "Equatorial Guinea" }, { code : "er", name: "Eritrea" }, { code : "ee", name: "Estonia" }, { code : "et", name: "Ethiopia" }, { code : "fk", name: "Falkland Islands (Malvinas)" }, { code : "fo", name: "Faroe Islands" }, { code : "fj", name: "Fiji" }, { code : "fi", name: "Finland" }, { code : "fr", name: "France" }, { code : "gf", name: "French Guiana" }, { code : "pf", name: "French Polynesia" }, { code : "tf", name: "French Southern Territories" }, { code : "ga", name: "Gabon" }, { code : "gm", name: "Gambia" }, { code : "ge", name: "Georgia" }, { code : "de", name: "Germany" }, { code : "gh", name: "Ghana" }, { code : "gi", name: "Gibraltar" }, { code : "gr", name: "Greece" }, { code : "gl", name: "Greenland" }, { code : "gd", name: "Grenada" }, { code : "gp", name: "Guadeloupe" }, { code : "gu", name: "Guam" }, { code : "gt", name: "Guatemala" }, { code : "gg", name: "Guernsey" }, { code : "gn", name: "Guinea" }, { code : "gw", name: "Guinea-Bissau" }, { code : "gy", name: "Guyana" }, { code : "ht", name: "Haiti" }, { code : "hm", name: "Heard Island and McDonald Islands" }, { code : "va", name: "Holy See (Vatican City)" }, { code : "hn", name: "Honduras" }, { code : "hk", name: "Hong Kong" }, { code : "hu", name: "Hungary" }, { code : "is", name: "Iceland" }, { code : "in", name: "India" }, { code : "id", name: "Indonesia" }, { code : "ir", name: "Iran" }, { code : "iq", name: "Iraq" }, { code : "ie", name: "Ireland" }, { code : "im", name: "Isle of Man" }, { code : "il", name: "Israel" }, { code : "it", name: "Italy" }, { code : "jm", name: "Jamaica" }, { code : "jp", name: "Japan" }, { code : "je", name: "Jersey" }, { code : "jo", name: "Jordan" }, { code : "kz", name: "Kazakhstan" }, { code : "ke", name: "Kenya" }, { code : "ki", name: "Kiribati" }, { code : "xk", name: "Kosovo" }, { code : "kw", name: "Kuwait" }, { code : "kg", name: "Kyrgyzstan" }, { code : "la", name: "Laos" }, { code : "lv", name: "Latvia" }, { code : "lb", name: "Lebanon" }, { code : "ls", name: "Lesotho" }, { code : "lr", name: "Liberia" }, { code : "ly", name: "Libya" }, { code : "li", name: "Liechtenstein" }, { code : "lt", name: "Lithuania" }, { code : "lu", name: "Luxembourg" }, { code : "mo", name: "Macao" }, { code : "mk", name: "Macedonia" }, { code : "mg", name: "Madagascar" }, { code : "mw", name: "Malawi" }, { code : "my", name: "Malaysia" }, { code : "mv", name: "Maldives" }, { code : "ml", name: "Mali" }, { code : "mt", name: "Malta" }, { code : "mh", name: "Marshall Islands" }, { code : "mq", name: "Martinique" }, { code : "mr", name: "Mauritania" }, { code : "mu", name: "Mauritius" }, { code : "yt", name: "Mayotte" }, { code : "mx", name: "Mexico" }, { code : "fm", name: "Micronesia" }, { code : "md", name: "Moldova" }, { code : "mc", name: "Monaco" }, { code : "mn", name: "Mongolia" }, { code : "me", name: "Montenegro" }, { code : "ms", name: "Montserrat" }, { code : "ma", name: "Morocco" }, { code : "mz", name: "Mozambique" }, { code : "mm", name: "Myanmar" }, { code : "na", name: "Namibia" }, { code : "nr", name: "Nauru" }, { code : "np", name: "Nepal" }, { code : "nl", name: "Netherlands" }, { code : "an", name: "Netherlands Antilles" }, { code : "nc", name: "New Caledonia" }, { code : "nz", name: "New Zealand" }, { code : "ni", name: "Nicaragua" }, { code : "ne", name: "Niger" }, { code : "ng", name: "Nigeria" }, { code : "nu", name: "Niue" }, { code : "nf", name: "Norfolk Island" }, { code : "mp", name: "Northern Mariana Islands" }, { code : "no", name: "Norway" }, { code : "om", name: "Oman" }, { code : "pk", name: "Pakistan" }, { code : "pw", name: "Palau" }, { code : "ps", name: "Palestine" }, { code : "pa", name: "Panama" }, { code : "pg", name: "Papua New Guinea" }, { code : "py", name: "Paraguay" }, { code : "pe", name: "Peru" }, { code : "ph", name: "Philippines" }, { code : "pn", name: "Pitcairn" }, { code : "pl", name: "Poland" }, { code : "pt", name: "Portugal" }, { code : "pr", name: "Puerto Rico" }, { code : "qa", name: "Qatar" }, { code : "kr", name: "Republic of Korea" }, { code : "re", name: "Réunion" }, { code : "ro", name: "Romania" }, { code : "ru", name: "Russia" }, { code : "rw", name: "Rwanda" }, { code : "bl", name: "Saint Barthélemy" }, { code : "sh", name: "Saint Helena, Ascension and Tristan da Cunha" }, { code : "kn", name: "Saint Kitts and Nevis" }, { code : "lc", name: "Saint Lucia" }, { code : "mf", name: "Saint Martin" }, { code : "pm", name: "Saint Pierre and Miquelon" }, { code : "vc", name: "Saint Vincent and the Grenadines" }, { code : "ws", name: "Samoa" }, { code : "sm", name: "San Marino" }, { code : "st", name: "Sao Tome and Principe" }, { code : "sa", name: "Saudi Arabia" }, { code : "sn", name: "Senegal" }, { code : "rs", name: "Serbia" }, { code : "sc", name: "Seychelles" }, { code : "sl", name: "Sierra Leone" }, { code : "sg", name: "Singapore" }, { code : "sx", name: "Sint Maarten" }, { code : "sk", name: "Slovakia" }, { code : "si", name: "Slovenia" }, { code : "sb", name: "Solomon Islands" }, { code : "so", name: "Somalia" }, { code : "za", name: "South Africa" }, { code : "gs", name: "South Georgia and the South Sandwich Islands" }, { code : "ss", name: "South Sudan" }, { code : "es", name: "Spain" }, { code : "lk", name: "Sri Lanka" }, { code : "sd", name: "Sudan" }, { code : "sr", name: "Suriname" }, { code : "sj", name: "Svalbard and Jan Mayen" }, { code : "sz", name: "Swaziland" }, { code : "se", name: "Sweden" }, { code : "ch", name: "Switzerland" }, { code : "sy", name: "Syria" }, { code : "tw", name: "Taiwan" }, { code : "tj", name: "Tajikistan" }, { code : "tz", name: "Tanzania" }, { code : "th", name: "Thailand" }, { code : "tl", name: "Timor-Leste" }, { code : "tg", name: "Togo" }, { code : "tk", name: "Tokelau" }, { code : "to", name: "Tonga" }, { code : "tt", name: "Trinidad and Tobago" }, { code : "tn", name: "Tunisia" }, { code : "tr", name: "Turkey" }, { code : "tm", name: "Turkmenistan" }, { code : "tc", name: "Turks and Caicos Islands" }, { code : "tv", name: "Tuvalu" }, { code : "vi", name: "U.S. Virgin Islands" }, { code : "ug", name: "Uganda" }, { code : "ua", name: "Ukraine" }, { code : "ae", name: "United Arab Emirates" }, { code : "gb", name: "United Kingdom" }, { code : "us", name: "United States of America" }, { code : "um", name: "United States Minor Outlying Islands" }, { code : "uy", name: "Uruguay" }, { code : "uz", name: "Uzbekistan" }, { code : "vu", name: "Vanuatu" }, { code : "ve", name: "Venezuela" }, { code : "vn", name: "Vietnam" }, { code : "wf", name: "Wallis and Futuna" }, { code : "eh", name: "Western Sahara" }, { code : "ye", name: "Yemen" }, { code : "zm", name: "Zambia" }, { code : "zw", name: "Zimbabwe" } ] universities = {} universitiesByDomain = {} + + defaultRoleHints = [ "Undergraduate Student", "Masters Student (MSc, MA, ...)", "Doctoral Student (PhD, EngD, ...)", "Postdoc", "Lecturer", "Senior Lecturer", "Reader", "Associate Professor ", "Assistant Professor ", "Professor", "Emeritus Professor" ] + defaultDepartmentHints = [ "Aeronautics & Astronautics", "Anesthesia", "Anthropology", "Applied Physics", "Art & Art History", "Biochemistry", "Bioengineering", "Biology", "Business School Library", "Business, Graduate School of", "Cardiothoracic Surgery", "Chemical and Systems Biology", "Chemical Engineering", "Chemistry", "Civil & Environmental Engineering", "Classics", "Communication", "Comparative Literature", "Comparative Medicine", "Computer Science", "Dermatology", "Developmental Biology", "Earth System Science", "East Asian Languages and Cultures", "Economics", "Education, School of", "Electrical Engineering", "Energy Resources Engineering", "English", "French and Italian", "Genetics", "Geological Sciences", "Geophysics", "German Studies", "Health Research & Policy", "History", "Iberian & Latin American Cultures", "Law Library", "Law School", "Linguistics", "Management Science & Engineering", "Materials Science & Engineering", "Mathematics", "Mechanical Engineering", "Medical Library", "Medicine", "Microbiology & Immunology", "Molecular & Cellular Physiology", "Music", "Neurobiology", "Neurology & Neurological Sciences", "Neurosurgery", "Obstetrics and Gynecology", "Ophthalmology", "Orthopaedic Surgery", "Otolaryngology (Head and Neck Surgery)", "Pathology", "Pediatrics", "Philosophy", "Physics", "Political Science", "Psychiatry and Behavioral Sciences", "Psychology", "Radiation Oncology", "Radiology", "Religious Studies", "Slavic Languages and Literature", "Sociology", "University Libraries", "Statistics", "Structural Biology", "Surgery", "Theater and Performance Studies", "Urology" ] domainsBlackList = { "overleaf.com" : true } commonTLDs = [ "br", "cn", "co", "co.jp", "co.uk", "com", "com.au", "de","fr", "in", "info", "io", "net", "no", "ru", "se", "us", "com.tw", "com.br", "pl", "it", "co.in", "com.mx" ] @@ -17,7 +19,13 @@ define [ App.factory "UserAffiliationsDataService", ["$http", "$q", "_", ($http, $q, _) -> getCountries = () -> - $q.resolve(countriesList) + $q.resolve countriesList + + getDefaultRoleHints = () -> + $q.resolve defaultRoleHints + + getDefaultDepartmentHints = () -> + $q.resolve defaultDepartmentHints getUserEmails = () -> $http.get "/user/emails" @@ -88,6 +96,8 @@ define [ return { getCountries + getDefaultRoleHints + getDefaultDepartmentHints getUserEmails getUniversitiesFromCountry getUniversityDomainFromPartialDomainInput From 2e46111b4ba9dcee4f6394eb3e40943a721dbae1 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 28 Jun 2018 17:02:04 +0100 Subject: [PATCH 27/27] Add explanation about using template cache directly. --- services/web/public/coffee/base.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index 370823703a..cb9e70b6bf 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -28,6 +28,9 @@ define [ }) App.run ($templateCache) -> + # UI Select templates are hard-coded and use Glyphicon icons (which we don't import). + # The line below simply overrides the hard-coded template with our own, which is + # basically the same but using Font Awesome icons. $templateCache.put "bootstrap/match.tpl.html", "
    {{$select.placeholder}}
    " sl_debugging = window.location?.search?.match(/debug=true/)?