mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #697 from sharelatex/pr-user-affilitations
User affiliations
This commit is contained in:
commit
04a98c4d91
24 changed files with 3595 additions and 107 deletions
1
services/web/.gitignore
vendored
1
services/web/.gitignore
vendored
|
@ -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/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -114,7 +114,7 @@ module.exports = class Router
|
|||
webRouter.post '/user/emails',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserEmailsController.add
|
||||
webRouter.delete '/user/emails',
|
||||
webRouter.post '/user/emails/delete',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserEmailsController.remove
|
||||
webRouter.post '/user/emails/default',
|
||||
|
|
|
@ -4,117 +4,122 @@ 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")}
|
||||
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
||||
if locals.showAffiliationsUI
|
||||
include settings/user-affiliations
|
||||
|
||||
form-messages(for="settingsForm")
|
||||
.alert.alert-success(ng-show="settingsForm.response.success")
|
||||
| #{translate("thanks_settings_updated")}
|
||||
form-messages(for="changePasswordForm")
|
||||
.container-fluid
|
||||
.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)}
|
||||
|
||||
|
|
172
services/web/app/views/user/settings/user-affiliations.pug
Normal file
172
services/web/app/views/user/settings/user-affiliations.pug
Normal file
|
@ -0,0 +1,172 @@
|
|||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
ui-select(
|
||||
ng-model="newAffiliation.department"
|
||||
tagging
|
||||
tagging-label="false"
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder="Department"
|
||||
) {{ $select.selected }}
|
||||
ui-select-choices(
|
||||
repeat="department in departments | 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -17,14 +17,22 @@ 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) ->
|
||||
# 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", "<div class=\"ui-select-match\" ng-hide=\"$select.open && $select.searchEnabled\" ng-disabled=\"$select.disabled\" ng-class=\"{\'btn-default-focus\':$select.focus}\"><span tabindex=\"-1\" class=\"btn btn-default form-control ui-select-toggle\" aria-label=\"{{ $select.baseTitle }} activate\" ng-disabled=\"$select.disabled\" ng-click=\"$select.activate()\" style=\"outline: 0;\"><span ng-show=\"$select.isEmpty()\" class=\"ui-select-placeholder text-muted\">{{$select.placeholder}}</span> <span ng-hide=\"$select.isEmpty()\" class=\"ui-select-match-text pull-left\" ng-class=\"{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}\" ng-transclude=\"\"></span> <i class=\"caret pull-right\" ng-click=\"$select.toggle($event)\"></i> <a ng-show=\"$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)\" aria-label=\"{{ $select.baseTitle }} clear\" style=\"margin-right: 10px\" ng-click=\"$select.clear($event)\" class=\"btn btn-xs btn-link pull-right\"><i class=\"fa fa-times\" aria-hidden=\"true\"></i></a></span></div>"
|
||||
|
||||
sl_debugging = window.location?.search?.match(/debug=true/)?
|
||||
window.sl_console =
|
||||
log: (args...) -> console.log(args...) if sl_debugging
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
inputSuggestionsController = ($scope, $element, $attrs, Keys) ->
|
||||
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.onBlur()
|
||||
ctrl.handleKeyDown = ($event) ->
|
||||
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
|
||||
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"
|
||||
localNgModelOptions: "=?ngModelOptions"
|
||||
getSuggestion: "&"
|
||||
onBlur: "&?"
|
||||
inputId: "@?"
|
||||
inputName: "@?"
|
||||
inputPlaceholder: "@?"
|
||||
inputType: "@?"
|
||||
inputRequired: "=?"
|
||||
controller: inputSuggestionsController
|
||||
template: [
|
||||
'<div class="input-suggestions">',
|
||||
'<div class="form-control input-suggestions-shadow">',
|
||||
'<span ng-bind="$ctrl.localNgModel"',
|
||||
' class="input-suggestions-shadow-existing"',
|
||||
' ng-show="$ctrl.showHint">',
|
||||
'</span>',
|
||||
'<span ng-bind="$ctrl.suggestion"',
|
||||
' class="input-suggestions-shadow-suggested"',
|
||||
' ng-show="$ctrl.showHint">',
|
||||
'</span>',
|
||||
'</div>',
|
||||
'<input type="text"',
|
||||
' class="form-control input-suggestions-main"',
|
||||
' ng-focus="$ctrl.handleFocus()"',
|
||||
' ng-keyDown="$ctrl.handleKeyDown($event)"',
|
||||
' ng-blur="$ctrl.handleBlur()"',
|
||||
' ng-model="$ctrl.localNgModel"',
|
||||
' ng-model-options="$ctrl.localNgModelOptions"',
|
||||
' ng-model-options="{ debounce: 50 }"',
|
||||
' ng-attr-id="{{ ::$ctrl.inputId }}"',
|
||||
' ng-attr-placeholder="{{ ::$ctrl.inputPlaceholder }}"',
|
||||
' ng-attr-type="{{ ::$ctrl.inputType }}"',
|
||||
' ng-attr-name="{{ ::$ctrl.inputName }}"',
|
||||
' ng-required="::$ctrl.inputRequired">',
|
||||
'</div>'
|
||||
].join ""
|
||||
|
||||
}
|
|
@ -11,5 +11,6 @@ define [
|
|||
"libs/sixpack"
|
||||
"libs/angular-sixpack"
|
||||
"libs/ng-tags-input-3.0.0"
|
||||
"libs/select/select"
|
||||
], () ->
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ define [
|
|||
"main/subscription/team-invite-controller"
|
||||
"main/contact-us"
|
||||
"main/learn"
|
||||
"main/affiliations/controllers/UserAffiliationsController"
|
||||
"main/affiliations/factories/UserAffiliationsDataService"
|
||||
"main/keys"
|
||||
"analytics/AbTestingManager"
|
||||
"directives/asyncForm"
|
||||
"directives/stopPropagation"
|
||||
|
@ -34,6 +37,7 @@ define [
|
|||
"services/queued-http"
|
||||
"services/validateCaptcha"
|
||||
"filters/formatDate"
|
||||
"components/inputSuggestions"
|
||||
"__MAIN_CLIENTSIDE_INCLUDES__"
|
||||
], () ->
|
||||
angular.module('SharelatexApp').config(
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", "_", ($scope, UserAffiliationsDataService, $q, _) ->
|
||||
$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_\-\.]+)([^\.])$/
|
||||
|
||||
_matchLocalAndDomain = (userEmailInput) ->
|
||||
match = userEmailInput?.match LOCAL_AND_DOMAIN_REGEX
|
||||
if match?
|
||||
{ local: match[1], domain: match[2] }
|
||||
else
|
||||
{ local: null, domain: null }
|
||||
|
||||
$scope.addUniversityToSelection = (universityName) ->
|
||||
{ name: universityName, isUserSuggested: true }
|
||||
|
||||
$scope.getEmailSuggestion = (userInput) ->
|
||||
userInputLocalAndDomain = _matchLocalAndDomain(userInput)
|
||||
$scope.ui.isValidEmail = EMAIL_REGEX.test userInput
|
||||
$scope.ui.isBlacklistedEmail = false
|
||||
$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
|
||||
if currentUserInputLocalAndDomain.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
|
||||
$q.reject null
|
||||
else
|
||||
$scope.newAffiliation.university = null
|
||||
$scope.newAffiliation.department = null
|
||||
$q.reject null
|
||||
|
||||
|
||||
$scope.selectUniversityManually = () ->
|
||||
$scope.newAffiliation.university = null
|
||||
$scope.newAffiliation.department = null
|
||||
$scope.ui.showManualUniversitySelectionUI = true
|
||||
|
||||
$scope.showAddEmailForm = () ->
|
||||
$scope.ui.showAddEmailUI = true
|
||||
|
||||
$scope.addNewEmail = () ->
|
||||
$scope.ui.isAddingNewEmail = true
|
||||
if !$scope.newAffiliation.university?
|
||||
addEmailPromise = UserAffiliationsDataService
|
||||
.addUserEmail $scope.newAffiliation.email
|
||||
else
|
||||
if $scope.newAffiliation.university.isUserSuggested
|
||||
addEmailPromise = UserAffiliationsDataService
|
||||
.addUserAffiliationWithUnknownUniversity(
|
||||
$scope.newAffiliation.email,
|
||||
$scope.newAffiliation.university.name,
|
||||
$scope.newAffiliation.country.code,
|
||||
$scope.newAffiliation.role,
|
||||
$scope.newAffiliation.department
|
||||
)
|
||||
else
|
||||
addEmailPromise = UserAffiliationsDataService
|
||||
.addUserAffiliation(
|
||||
$scope.newAffiliation.email,
|
||||
$scope.newAffiliation.university.id
|
||||
$scope.newAffiliation.role,
|
||||
$scope.newAffiliation.department
|
||||
)
|
||||
addEmailPromise.then () ->
|
||||
_reset()
|
||||
_getUserEmails()
|
||||
|
||||
$scope.setDefaultUserEmail = (email) ->
|
||||
$scope.ui.isLoadingEmails = true
|
||||
UserAffiliationsDataService
|
||||
.setDefaultUserEmail email
|
||||
.then () -> _getUserEmails()
|
||||
|
||||
$scope.removeUserEmail = (email) ->
|
||||
$scope.ui.isLoadingEmails = true
|
||||
UserAffiliationsDataService
|
||||
.removeUserEmail email
|
||||
.then () -> _getUserEmails()
|
||||
|
||||
$scope.getDepartments = () ->
|
||||
if $scope.newAffiliation.university?.departments.length > 0
|
||||
_.uniq $scope.newAffiliation.university.departments
|
||||
else
|
||||
UserAffiliationsDataService.getDefaultDepartmentHints()
|
||||
|
||||
_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 = () ->
|
||||
$scope.ui.isLoadingEmails = true
|
||||
UserAffiliationsDataService
|
||||
.getUserEmails()
|
||||
.then (emails) ->
|
||||
$scope.userEmails = emails
|
||||
$scope.ui.isLoadingEmails = false
|
||||
_getUserEmails()
|
||||
|
||||
# Populates the countries dropdown
|
||||
UserAffiliationsDataService
|
||||
.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
|
||||
$scope.newAffiliation.university = null
|
||||
$scope.newAffiliation.role = null
|
||||
$scope.newAffiliation.department = null
|
||||
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
|
||||
|
||||
]
|
File diff suppressed because one or more lines are too long
16
services/web/public/coffee/main/keys.coffee
Normal file
16
services/web/public/coffee/main/keys.coffee
Normal file
|
@ -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
|
362
services/web/public/js/libs/select/select.css
Executable file
362
services/web/public/js/libs/select/select.css
Executable file
|
@ -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;
|
||||
}
|
2427
services/web/public/js/libs/select/select.js
Executable file
2427
services/web/public/js/libs/select/select.js
Executable file
File diff suppressed because it is too large
Load diff
7
services/web/public/js/libs/select/select.min.css
vendored
Executable file
7
services/web/public/js/libs/select/select.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
services/web/public/js/libs/select/select.min.css.map
Executable file
1
services/web/public/js/libs/select/select.min.css.map
Executable file
File diff suppressed because one or more lines are too long
9
services/web/public/js/libs/select/select.min.js
vendored
Executable file
9
services/web/public/js/libs/select/select.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
services/web/public/js/libs/select/select.min.js.map
Executable file
1
services/web/public/js/libs/select/select.min.js.map
Executable file
File diff suppressed because one or more lines are too long
|
@ -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";
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
.alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#delete-account-modal {
|
||||
|
@ -10,3 +13,29 @@
|
|||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.affiliations-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.affiliations-table-email {
|
||||
width: 40%;
|
||||
}
|
||||
.affiliations-table-institution {
|
||||
width: 40%;
|
||||
}
|
||||
.affiliations-table-inline-actions {
|
||||
width: 20%;
|
||||
}
|
||||
.affiliations-table-highlighted-row {
|
||||
background-color: tint(@content-alt-bg-color, 6%);
|
||||
}
|
||||
|
||||
.affiliations-form-group {
|
||||
margin-top: @table-cell-padding;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.affiliations-table-label {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
|
|
@ -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%);
|
||||
}
|
60
services/web/public/stylesheets/components/ui-select.less
Normal file
60
services/web/public/stylesheets/components/ui-select.less
Normal file
|
@ -0,0 +1,60 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-select-container[tagging] {
|
||||
.ui-select-toggle {
|
||||
cursor: text;
|
||||
> i.caret.pull-right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue