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/libs/sharejs.js
|
||||||
public/js/analytics/
|
public/js/analytics/
|
||||||
public/js/directives/
|
public/js/directives/
|
||||||
|
public/js/components/
|
||||||
public/js/es/
|
public/js/es/
|
||||||
public/js/filters/
|
public/js/filters/
|
||||||
public/js/ide/
|
public/js/ide/
|
||||||
|
|
|
@ -68,6 +68,7 @@ module.exports =
|
||||||
shouldAllowEditingDetails: shouldAllowEditingDetails
|
shouldAllowEditingDetails: shouldAllowEditingDetails
|
||||||
languages: Settings.languages,
|
languages: Settings.languages,
|
||||||
accountSettingsTabActive: true
|
accountSettingsTabActive: true
|
||||||
|
showAffiliationsUI: (req.query?.aff == "true") or false
|
||||||
|
|
||||||
sessionsPage: (req, res, next) ->
|
sessionsPage: (req, res, next) ->
|
||||||
user = AuthenticationController.getSessionUser(req)
|
user = AuthenticationController.getSessionUser(req)
|
||||||
|
|
|
@ -114,7 +114,7 @@ module.exports = class Router
|
||||||
webRouter.post '/user/emails',
|
webRouter.post '/user/emails',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
UserEmailsController.add
|
UserEmailsController.add
|
||||||
webRouter.delete '/user/emails',
|
webRouter.post '/user/emails/delete',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
UserEmailsController.remove
|
UserEmailsController.remove
|
||||||
webRouter.post '/user/emails/default',
|
webRouter.post '/user/emails/default',
|
||||||
|
|
|
@ -4,117 +4,122 @@ block content
|
||||||
.content.content-alt
|
.content.content-alt
|
||||||
.container
|
.container
|
||||||
.row
|
.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
|
.card
|
||||||
.page-header
|
.page-header
|
||||||
h1 #{translate("account_settings")}
|
h1 #{translate("account_settings")}
|
||||||
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
||||||
|
if locals.showAffiliationsUI
|
||||||
|
include settings/user-affiliations
|
||||||
|
|
||||||
form-messages(for="settingsForm")
|
form-messages(for="settingsForm")
|
||||||
.alert.alert-success(ng-show="settingsForm.response.success")
|
.alert.alert-success(ng-show="settingsForm.response.success")
|
||||||
| #{translate("thanks_settings_updated")}
|
| #{translate("thanks_settings_updated")}
|
||||||
form-messages(for="changePasswordForm")
|
form-messages(for="changePasswordForm")
|
||||||
.container-fluid
|
|
||||||
.row
|
.row
|
||||||
.col-md-5
|
.col-md-5
|
||||||
h3 #{translate("update_account_info")}
|
h3 #{translate("update_account_info")}
|
||||||
form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
|
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)
|
input(type="hidden", name="_csrf", value=csrfToken)
|
||||||
if !externalAuthenticationSystemUsed()
|
.form-group
|
||||||
.form-group
|
label(for='currentPassword') #{translate("current_password")}
|
||||||
label(for='email') #{translate("email")}
|
input.form-control(
|
||||||
input.form-control(
|
type='password',
|
||||||
type='email',
|
name='currentPassword',
|
||||||
name='email',
|
placeholder='*********',
|
||||||
placeholder="email@example.com"
|
ng-model="currentPassword",
|
||||||
required,
|
required
|
||||||
ng-model="email",
|
)
|
||||||
ng-init="email = "+JSON.stringify(user.email),
|
span.small.text-primary(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty")
|
||||||
ng-model-options="{ updateOn: 'blur' }"
|
| #{translate("required")}
|
||||||
)
|
.form-group
|
||||||
span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
|
label(for='newPassword1') #{translate("new_password")}
|
||||||
| #{translate("must_be_email_address")}
|
input.form-control(
|
||||||
else
|
id='passwordField',
|
||||||
// show the email, non-editable
|
type='password',
|
||||||
.form-group
|
name='newPassword1',
|
||||||
label.control-label #{translate("email")}
|
placeholder='*********',
|
||||||
div.form-control(readonly="true") #{user.email}
|
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)}
|
| !{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"
|
sessionSecret = "secret-please-change"
|
||||||
|
|
||||||
|
v1Api =
|
||||||
|
url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000"
|
||||||
|
user: 'overleaf'
|
||||||
|
pass: 'password'
|
||||||
|
|
||||||
module.exports = settings =
|
module.exports = settings =
|
||||||
|
|
||||||
allowAnonymousReadAndWriteSharing:
|
allowAnonymousReadAndWriteSharing:
|
||||||
|
@ -157,9 +162,9 @@ module.exports = settings =
|
||||||
thirdpartyreferences:
|
thirdpartyreferences:
|
||||||
url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046"
|
url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046"
|
||||||
v1:
|
v1:
|
||||||
url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000"
|
url: v1Api.url
|
||||||
user: 'overleaf'
|
user: v1Api.user
|
||||||
pass: 'password'
|
pass: v1Api.pass
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
|
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
|
||||||
|
@ -420,7 +425,9 @@ module.exports = settings =
|
||||||
redirects:
|
redirects:
|
||||||
"/templates/index": "/templates/"
|
"/templates/index": "/templates/"
|
||||||
|
|
||||||
proxyUrls: {}
|
proxyUrls:
|
||||||
|
'/institutions/list': { baseUrl: v1Api.url, path: '/universities/list' }
|
||||||
|
'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }
|
||||||
|
|
||||||
reloadModuleViewsOnEachRequest: true
|
reloadModuleViewsOnEachRequest: true
|
||||||
|
|
||||||
|
|
|
@ -17,14 +17,22 @@ define [
|
||||||
"ErrorCatcher"
|
"ErrorCatcher"
|
||||||
"localStorage"
|
"localStorage"
|
||||||
"ngTagsInput"
|
"ngTagsInput"
|
||||||
]).config ($qProvider, sixpackProvider, $httpProvider)->
|
"ui.select"
|
||||||
|
]).config ($qProvider, sixpackProvider, $httpProvider, uiSelectConfig) ->
|
||||||
$qProvider.errorOnUnhandledRejections(false)
|
$qProvider.errorOnUnhandledRejections(false)
|
||||||
|
uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
|
||||||
sixpackProvider.setOptions({
|
sixpackProvider.setOptions({
|
||||||
debug: false
|
debug: false
|
||||||
baseUrl: window.sharelatex.sixpackDomain
|
baseUrl: window.sharelatex.sixpackDomain
|
||||||
client_id: window.user_id
|
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/)?
|
sl_debugging = window.location?.search?.match(/debug=true/)?
|
||||||
window.sl_console =
|
window.sl_console =
|
||||||
log: (args...) -> console.log(args...) if sl_debugging
|
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/sixpack"
|
||||||
"libs/angular-sixpack"
|
"libs/angular-sixpack"
|
||||||
"libs/ng-tags-input-3.0.0"
|
"libs/ng-tags-input-3.0.0"
|
||||||
|
"libs/select/select"
|
||||||
], () ->
|
], () ->
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,9 @@ define [
|
||||||
"main/subscription/team-invite-controller"
|
"main/subscription/team-invite-controller"
|
||||||
"main/contact-us"
|
"main/contact-us"
|
||||||
"main/learn"
|
"main/learn"
|
||||||
|
"main/affiliations/controllers/UserAffiliationsController"
|
||||||
|
"main/affiliations/factories/UserAffiliationsDataService"
|
||||||
|
"main/keys"
|
||||||
"analytics/AbTestingManager"
|
"analytics/AbTestingManager"
|
||||||
"directives/asyncForm"
|
"directives/asyncForm"
|
||||||
"directives/stopPropagation"
|
"directives/stopPropagation"
|
||||||
|
@ -34,6 +37,7 @@ define [
|
||||||
"services/queued-http"
|
"services/queued-http"
|
||||||
"services/validateCaptcha"
|
"services/validateCaptcha"
|
||||||
"filters/formatDate"
|
"filters/formatDate"
|
||||||
|
"components/inputSuggestions"
|
||||||
"__MAIN_CLIENTSIDE_INCLUDES__"
|
"__MAIN_CLIENTSIDE_INCLUDES__"
|
||||||
], () ->
|
], () ->
|
||||||
angular.module('SharelatexApp').config(
|
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/close.less";
|
||||||
@import "components/fineupload.less";
|
@import "components/fineupload.less";
|
||||||
@import "components/hover.less";
|
@import "components/hover.less";
|
||||||
|
@import "components/ui-select.less";
|
||||||
|
@import "components/input-suggestions.less";
|
||||||
|
|
||||||
// Components w/ JavaScript
|
// Components w/ JavaScript
|
||||||
@import "components/modals.less";
|
@import "components/modals.less";
|
||||||
|
@ -87,4 +89,5 @@
|
||||||
@import "../js/libs/pdfListView/TextLayer.css";
|
@import "../js/libs/pdfListView/TextLayer.css";
|
||||||
@import "../js/libs/pdfListView/AnnotationsLayer.css";
|
@import "../js/libs/pdfListView/AnnotationsLayer.css";
|
||||||
@import "../js/libs/pdfListView/HighlightsLayer.css";
|
@import "../js/libs/pdfListView/HighlightsLayer.css";
|
||||||
|
@import "../js/libs/select/select.css";
|
||||||
@import "vendor/codemirror.css";
|
@import "vendor/codemirror.css";
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
.alert {
|
.alert {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#delete-account-modal {
|
#delete-account-modal {
|
||||||
|
@ -10,3 +13,29 @@
|
||||||
margin-bottom: 4px;
|
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-bg : @ol-blue;
|
||||||
@btn-info-border : transparent;
|
@btn-info-border : transparent;
|
||||||
|
|
||||||
|
@padding-xs-horizontal : 8px;
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
@alert-padding : 15px;
|
@alert-padding : 15px;
|
||||||
@alert-border-radius : @border-radius-base;
|
@alert-border-radius : @border-radius-base;
|
||||||
|
|
Loading…
Reference in a new issue