From 3603d291bc75efec27e0e1b966531bcd65efc9ff Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Mon, 7 Jun 2021 10:43:26 +0100 Subject: [PATCH] Merge pull request #4140 from overleaf/ae-react-share-modal Remove Angular share modal code GitOrigin-RevId: 136b10c7b9768f2d8de13e48b16fd71947506624 --- app/src/Features/Project/ProjectController.js | 4 - app/views/project/editor.pug | 1 - app/views/project/editor/header-react.pug | 13 +- app/views/project/editor/header.pug | 13 +- app/views/project/editor/share.pug | 287 ---- frontend/js/base.js | 1 - frontend/js/ide.js | 2 +- ...OwnershipTransferConfirmModalController.js | 35 - .../ide/share/controllers/ShareController.js | 55 - .../ShareProjectModalController.js | 309 ----- .../ShareProjectModalMemberRowController.js | 46 - frontend/js/ide/share/index.js | 7 - .../js/ide/share/services/projectInvites.js | 37 - .../js/ide/share/services/projectMembers.js | 48 - frontend/js/libraries.js | 1 - .../js/vendor/libs/ng-tags-input-3.0.0.js | 1151 ----------------- 16 files changed, 13 insertions(+), 1997 deletions(-) delete mode 100644 app/views/project/editor/share.pug delete mode 100644 frontend/js/ide/share/controllers/OwnershipTransferConfirmModalController.js delete mode 100644 frontend/js/ide/share/controllers/ShareController.js delete mode 100644 frontend/js/ide/share/controllers/ShareProjectModalController.js delete mode 100644 frontend/js/ide/share/controllers/ShareProjectModalMemberRowController.js delete mode 100644 frontend/js/ide/share/index.js delete mode 100644 frontend/js/ide/share/services/projectInvites.js delete mode 100644 frontend/js/ide/share/services/projectMembers.js delete mode 100644 frontend/js/vendor/libs/ng-tags-input-3.0.0.js diff --git a/app/src/Features/Project/ProjectController.js b/app/src/Features/Project/ProjectController.js index d85f325abd..597a8c5d5f 100644 --- a/app/src/Features/Project/ProjectController.js +++ b/app/src/Features/Project/ProjectController.js @@ -887,10 +887,6 @@ const ProjectController = { 'new_navigation_ui', user.alphaProgram ), - showReactShareModal: shouldDisplayFeature( - 'new_share_modal_ui', - true - ), showReactDropboxModal: shouldDisplayFeature( 'new_dropbox_modal_ui', user.betaProgram diff --git a/app/views/project/editor.pug b/app/views/project/editor.pug index fc05f46c5d..ba8c673f1a 100644 --- a/app/views/project/editor.pug +++ b/app/views/project/editor.pug @@ -81,7 +81,6 @@ block content else include ./editor/header - include ./editor/share != moduleIncludes("publish:body", locals) include ./editor/history/toolbarV2.pug diff --git a/app/views/project/editor/header-react.pug b/app/views/project/editor/header-react.pug index 868e119c64..1382120b56 100644 --- a/app/views/project/editor/header-react.pug +++ b/app/views/project/editor/header-react.pug @@ -1,10 +1,9 @@ -div(ng-controller=showReactShareModal ? 'ReactShareProjectModalController': 'ShareController') - if showReactShareModal - share-project-modal( - handle-hide="handleHide" - show="show" - is-admin="isAdmin" - ) +div(ng-controller="ReactShareProjectModalController") + share-project-modal( + handle-hide="handleHide" + show="show" + is-admin="isAdmin" + ) div(ng-controller="EditorNavigationToolbarController") editor-navigation-toolbar-root( diff --git a/app/views/project/editor/header.pug b/app/views/project/editor/header.pug index 651fcef9d0..e6951a16e0 100644 --- a/app/views/project/editor/header.pug +++ b/app/views/project/editor/header.pug @@ -119,17 +119,16 @@ header.toolbar.toolbar-header.toolbar-with-labels( a.btn.btn-full-height( href ng-click="openShareProjectModal(permissions.admin);" - ng-controller=(showReactShareModal ? 'ReactShareProjectModalController': 'ShareController') + ng-controller="ReactShareProjectModalController" ) i.fa.fa-fw.fa-group p.toolbar-label #{translate("share")} - if showReactShareModal - share-project-modal( - handle-hide="handleHide" - show="show" - is-admin="isAdmin" - ) + share-project-modal( + handle-hide="handleHide" + show="show" + is-admin="isAdmin" + ) != moduleIncludes('publish:button', locals) if !isRestrictedTokenMember diff --git a/app/views/project/editor/share.pug b/app/views/project/editor/share.pug deleted file mode 100644 index 02d1df3308..0000000000 --- a/app/views/project/editor/share.pug +++ /dev/null @@ -1,287 +0,0 @@ -script(type='text/ng-template', id='shareProjectModalTemplate') - .modal-header - button.close( - type="button" - data-dismiss="modal" - ng-click="cancel()" - aria-label="Close" - ) - span(aria-hidden="true") × - h3 #{translate("share_project")} - .modal-body.modal-body-share - .container-fluid - - if isRestrictedTokenMember - //- Token-based access - .row.public-access-level - .col-xs-12.access-token-display-area - div.access-token-wrapper - strong #{translate('anyone_with_link_can_view')} - pre.access-token {{ readOnlyTokenLink }} - - if !isRestrictedTokenMember - //- Private (with token-access available) - .row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'private'") - .col-xs-12.text-center - | #{translate('link_sharing_is_off')} - |    - a( - href - ng-click="makeTokenBased()" - ) #{translate('turn_on_link_sharing')} - span    - a( - href="/learn/how-to/What_is_Link_Sharing%3F" - target="_blank" - ) - i.fa.fa-question-circle( - tooltip=translate('learn_more_about_link_sharing') - ) - - //- Token-based access - .row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'tokenBased'") - .col-xs-12.text-center - strong - | #{translate('link_sharing_is_on')}. - |    - a( - href - ng-click="makePrivate()" - ) #{translate('turn_off_link_sharing')} - span    - a( - href="/learn/how-to/What_is_Link_Sharing%3F" - target="_blank" - ) - i.fa.fa-question-circle( - tooltip=translate('learn_more_about_link_sharing') - ) - - .col-xs-12.access-token-display-area - div.access-token-wrapper - strong #{translate('anyone_with_link_can_edit')} - pre.access-token(ng-show="readAndWriteTokenLink") {{ readAndWriteTokenLink }} - pre.access-token(ng-hide="readAndWriteTokenLink") #{translate('loading')}… - div.access-token-wrapper - strong #{translate('anyone_with_link_can_view')} - pre.access-token(ng-show="readOnlyTokenLink") {{ readOnlyTokenLink }} - pre.access-token(ng-hide="readOnlyTokenLink") #{translate('loading')}… - - //- legacy public-access - .row.public-access-level(ng-show="isAdmin && (project.publicAccesLevel == 'readAndWrite' || project.publicAccesLevel == 'readOnly')") - .col-xs-12.text-center - strong(ng-if="project.publicAccesLevel == 'readAndWrite'") #{translate("this_project_is_public")} - strong(ng-if="project.publicAccesLevel == 'readOnly'") #{translate("this_project_is_public_read_only")} - |    - a( - href - ng-click="makePrivate()" - ) #{translate("make_private")} - - .row.project-member - .col-xs-7 {{ project.owner.email }} - .text-left.col-xs-3 #{translate("owner")} - form.form-horizontal( - ng-if="isAdmin" - ng-repeat="member in project.members" - ng-controller="ShareProjectModalMemberRowController" - ) - .row.form-group.project-member - .col-xs-7.form-control-static {{ member.email }} - .col-xs-3 - select.privileges.form-control.input-sm(name="privileges" ng-model="form.privileges") - option(value="owner") #{translate("owner")} - option(value="readAndWrite") #{translate("can_edit")} - option(value="readOnly") #{translate("read_only")} - .col-xs-2.form-control-static.text-center(ng-hide="form.isModified()") - a( - href - tooltip=translate('remove_collaborator') - tooltip-placement="bottom" - ng-click="removeMember(member)" - aria-label=translate('remove_collaborator') - ) - i.fa.fa-times - .col-xs-2.text-center(ng-show="form.isModified()") - button.btn.btn-sm.btn-success( - type="submit" - ng-click="form.submit()" - ) #{translate("change")} - .text-sm - | #{translate("or")} - | - button.btn.btn-inline-link(ng-click="form.reset()") #{translate("cancel").toLowerCase()} - - .row.project-member(ng-if="!isAdmin" ng-repeat="member in project.members") - .col-xs-7 {{ member.email }} - .col-xs-3 - span(ng-if="member.privileges == 'readAndWrite'") #{translate("can_edit")} - span(ng-if="member.privileges == 'readOnly'") #{translate("read_only")} - .row.project-invite(ng-repeat="invite in project.invites") - .col-xs-7 {{ invite.email }}  - div.small - | #{translate("invite_not_accepted")}.  - button.btn.btn-inline-link( - ng-show="isAdmin", - ng-click="resendInvite(invite, $event)" - ) #{translate("resend")} - .col-xs-3.text-left - // todo: get invite privileges - span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")} - span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")} - .col-xs-2.text-center(ng-if="isAdmin") - a( - href - tooltip=translate('revoke_invite') - tooltip-placement="bottom" - ng-click="revokeInvite(invite)" - ) - i.fa.fa-times - .row.invite-controls(ng-show="isAdmin") - form(ng-show="canAddCollaborators") - .small #{translate("share_with_your_collabs")} - .form-group - tags-input( - class="tags-input" - template="shareTagTemplate" - placeholder=settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, …' - ng-model="inputs.contacts" - focus-on="open" - display-property="display" - add-on-paste="true" - add-on-enter="false" - replace-spaces-with-dashes="false" - type="email" - ) - auto-complete( - source="filterAutocompleteUsers($query)" - template="shareAutocompleteTemplate" - display-property="email" - min-length="0" - ) - .form-group - .pull-right - select.privileges.form-control( - ng-model="inputs.privileges" - name="privileges" - ) - option(value="readAndWrite") #{translate("can_edit")} - option(value="readOnly") #{translate("read_only")} - |    - //- We have to use mousedown here since click has issues with the - //- blur handler in tags-input sometimes changing its height and - //- moving this button, preventing the click registering. - button.btn.btn-info( - type="submit" - ng-mousedown="addMembers()" - ng-keyup="$event.keyCode == 13 ? addMembers() : null" - ) #{translate("share")} - div(ng-hide="canAddCollaborators") - p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also: - .row - .col-md-8.col-md-offset-2 - ul.list-unstyled - li - i.fa.fa-check   - | #{translate("unlimited_projects")} - - li - i.fa.fa-check   - | #{translate("collabs_per_proj", {collabcount:'Multiple'})} - - li - i.fa.fa-check   - | #{translate("full_doc_history")} - - li - i.fa.fa-check   - | #{translate("sync_to_dropbox")} - - li - i.fa.fa-check   - | #{translate("sync_to_github")} - - li - i.fa.fa-check   - |#{translate("compile_larger_projects")} - p.text-center.row-spaced-thin(ng-show="user.allowedFreeTrial" ng-controller="FreeTrialModalController") - a.btn.btn-success( - href - ng-class="buttonClass" - ng-click="startFreeTrial('projectMembers')" - ) #{translate("start_free_trial")} - - p.text-center.row-spaced-thin(ng-show="!user.allowedFreeTrial" ng-controller="UpgradeModalController") - a.btn.btn-success( - href - ng-class="buttonClass" - ng-click="upgradePlan('projectMembers')" - ) #{translate("upgrade")} - - p.small(ng-show="startedFreeTrial") - | #{translate("refresh_page_after_starting_free_trial")} - .row.public-access-level.public-access-level--notice(ng-show="!isAdmin") - .col-xs-12.text-center(ng-show="project.publicAccesLevel == 'private'") #{translate("to_add_more_collaborators")} - .col-xs-12.text-center(ng-show="project.publicAccesLevel == 'tokenBased'") #{translate("to_change_access_permissions")} - .modal-footer.modal-footer-share - .modal-footer-left - i.fa.fa-refresh.fa-spin(ng-show="state.inflight") - span.text-danger.error(ng-show="state.error") - span(ng-switch="state.errorReason") - span(ng-switch-when="cannot_invite_non_user") - | #{translate("cannot_invite_non_user")} - span(ng-switch-when="cannot_verify_user_not_robot") - | #{translate("cannot_verify_user_not_robot")} - span(ng-switch-when="cannot_invite_self") - | #{translate("cannot_invite_self")} - span(ng-switch-when="invalid_email") - | #{translate("invalid_email")} - span(ng-switch-default) - | #{translate("generic_something_went_wrong")} - .modal-footer-right - button.btn.btn-default( - ng-click="done()" - ) #{translate("close")} - -script(type="text/ng-template", id="shareTagTemplate") - .tag-template - span(ng-if="data.type") - i.fa.fa-fw(ng-class="{'fa-user': data.type != 'group', 'fa-group': data.type == 'group'}") - | - span {{$getDisplayText()}} - | - a(href, ng-click="$removeTag()").remove-button - i.fa.fa-fw.fa-close - -script(type="text/ng-template", id="shareAutocompleteTemplate") - .autocomplete-template - div(ng-if="data.type == 'user'") - i.fa.fa-fw.fa-user - | - span(ng-bind-html="$highlight(data.display)") - div(ng-if="data.type == 'group'") - i.fa.fa-fw.fa-group - | - span(ng-bind-html="$highlight(data.name)") - span.subdued.small(ng-show="data.member_count") ({{ data.member_count }} members) - -script(type="text/ng-template", id="ownershipTransferConfirmTemplate") - .modal-header - button.close( - type="button" - data-dismiss="modal" - ng-click="cancel()" - aria-label="Close" - ) - span(aria-hidden="true") × - h3 #{translate("change_project_owner")} - .modal-body - p !{translate('project_ownership_transfer_confirmation_1', { user: '{{ member.email }}', project: '{{ project.name }}' }, ['strong', 'strong'])} - p #{translate('project_ownership_transfer_confirmation_2')} - .modal-footer - .modal-footer-left - i.fa.fa-refresh.fa-spin(ng-show="state.inflight") - span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} - .modal-footer-right - button.btn.btn-default(ng-click="cancel()") #{translate("cancel")} - button.btn.btn-success(ng-click="confirm()") #{translate("change_owner")} diff --git a/frontend/js/base.js b/frontend/js/base.js index 2082f5e3ba..60f0b38da3 100644 --- a/frontend/js/base.js +++ b/frontend/js/base.js @@ -35,7 +35,6 @@ const App = angular 'ErrorCatcher', 'localStorage', 'sessionStorage', - 'ngTagsInput', 'ui.select', ]) .config(function ($qProvider, $httpProvider, uiSelectConfig) { diff --git a/frontend/js/ide.js b/frontend/js/ide.js index ebc808ae73..0da7f26f3c 100644 --- a/frontend/js/ide.js +++ b/frontend/js/ide.js @@ -35,7 +35,6 @@ import SafariScrollPatcher from './ide/SafariScrollPatcher' import { loadServiceWorker } from './ide/pdfng/directives/serviceWorkerManager' import './ide/cobranding/CobrandingDataService' import './ide/settings/index' -import './ide/share/index' import './ide/binary-files/index' import './ide/chat/index' import './ide/clone/index' @@ -64,6 +63,7 @@ import './main/system-messages' import '../../modules/modules-ide.js' import './shared/context/controllers/root-context-controller' import './features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller' +import './features/share-project-modal/controllers/react-share-project-modal-controller' import getMeta from './utils/meta' App.controller( diff --git a/frontend/js/ide/share/controllers/OwnershipTransferConfirmModalController.js b/frontend/js/ide/share/controllers/OwnershipTransferConfirmModalController.js deleted file mode 100644 index b5bdc4cc55..0000000000 --- a/frontend/js/ide/share/controllers/OwnershipTransferConfirmModalController.js +++ /dev/null @@ -1,35 +0,0 @@ -import App from '../../../base' -App.controller( - 'OwnershipTransferConfirmModalController', - function ($scope, $window, $modalInstance, projectMembers) { - $scope.state = { - inflight: false, - error: false, - } - - $scope.confirm = function () { - const userId = $scope.member._id - transferOwnership(userId) - } - - $scope.cancel = function () { - $modalInstance.dismiss() - } - - function transferOwnership(userId) { - $scope.state.inflight = true - $scope.state.error = false - projectMembers - .transferOwnership(userId) - .then(() => { - $scope.state.inflight = false - $scope.state.error = false - $window.location.reload() - }) - .catch(() => { - $scope.state.inflight = false - $scope.state.error = true - }) - } - } -) diff --git a/frontend/js/ide/share/controllers/ShareController.js b/frontend/js/ide/share/controllers/ShareController.js deleted file mode 100644 index b8f4b0b655..0000000000 --- a/frontend/js/ide/share/controllers/ShareController.js +++ /dev/null @@ -1,55 +0,0 @@ -import App from '../../../base' -App.controller('ShareController', function ( - $scope, - $modal, - ide, - projectInvites, - projectMembers, - // eslint-disable-next-line camelcase - eventTracking -) { - $scope.openShareProjectModal = function (isAdmin) { - $scope.isAdmin = isAdmin - eventTracking.sendMBOnce('ide-open-share-modal-once') - - $modal.open({ - templateUrl: 'shareProjectModalTemplate', - controller: 'ShareProjectModalController', - scope: $scope, - }) - } - - ide.socket.on('project:tokens:changed', data => { - if (data.tokens != null) { - ide.$scope.project.tokens = data.tokens - $scope.$digest() - } - }) - - ide.socket.on('project:membership:changed', data => { - if (data.members) { - projectMembers - .getMembers() - .then(response => { - if (response.data.members) { - $scope.project.members = response.data.members - } - }) - .catch(() => { - console.error('Error fetching members for project') - }) - } - if (data.invites) { - projectInvites - .getInvites() - .then(response => { - if (response.data.invites) { - $scope.project.invites = response.data.invites - } - }) - .catch(() => { - console.error('Error fetching invites for project') - }) - } - }) -}) diff --git a/frontend/js/ide/share/controllers/ShareProjectModalController.js b/frontend/js/ide/share/controllers/ShareProjectModalController.js deleted file mode 100644 index 944bfcc8c1..0000000000 --- a/frontend/js/ide/share/controllers/ShareProjectModalController.js +++ /dev/null @@ -1,309 +0,0 @@ -import _ from 'lodash' -import App from '../../../base' -App.controller('ShareProjectModalController', function ( - $scope, - $modalInstance, - $timeout, - projectMembers, - projectInvites, - $modal, - $http, - ide, - validateCaptcha, - validateCaptchaV3, - settings, - // eslint-disable-next-line camelcase - eventTracking -) { - $scope.inputs = { - privileges: 'readAndWrite', - contacts: [], - } - $scope.state = { - error: null, - errorReason: null, - inflight: false, - startedFreeTrial: false, - invites: [], - } - - $modalInstance.opened.then(() => - $timeout(() => $scope.$broadcast('open'), 200) - ) - - const INFINITE_COLLABORATORS = -1 - - $scope.refreshCanAddCollaborators = function () { - const allowedNoOfMembers = $scope.project.features.collaborators - $scope.canAddCollaborators = - $scope.project.members.length + $scope.project.invites.length < - allowedNoOfMembers || allowedNoOfMembers === INFINITE_COLLABORATORS - } - $scope.refreshCanAddCollaborators() - - $scope.$watch('canAddCollaborators', function () { - if (!$scope.canAddCollaborators) { - eventTracking.send( - 'subscription-funnel', - 'editor-click-feature', - 'projectMembers' - ) - } - }) - - $scope.$watch( - '(project.members.length + project.invites.length)', - _noOfMembers => $scope.refreshCanAddCollaborators() - ) - - $scope.autocompleteContacts = [] - if ($scope.isRestrictedTokenMember) { - // Restricted token members are users who join via a read-only link. - // They will not be able to invite any users, so skip the lookup of - // their contacts. This request would result in a 403 for anonymous - // users, which in turn would redirect them to the /login. - } else { - $http.get('/user/contacts').then(processContactsResponse) - } - - function processContactsResponse(response) { - const { data } = response - $scope.autocompleteContacts = data.contacts || [] - for (const contact of $scope.autocompleteContacts) { - if (contact.type === 'user') { - if ( - contact.first_name === contact.email.split('@')[0] && - !contact.last_name - ) { - // User has not set their proper name so use email as canonical display property - contact.display = contact.email - } else { - contact.name = `${contact.first_name} ${contact.last_name}` - contact.display = `${contact.name} <${contact.email}>` - } - } else { - // Must be a group - contact.display = contact.name - } - } - } - - const getCurrentMemberEmails = () => - ($scope.project.members || []).map(u => u.email) - - const getCurrentInviteEmails = () => - ($scope.project.invites || []).map(u => u.email) - - $scope.filterAutocompleteUsers = function ($query) { - const currentMemberEmails = getCurrentMemberEmails() - return $scope.autocompleteContacts.filter(function (contact) { - if ( - contact.email != null && - currentMemberEmails.includes(contact.email) - ) { - return false - } - for (const text of [contact.name, contact.email]) { - if ( - text != null && - text.toLowerCase().indexOf($query.toLowerCase()) > -1 - ) { - return true - } - } - return false - }) - } - - $scope.addMembers = function () { - const addMembers = function () { - if ($scope.inputs.contacts.length === 0) { - return - } - - const members = $scope.inputs.contacts - $scope.inputs.contacts = [] - $scope.clearError() - $scope.state.inflight = true - - if ($scope.project.invites == null) { - $scope.project.invites = [] - } - - const currentMemberEmails = getCurrentMemberEmails() - const currentInviteEmails = getCurrentInviteEmails() - addNextMember() - - function addNextMember() { - let email - if (members.length === 0 || !$scope.canAddCollaborators) { - $scope.state.inflight = false - $scope.$apply() - return - } - - const member = members.shift() - if (member.type === 'user') { - email = member.email - } else { - // Not an auto-complete object, so email == display - email = member.display - } - email = email.toLowerCase() - - if (currentMemberEmails.includes(email)) { - // Skip this existing member - return addNextMember() - } - // do v3 captcha to collect data only - validateCaptchaV3('invite') - // do v2 captcha - const ExposedSettings = window.ExposedSettings - validateCaptcha(function (response) { - $scope.grecaptchaResponse = response - const invites = $scope.project.invites || [] - const invite = _.find(invites, invite => invite.email === email) - let request - if (currentInviteEmails.includes(email) && invite) { - request = projectInvites.resendInvite(invite._id) - } else { - request = projectInvites.sendInvite( - email, - $scope.inputs.privileges, - $scope.grecaptchaResponse - ) - } - - request - .then(function (response) { - const { data } = response - if (data.error) { - $scope.setError(data.error) - $scope.state.inflight = false - } else { - if (data.invite) { - const { invite } = data - $scope.project.invites.push(invite) - } else { - const users = - data.users != null - ? data.users - : data.user != null - ? [data.user] - : [] - $scope.project.members.push(...users) - } - } - - setTimeout( - () => - // Give $scope a chance to update $scope.canAddCollaborators - // with new collaborator information. - addNextMember(), - - 0 - ) - }) - .catch(function (httpResponse) { - const { data } = httpResponse - $scope.state.inflight = false - $scope.setError(data.errorReason) - }) - }, ExposedSettings.recaptchaDisabled.invite) - } - } - - $timeout(addMembers, 50) // Give email list a chance to update - } - - $scope.removeMember = function (member) { - $scope.monitorRequest( - projectMembers.removeMember(member).then(function () { - const index = $scope.project.members.indexOf(member) - if (index === -1) { - return - } - $scope.project.members.splice(index, 1) - }) - ) - } - - $scope.revokeInvite = function (invite) { - $scope.monitorRequest( - projectInvites.revokeInvite(invite._id).then(function () { - const index = $scope.project.invites.indexOf(invite) - if (index === -1) { - return - } - $scope.project.invites.splice(index, 1) - }) - ) - } - - $scope.resendInvite = function (invite, event) { - $scope.monitorRequest( - projectInvites - .resendInvite(invite._id) - .then(function () { - event.target.blur() - }) - .catch(function () { - event.target.blur() - }) - ) - } - - $scope.makeTokenBased = function () { - $scope.project.publicAccesLevel = 'tokenBased' - settings.saveProjectAdminSettings({ publicAccessLevel: 'tokenBased' }) - eventTracking.sendMB('project-make-token-based') - } - - $scope.makePrivate = function () { - $scope.project.publicAccesLevel = 'private' - settings.saveProjectAdminSettings({ publicAccessLevel: 'private' }) - } - - $scope.$watch('project.tokens.readAndWrite', function (token) { - if (token != null) { - $scope.readAndWriteTokenLink = `${location.origin}/${token}` - } else { - $scope.readAndWriteTokenLink = null - } - }) - - $scope.$watch('project.tokens.readOnly', function (token) { - if (token != null) { - $scope.readOnlyTokenLink = `${location.origin}/read/${token}` - } else { - $scope.readOnlyTokenLink = null - } - }) - - $scope.done = () => $modalInstance.close() - - $scope.cancel = () => $modalInstance.dismiss() - - $scope.monitorRequest = function monitorRequest(request) { - $scope.clearError() - $scope.state.inflight = true - return request - .then(() => { - $scope.state.inflight = false - $scope.clearError() - }) - .catch(err => { - $scope.state.inflight = false - $scope.setError(err.data && err.data.error) - }) - } - - $scope.clearError = function clearError() { - $scope.state.error = false - } - - $scope.setError = function setError(reason) { - $scope.state.error = true - $scope.state.errorReason = reason - } -}) diff --git a/frontend/js/ide/share/controllers/ShareProjectModalMemberRowController.js b/frontend/js/ide/share/controllers/ShareProjectModalMemberRowController.js deleted file mode 100644 index 0741138b5f..0000000000 --- a/frontend/js/ide/share/controllers/ShareProjectModalMemberRowController.js +++ /dev/null @@ -1,46 +0,0 @@ -import App from '../../../base' -App.controller( - 'ShareProjectModalMemberRowController', - function ($scope, $modal, projectMembers) { - $scope.form = { - privileges: $scope.member.privileges, - - isModified() { - return this.privileges !== $scope.member.privileges - }, - - submit() { - const userId = $scope.member._id - const privilegeLevel = $scope.form.privileges - if (privilegeLevel === 'owner') { - openOwnershipTransferConfirmModal(userId) - } else { - setPrivilegeLevel(userId, privilegeLevel) - } - }, - - reset() { - this.privileges = $scope.member.privileges - $scope.clearError() - }, - } - - function setPrivilegeLevel(userId, privilegeLevel) { - $scope.monitorRequest( - projectMembers - .setMemberPrivilegeLevel(userId, privilegeLevel) - .then(() => { - $scope.member.privileges = privilegeLevel - }) - ) - } - - function openOwnershipTransferConfirmModal(userId) { - $modal.open({ - templateUrl: 'ownershipTransferConfirmTemplate', - controller: 'OwnershipTransferConfirmModalController', - scope: $scope, - }) - } - } -) diff --git a/frontend/js/ide/share/index.js b/frontend/js/ide/share/index.js deleted file mode 100644 index 6ced21a543..0000000000 --- a/frontend/js/ide/share/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import './controllers/ShareController' -import './controllers/ShareProjectModalController' -import './controllers/ShareProjectModalMemberRowController' -import './controllers/OwnershipTransferConfirmModalController' -import './services/projectMembers' -import './services/projectInvites' -import '../../features/share-project-modal/controllers/react-share-project-modal-controller' diff --git a/frontend/js/ide/share/services/projectInvites.js b/frontend/js/ide/share/services/projectInvites.js deleted file mode 100644 index 041905f2fa..0000000000 --- a/frontend/js/ide/share/services/projectInvites.js +++ /dev/null @@ -1,37 +0,0 @@ -import App from '../../../base' - -export default App.factory('projectInvites', (ide, $http) => ({ - sendInvite(email, privileges, grecaptchaResponse) { - return $http.post(`/project/${ide.project_id}/invite`, { - email, - privileges, - _csrf: window.csrfToken, - 'g-recaptcha-response': grecaptchaResponse, - }) - }, - - revokeInvite(inviteId) { - return $http({ - url: `/project/${ide.project_id}/invite/${inviteId}`, - method: 'DELETE', - headers: { - 'X-Csrf-Token': window.csrfToken, - }, - }) - }, - - resendInvite(inviteId, privileges) { - return $http.post(`/project/${ide.project_id}/invite/${inviteId}/resend`, { - _csrf: window.csrfToken, - }) - }, - - getInvites() { - return $http.get(`/project/${ide.project_id}/invites`, { - json: true, - headers: { - 'X-Csrf-Token': window.csrfToken, - }, - }) - }, -})) diff --git a/frontend/js/ide/share/services/projectMembers.js b/frontend/js/ide/share/services/projectMembers.js deleted file mode 100644 index 5cdd524dd7..0000000000 --- a/frontend/js/ide/share/services/projectMembers.js +++ /dev/null @@ -1,48 +0,0 @@ -import App from '../../../base' -App.factory('projectMembers', (ide, $http) => ({ - removeMember(member) { - return $http({ - url: `/project/${ide.project_id}/users/${member._id}`, - method: 'DELETE', - headers: { - 'X-Csrf-Token': window.csrfToken, - }, - }) - }, - - addGroup(groupId, privileges) { - return $http.post(`/project/${ide.project_id}/group`, { - group_id: groupId, - privileges, - _csrf: window.csrfToken, - }) - }, - - getMembers() { - return $http.get(`/project/${ide.project_id}/members`, { - json: true, - headers: { - 'X-Csrf-Token': window.csrfToken, - }, - }) - }, - - setMemberPrivilegeLevel(userId, privilegeLevel) { - return $http.put( - `/project/${ide.project_id}/users/${userId}`, - { privilegeLevel }, - { - headers: { - 'X-Csrf-Token': window.csrfToken, - }, - } - ) - }, - - transferOwnership(userId) { - return $http.post(`/project/${ide.project_id}/transfer-ownership`, { - user_id: userId, - _csrf: window.csrfToken, - }) - }, -})) diff --git a/frontend/js/libraries.js b/frontend/js/libraries.js index 4a931c6ecd..87ac1df89c 100644 --- a/frontend/js/libraries.js +++ b/frontend/js/libraries.js @@ -8,7 +8,6 @@ import 'libs/ng-context-menu-0.1.4' import 'libs/jquery.storage' import 'libs/angular-cookie' import 'libs/passfield' -import 'libs/ng-tags-input-3.0.0' import 'libs/select/select' // CSS diff --git a/frontend/js/vendor/libs/ng-tags-input-3.0.0.js b/frontend/js/vendor/libs/ng-tags-input-3.0.0.js deleted file mode 100644 index fb5f2d7fb4..0000000000 --- a/frontend/js/vendor/libs/ng-tags-input-3.0.0.js +++ /dev/null @@ -1,1151 +0,0 @@ -/*! - * ngTagsInput v3.0.0 - * http://mbenford.github.io/ngTagsInput - * - * Copyright (c) 2013-2015 Michael Benford - * License: MIT - * - * Generated at 2015-07-13 02:08:11 -0300 - */ -(function() { -'use strict'; - -var KEYS = { - backspace: 8, - tab: 9, - enter: 13, - escape: 27, - space: 32, - up: 38, - down: 40, - left: 37, - right: 39, - delete: 46, - comma: 188 -}; - -var MAX_SAFE_INTEGER = 9007199254740991; -var SUPPORTED_INPUT_TYPES = ['text', 'email', 'url']; - -var tagsInput = angular.module('ngTagsInput', []); - -/** - * @ngdoc directive - * @name tagsInput - * @module ngTagsInput - * - * @description - * Renders an input box with tag editing support. - * - * @param {string} ngModel Assignable Angular expression to data-bind to. - * @param {string=} [template=NA] URL or id of a custom template for rendering each tag. - * @param {string=} [displayProperty=text] Property to be rendered as the tag label. - * @param {string=} [keyProperty=text] Property to be used as a unique identifier for the tag. - * @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values. - * @param {string=} [text=NA] Assignable Angular expression for data-binding to the element's text. - * @param {number=} tabindex Tab order of the control. - * @param {string=} [placeholder=Add a tag] Placeholder text for the control. - * @param {number=} [minLength=3] Minimum length for a new tag. - * @param {number=} [maxLength=MAX_SAFE_INTEGER] Maximum length allowed for a new tag. - * @param {number=} [minTags=0] Sets minTags validation error key if the number of tags added is less than minTags. - * @param {number=} [maxTags=MAX_SAFE_INTEGER] Sets maxTags validation error key if the number of tags added is greater - * than maxTags. - * @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in - * the input element when the directive loses focus. - * @param {string=} [removeTagSymbol=×] (Obsolete) Symbol character for the remove tag button. - * @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key. - * @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key. - * @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key. - * @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus. - * @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags. - * @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags. - * @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes. - * @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid. - * @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into the new tag - * input box instead of being removed when the backspace key is pressed and the input box is empty. - * @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list - * will be allowed. When this flag is true, addOnEnter, addOnComma, addOnSpace and addOnBlur values are ignored. - * @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not. - * @param {expression=} [onTagAdding=NA] Expression to evaluate that will be invoked before adding a new tag. The new - * tag is available as $tag. This method must return either true or false. If false, the tag will not be added. - * @param {expression=} [onTagAdded=NA] Expression to evaluate upon adding a new tag. The new tag is available as $tag. - * @param {expression=} [onInvalidTag=NA] Expression to evaluate when a tag is invalid. The invalid tag is available as $tag. - * @param {expression=} [onTagRemoving=NA] Expression to evaluate that will be invoked before removing a tag. The tag - * is available as $tag. This method must return either true or false. If false, the tag will not be removed. - * @param {expression=} [onTagRemoved=NA] Expression to evaluate upon removing an existing tag. The removed tag is - * available as $tag. - * @param {expression=} [onTagClicked=NA] Expression to evaluate upon clicking an existing tag. The clicked tag is available as $tag. - */ -tagsInput.directive('tagsInput', ["$timeout", "$document", "$window", "tagsInputConfig", "tiUtil", function($timeout, $document, $window, tagsInputConfig, tiUtil) { - function TagList(options, events, onTagAdding, onTagRemoving) { - var self = {}, getTagText, setTagText, tagIsValid; - - getTagText = function(tag) { - return tiUtil.safeToString(tag[options.displayProperty]); - }; - - setTagText = function(tag, text) { - tag[options.displayProperty] = text; - }; - - tagIsValid = function(tag) { - var tagText = getTagText(tag); - - return tagText && - tagText.length >= options.minLength && - tagText.length <= options.maxLength && - options.allowedTagsPattern.test(tagText) && - !tiUtil.findInObjectArray(self.items, tag, options.keyProperty || options.displayProperty) && - onTagAdding({ $tag: tag }); - }; - - self.items = []; - - self.addText = function(text) { - var tag = {}; - setTagText(tag, text); - return self.add(tag); - }; - - self.add = function(tag) { - var tagText = getTagText(tag); - - if (options.replaceSpacesWithDashes) { - tagText = tiUtil.replaceSpacesWithDashes(tagText); - } - - setTagText(tag, tagText); - - if (tagIsValid(tag)) { - self.items.push(tag); - events.trigger('tag-added', { $tag: tag }); - } - else if (tagText) { - events.trigger('invalid-tag', { $tag: tag }); - } - - return tag; - }; - - self.remove = function(index) { - var tag = self.items[index]; - - if (onTagRemoving({ $tag: tag })) { - self.items.splice(index, 1); - self.clearSelection(); - events.trigger('tag-removed', { $tag: tag }); - return tag; - } - }; - - self.select = function(index) { - if (index < 0) { - index = self.items.length - 1; - } - else if (index >= self.items.length) { - index = 0; - } - - self.index = index; - self.selected = self.items[index]; - }; - - self.selectPrior = function() { - self.select(--self.index); - }; - - self.selectNext = function() { - self.select(++self.index); - }; - - self.removeSelected = function() { - return self.remove(self.index); - }; - - self.clearSelection = function() { - self.selected = null; - self.index = -1; - }; - - self.clearSelection(); - - return self; - } - - function validateType(type) { - return SUPPORTED_INPUT_TYPES.indexOf(type) !== -1; - } - - return { - restrict: 'E', - require: 'ngModel', - scope: { - tags: '=ngModel', - text: '=?', - onTagAdding: '&', - onTagAdded: '&', - onInvalidTag: '&', - onTagRemoving: '&', - onTagRemoved: '&', - onTagClicked: '&' - }, - replace: false, - transclude: true, - templateUrl: 'ngTagsInput/tags-input.html', - controller: ["$scope", "$attrs", "$element", function($scope, $attrs, $element) { - $scope.events = tiUtil.simplePubSub(); - - tagsInputConfig.load('tagsInput', $scope, $attrs, { - template: [String, 'ngTagsInput/tag-item.html'], - type: [String, 'text', validateType], - placeholder: [String, 'Add a tag'], - tabindex: [Number, null], - removeTagSymbol: [String, String.fromCharCode(215)], - replaceSpacesWithDashes: [Boolean, true], - minLength: [Number, 3], - maxLength: [Number, MAX_SAFE_INTEGER], - addOnEnter: [Boolean, true], - addOnSpace: [Boolean, false], - addOnComma: [Boolean, true], - addOnBlur: [Boolean, true], - addOnPaste: [Boolean, false], - pasteSplitPattern: [RegExp, /,/], - allowedTagsPattern: [RegExp, /.+/], - enableEditingLastTag: [Boolean, false], - minTags: [Number, 0], - maxTags: [Number, MAX_SAFE_INTEGER], - displayProperty: [String, 'text'], - keyProperty: [String, ''], - allowLeftoverText: [Boolean, false], - addFromAutocompleteOnly: [Boolean, false], - spellcheck: [Boolean, true] - }); - - $scope.tagList = new TagList($scope.options, $scope.events, - tiUtil.handleUndefinedResult($scope.onTagAdding, true), - tiUtil.handleUndefinedResult($scope.onTagRemoving, true)); - - this.registerAutocomplete = function() { - var input = $element.find('input'); - - return { - addTag: function(tag) { - return $scope.tagList.add(tag); - }, - focusInput: function() { - input[0].focus(); - }, - getTags: function() { - return $scope.tagList.items; - }, - getCurrentTagText: function() { - return $scope.newTag.text(); - }, - getOptions: function() { - return $scope.options; - }, - on: function(name, handler) { - $scope.events.on(name, handler); - return this; - } - }; - }; - - this.registerTagItem = function() { - return { - getOptions: function() { - return $scope.options; - }, - removeTag: function(index) { - if ($scope.disabled) { - return; - } - $scope.tagList.remove(index); - } - }; - }; - }], - link: function(scope, element, attrs, ngModelCtrl) { - var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right], - tagList = scope.tagList, - events = scope.events, - options = scope.options, - input = element.find('input'), - validationOptions = ['minTags', 'maxTags', 'allowLeftoverText'], - setElementValidity; - - setElementValidity = function() { - ngModelCtrl.$setValidity('maxTags', tagList.items.length <= options.maxTags); - ngModelCtrl.$setValidity('minTags', tagList.items.length >= options.minTags); - ngModelCtrl.$setValidity('leftoverText', scope.hasFocus || options.allowLeftoverText ? true : !scope.newTag.text()); - }; - - ngModelCtrl.$isEmpty = function(value) { - return !value || !value.length; - }; - - scope.newTag = { - text: function(value) { - if (angular.isDefined(value)) { - scope.text = value; - events.trigger('input-change', value); - } - else { - return scope.text || ''; - } - }, - invalid: null - }; - - scope.track = function(tag) { - return tag[options.keyProperty || options.displayProperty]; - }; - - scope.$watch('tags', function(value) { - if (value) { - tagList.items = tiUtil.makeObjectArray(value, options.displayProperty); - scope.tags = tagList.items; - } - else { - tagList.items = []; - } - }); - - scope.$watch('tags.length', function() { - setElementValidity(); - - // ngModelController won't trigger validators when the model changes (because it's an array), - // so we need to do it ourselves. Unfortunately this won't trigger any registered formatter. - ngModelCtrl.$validate(); - }); - - attrs.$observe('disabled', function(value) { - scope.disabled = value; - }); - - scope.eventHandlers = { - input: { - keydown: function($event) { - events.trigger('input-keydown', $event); - }, - focus: function() { - if (scope.hasFocus) { - return; - } - - scope.hasFocus = true; - events.trigger('input-focus'); - }, - blur: function() { - $timeout(function() { - var activeElement = $document.prop('activeElement'), - lostFocusToBrowserWindow = activeElement === input[0], - lostFocusToChildElement = element[0].contains(activeElement); - - if (lostFocusToBrowserWindow || !lostFocusToChildElement) { - scope.hasFocus = false; - events.trigger('input-blur'); - } - }); - }, - paste: function($event) { - $event.getTextData = function() { - var clipboardData = $event.clipboardData || ($event.originalEvent && $event.originalEvent.clipboardData); - return clipboardData ? clipboardData.getData('text/plain') : $window.clipboardData.getData('Text'); - }; - events.trigger('input-paste', $event); - } - }, - host: { - click: function() { - if (scope.disabled) { - return; - } - input[0].focus(); - } - }, - tag: { - click: function(tag) { - events.trigger('tag-clicked', { $tag: tag }); - } - } - }; - - events - .on('tag-added', scope.onTagAdded) - .on('invalid-tag', scope.onInvalidTag) - .on('tag-removed', scope.onTagRemoved) - .on('tag-clicked', scope.onTagClicked) - .on('tag-added', function() { - scope.newTag.text(''); - }) - .on('tag-added tag-removed', function() { - scope.tags = tagList.items; - // Ideally we should be able call $setViewValue here and let it in turn call $setDirty and $validate - // automatically, but since the model is an array, $setViewValue does nothing and it's up to us to do it. - // Unfortunately this won't trigger any registered $parser and there's no safe way to do it. - ngModelCtrl.$setDirty(); - }) - .on('invalid-tag', function() { - scope.newTag.invalid = true; - }) - .on('option-change', function(e) { - if (validationOptions.indexOf(e.name) !== -1) { - setElementValidity(); - } - }) - .on('input-change', function() { - tagList.clearSelection(); - scope.newTag.invalid = null; - }) - .on('input-focus', function() { - element.triggerHandler('focus'); - ngModelCtrl.$setValidity('leftoverText', true); - }) - .on('input-blur', function() { - if (options.addOnBlur && !options.addFromAutocompleteOnly) { - tagList.addText(scope.newTag.text()); - } - element.triggerHandler('blur'); - setElementValidity(); - }) - .on('input-keydown', function(event) { - var key = event.keyCode, - addKeys = {}, - shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag; - - if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) { - return; - } - - addKeys[KEYS.enter] = options.addOnEnter; - addKeys[KEYS.comma] = options.addOnComma; - addKeys[KEYS.space] = options.addOnSpace; - - shouldAdd = !options.addFromAutocompleteOnly && addKeys[key]; - shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected; - shouldEditLastTag = key === KEYS.backspace && scope.newTag.text().length === 0 && options.enableEditingLastTag; - shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text().length === 0 && !options.enableEditingLastTag; - - if (shouldAdd) { - tagList.addText(scope.newTag.text()); - } - else if (shouldEditLastTag) { - var tag; - - tagList.selectPrior(); - tag = tagList.removeSelected(); - - if (tag) { - scope.newTag.text(tag[options.displayProperty]); - } - } - else if (shouldRemove) { - tagList.removeSelected(); - } - else if (shouldSelect) { - if (key === KEYS.left || key === KEYS.backspace) { - tagList.selectPrior(); - } - else if (key === KEYS.right) { - tagList.selectNext(); - } - } - - if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) { - event.preventDefault(); - } - }) - .on('input-paste', function(event) { - if (options.addOnPaste) { - var data = event.getTextData(); - var tags = data.split(options.pasteSplitPattern); - - if (tags.length > 1) { - tags.forEach(function(tag) { - tagList.addText(tag); - }); - event.preventDefault(); - } - } - }); - } - }; -}]); - - -/** - * @ngdoc directive - * @name tiTagItem - * @module ngTagsInput - * - * @description - * Represents a tag item. Used internally by the tagsInput directive. - */ -tagsInput.directive('tiTagItem', ["tiUtil", function(tiUtil) { - return { - restrict: 'E', - require: '^tagsInput', - template: '', - scope: { data: '=' }, - link: function(scope, element, attrs, tagsInputCtrl) { - var tagsInput = tagsInputCtrl.registerTagItem(), - options = tagsInput.getOptions(); - - scope.$$template = options.template; - scope.$$removeTagSymbol = options.removeTagSymbol; - - scope.$getDisplayText = function() { - return tiUtil.safeToString(scope.data[options.displayProperty]); - }; - scope.$removeTag = function() { - tagsInput.removeTag(scope.$index); - }; - - scope.$watch('$parent.$index', function(value) { - scope.$index = value; - }); - } - }; -}]); - - -/** - * @ngdoc directive - * @name autoComplete - * @module ngTagsInput - * - * @description - * Provides autocomplete support for the tagsInput directive. - * - * @param {expression} source Expression to evaluate upon changing the input content. The input value is available as - * $query. The result of the expression must be a promise that eventually resolves to an array of strings. - * @param {string=} [template=NA] URL or id of a custom template for rendering each element of the autocomplete list. - * @param {string=} [displayProperty=tagsInput.displayText] Property to be rendered as the autocomplete label. - * @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait before evaluating the expression in - * the source option after the last keystroke. - * @param {number=} [minLength=3] Minimum number of characters that must be entered before evaluating the expression - * in the source option. - * @param {boolean=} [highlightMatchedText=true] Flag indicating that the matched text will be highlighted in the - * suggestions list. - * @param {number=} [maxResultsToShow=10] Maximum number of results to be displayed at a time. - * @param {boolean=} [loadOnDownArrow=false] Flag indicating that the source option will be evaluated when the down arrow - * key is pressed and the suggestion list is closed. The current input value is available as $query. - * @param {boolean=} [loadOnEmpty=false] Flag indicating that the source option will be evaluated when the input content - * becomes empty. The $query variable will be passed to the expression as an empty string. - * @param {boolean=} [loadOnFocus=false] Flag indicating that the source option will be evaluated when the input element - * gains focus. The current input value is available as $query. - * @param {boolean=} [selectFirstMatch=true] Flag indicating that the first match will be automatically selected once - * the suggestion list is shown. - */ -tagsInput.directive('autoComplete', ["$document", "$timeout", "$sce", "$q", "tagsInputConfig", "tiUtil", function($document, $timeout, $sce, $q, tagsInputConfig, tiUtil) { - function SuggestionList(loadFn, options, events) { - var self = {}, getDifference, lastPromise, getTagId; - - getTagId = function() { - return options.tagsInput.keyProperty || options.tagsInput.displayProperty; - }; - - getDifference = function(array1, array2) { - return array1.filter(function(item) { - return !tiUtil.findInObjectArray(array2, item, getTagId(), function(a, b) { - if (options.tagsInput.replaceSpacesWithDashes) { - a = tiUtil.replaceSpacesWithDashes(a); - b = tiUtil.replaceSpacesWithDashes(b); - } - return tiUtil.defaultComparer(a, b); - }); - }); - }; - - self.reset = function() { - lastPromise = null; - - self.items = []; - self.visible = false; - self.index = -1; - self.selected = null; - self.query = null; - }; - self.show = function() { - if (options.selectFirstMatch) { - self.select(0); - } - else { - self.selected = null; - } - self.visible = true; - }; - self.load = tiUtil.debounce(function(query, tags) { - self.query = query; - - var promise = $q.when(loadFn({ $query: query })); - lastPromise = promise; - - promise.then(function(items) { - if (promise !== lastPromise) { - return; - } - - items = tiUtil.makeObjectArray(items.data || items, getTagId()); - items = getDifference(items, tags); - self.items = items.slice(0, options.maxResultsToShow); - - if (self.items.length > 0) { - self.show(); - } - else { - self.reset(); - } - }); - }, options.debounceDelay); - - self.selectNext = function() { - self.select(++self.index); - }; - self.selectPrior = function() { - self.select(--self.index); - }; - self.select = function(index) { - if (index < 0) { - index = self.items.length - 1; - } - else if (index >= self.items.length) { - index = 0; - } - self.index = index; - self.selected = self.items[index]; - events.trigger('suggestion-selected', index); - }; - - self.reset(); - - return self; - } - - function scrollToElement(root, index) { - var element = root.find('li').eq(index), - parent = element.parent(), - elementTop = element.prop('offsetTop'), - elementHeight = element.prop('offsetHeight'), - parentHeight = parent.prop('clientHeight'), - parentScrollTop = parent.prop('scrollTop'); - - if (elementTop < parentScrollTop) { - parent.prop('scrollTop', elementTop); - } - else if (elementTop + elementHeight > parentHeight + parentScrollTop) { - parent.prop('scrollTop', elementTop + elementHeight - parentHeight); - } - } - - return { - restrict: 'E', - require: '^tagsInput', - scope: { source: '&' }, - templateUrl: 'ngTagsInput/auto-complete.html', - controller: ["$scope", "$element", "$attrs", function($scope, $element, $attrs) { - $scope.events = tiUtil.simplePubSub(); - - tagsInputConfig.load('autoComplete', $scope, $attrs, { - template: [String, 'ngTagsInput/auto-complete-match.html'], - debounceDelay: [Number, 100], - minLength: [Number, 3], - highlightMatchedText: [Boolean, true], - maxResultsToShow: [Number, 10], - loadOnDownArrow: [Boolean, false], - loadOnEmpty: [Boolean, false], - loadOnFocus: [Boolean, false], - selectFirstMatch: [Boolean, true], - displayProperty: [String, ''] - }); - - $scope.suggestionList = new SuggestionList($scope.source, $scope.options, $scope.events); - - this.registerAutocompleteMatch = function() { - return { - getOptions: function() { - return $scope.options; - }, - getQuery: function() { - return $scope.suggestionList.query; - } - }; - }; - }], - link: function(scope, element, attrs, tagsInputCtrl) { - var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down], - suggestionList = scope.suggestionList, - tagsInput = tagsInputCtrl.registerAutocomplete(), - options = scope.options, - events = scope.events, - shouldLoadSuggestions; - - options.tagsInput = tagsInput.getOptions(); - - shouldLoadSuggestions = function(value) { - return value && value.length >= options.minLength || !value && options.loadOnEmpty; - }; - - scope.addSuggestionByIndex = function(index) { - suggestionList.select(index); - scope.addSuggestion(); - }; - - scope.addSuggestion = function() { - var added = false; - - if (suggestionList.selected) { - tagsInput.addTag(angular.copy(suggestionList.selected)); - suggestionList.reset(); - tagsInput.focusInput(); - - added = true; - } - return added; - }; - - scope.track = function(item) { - return item[options.tagsInput.keyProperty || options.tagsInput.displayProperty]; - }; - - tagsInput - .on('tag-added tag-removed invalid-tag input-blur', function() { - suggestionList.reset(); - }) - .on('input-change', function(value) { - if (shouldLoadSuggestions(value)) { - suggestionList.load(value, tagsInput.getTags()); - } - else { - suggestionList.reset(); - } - }) - .on('input-focus', function() { - var value = tagsInput.getCurrentTagText(); - if (options.loadOnFocus && shouldLoadSuggestions(value)) { - suggestionList.load(value, tagsInput.getTags()); - } - }) - .on('input-keydown', function(event) { - var key = event.keyCode, - handled = false; - - if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) { - return; - } - - if (suggestionList.visible) { - - if (key === KEYS.down) { - suggestionList.selectNext(); - handled = true; - } - else if (key === KEYS.up) { - suggestionList.selectPrior(); - handled = true; - } - else if (key === KEYS.escape) { - suggestionList.reset(); - handled = true; - } - else if (key === KEYS.enter || key === KEYS.tab) { - handled = scope.addSuggestion(); - } - } - else { - if (key === KEYS.down && scope.options.loadOnDownArrow) { - suggestionList.load(tagsInput.getCurrentTagText(), tagsInput.getTags()); - handled = true; - } - } - - if (handled) { - event.preventDefault(); - event.stopImmediatePropagation(); - return false; - } - }); - - events.on('suggestion-selected', function(index) { - scrollToElement(element, index); - }); - } - }; -}]); - - -/** - * @ngdoc directive - * @name tiAutocompleteMatch - * @module ngTagsInput - * - * @description - * Represents an autocomplete match. Used internally by the autoComplete directive. - */ -tagsInput.directive('tiAutocompleteMatch', ["$sce", "tiUtil", function($sce, tiUtil) { - return { - restrict: 'E', - require: '^autoComplete', - template: '', - scope: { data: '=' }, - link: function(scope, element, attrs, autoCompleteCtrl) { - var autoComplete = autoCompleteCtrl.registerAutocompleteMatch(), - options = autoComplete.getOptions(); - - scope.$$template = options.template; - scope.$index = scope.$parent.$index; - - scope.$highlight = function(text) { - if (options.highlightMatchedText) { - text = tiUtil.safeHighlight(text, autoComplete.getQuery()); - } - return $sce.trustAsHtml(text); - }; - scope.$getDisplayText = function() { - return tiUtil.safeToString(scope.data[options.displayProperty || options.tagsInput.displayProperty]); - }; - } - }; -}]); - - -/** - * @ngdoc directive - * @name tiTranscludeAppend - * @module ngTagsInput - * - * @description - * Re-creates the old behavior of ng-transclude. Used internally by tagsInput directive. - */ -tagsInput.directive('tiTranscludeAppend', function() { - return function(scope, element, attrs, ctrl, transcludeFn) { - transcludeFn(function(clone) { - element.append(clone); - }); - }; -}); - -/** - * @ngdoc directive - * @name tiAutosize - * @module ngTagsInput - * - * @description - * Automatically sets the input's width so its content is always visible. Used internally by tagsInput directive. - */ -tagsInput.directive('tiAutosize', ["tagsInputConfig", function(tagsInputConfig) { - return { - restrict: 'A', - require: 'ngModel', - link: function(scope, element, attrs, ctrl) { - var threshold = tagsInputConfig.getTextAutosizeThreshold(), - span, resize; - - span = angular.element(''); - span.css('display', 'none') - .css('visibility', 'hidden') - .css('width', 'auto') - .css('white-space', 'pre'); - - element.parent().append(span); - - resize = function(originalValue) { - var value = originalValue, width; - - if (angular.isString(value) && value.length === 0) { - value = attrs.placeholder; - } - - if (value) { - span.text(value); - span.css('display', ''); - width = span.prop('offsetWidth'); - span.css('display', 'none'); - } - - element.css('width', width ? width + threshold + 'px' : ''); - - return originalValue; - }; - - ctrl.$parsers.unshift(resize); - ctrl.$formatters.unshift(resize); - - attrs.$observe('placeholder', function(value) { - if (!ctrl.$modelValue) { - resize(value); - } - }); - } - }; -}]); - -/** - * @ngdoc directive - * @name tiBindAttrs - * @module ngTagsInput - * - * @description - * Binds attributes to expressions. Used internally by tagsInput directive. - */ -tagsInput.directive('tiBindAttrs', function() { - return function(scope, element, attrs) { - scope.$watch(attrs.tiBindAttrs, function(value) { - angular.forEach(value, function(value, key) { - attrs.$set(key, value); - }); - }, true); - }; -}); - -/** - * @ngdoc service - * @name tagsInputConfig - * @module ngTagsInput - * - * @description - * Sets global configuration settings for both tagsInput and autoComplete directives. It's also used internally to parse and - * initialize options from HTML attributes. - */ -tagsInput.provider('tagsInputConfig', function() { - var globalDefaults = {}, - interpolationStatus = {}, - autosizeThreshold = 3; - - /** - * @ngdoc method - * @name tagsInputConfig#setDefaults - * @description Sets the default configuration option for a directive. - * - * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'. - * @param {object} defaults Object containing options and their values. - * - * @returns {object} The service itself for chaining purposes. - */ - this.setDefaults = function(directive, defaults) { - globalDefaults[directive] = defaults; - return this; - }; - - /** - * @ngdoc method - * @name tagsInputConfig#setActiveInterpolation - * @description Sets active interpolation for a set of options. - * - * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'. - * @param {object} options Object containing which options should have interpolation turned on at all times. - * - * @returns {object} The service itself for chaining purposes. - */ - this.setActiveInterpolation = function(directive, options) { - interpolationStatus[directive] = options; - return this; - }; - - /** - * @ngdoc method - * @name tagsInputConfig#setTextAutosizeThreshold - * @description Sets the threshold used by the tagsInput directive to re-size the inner input field element based on its contents. - * - * @param {number} threshold Threshold value, in pixels. - * - * @returns {object} The service itself for chaining purposes. - */ - this.setTextAutosizeThreshold = function(threshold) { - autosizeThreshold = threshold; - return this; - }; - - this.$get = ["$interpolate", function($interpolate) { - var converters = {}; - converters[String] = function(value) { return value; }; - converters[Number] = function(value) { return parseInt(value, 10); }; - converters[Boolean] = function(value) { return value.toLowerCase() === 'true'; }; - converters[RegExp] = function(value) { return new RegExp(value); }; - - return { - load: function(directive, scope, attrs, options) { - var defaultValidator = function() { return true; }; - - scope.options = {}; - - angular.forEach(options, function(value, key) { - var type, localDefault, validator, converter, getDefault, updateValue; - - type = value[0]; - localDefault = value[1]; - validator = value[2] || defaultValidator; - converter = converters[type]; - - getDefault = function() { - var globalValue = globalDefaults[directive] && globalDefaults[directive][key]; - return angular.isDefined(globalValue) ? globalValue : localDefault; - }; - - updateValue = function(value) { - scope.options[key] = value && validator(value) ? converter(value) : getDefault(); - }; - - if (interpolationStatus[directive] && interpolationStatus[directive][key]) { - attrs.$observe(key, function(value) { - updateValue(value); - scope.events.trigger('option-change', { name: key, newValue: value }); - }); - } - else { - updateValue(attrs[key] && $interpolate(attrs[key])(scope.$parent)); - } - }); - }, - getTextAutosizeThreshold: function() { - return autosizeThreshold; - } - }; - }]; -}); - - -/*** - * @ngdoc service - * @name tiUtil - * @module ngTagsInput - * - * @description - * Helper methods used internally by the directive. Should not be called directly from user code. - */ -tagsInput.factory('tiUtil', ["$timeout", function($timeout) { - var self = {}; - - self.debounce = function(fn, delay) { - var timeoutId; - return function() { - var args = arguments; - $timeout.cancel(timeoutId); - timeoutId = $timeout(function() { fn.apply(null, args); }, delay); - }; - }; - - self.makeObjectArray = function(array, key) { - if (!angular.isArray(array) || array.length === 0 || angular.isObject(array[0])) { - return array; - } - - var newArray = []; - array.forEach(function(item) { - var obj = {}; - obj[key] = item; - newArray.push(obj); - }); - return newArray; - }; - - self.findInObjectArray = function(array, obj, key, comparer) { - var item = null; - comparer = comparer || self.defaultComparer; - - array.some(function(element) { - if (comparer(element[key], obj[key])) { - item = element; - return true; - } - }); - - return item; - }; - - self.defaultComparer = function(a, b) { - // I'm aware of the internationalization issues regarding toLowerCase() - // but I couldn't come up with a better solution right now - return self.safeToString(a).toLowerCase() === self.safeToString(b).toLowerCase(); - }; - - self.safeHighlight = function(str, value) { - if (!value) { - return str; - } - - function escapeRegexChars(str) { - return str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); - } - - str = self.encodeHTML(str); - value = self.encodeHTML(value); - - var expression = new RegExp('&[^;]+;|' + escapeRegexChars(value), 'gi'); - return str.replace(expression, function(match) { - return match.toLowerCase() === value.toLowerCase() ? '' + match + '' : match; - }); - }; - - self.safeToString = function(value) { - return angular.isUndefined(value) || value == null ? '' : value.toString().trim(); - }; - - self.encodeHTML = function(value) { - return self.safeToString(value) - .replace(/&/g, '&') - .replace(//g, '>'); - }; - - self.handleUndefinedResult = function(fn, valueIfUndefined) { - return function() { - var result = fn.apply(null, arguments); - return angular.isUndefined(result) ? valueIfUndefined : result; - }; - }; - - self.replaceSpacesWithDashes = function(str) { - return self.safeToString(str).replace(/\s/g, '-'); - }; - - self.isModifierOn = function(event) { - return event.shiftKey || event.ctrlKey || event.altKey || event.metaKey; - }; - - self.simplePubSub = function() { - var events = {}; - return { - on: function(names, handler) { - names.split(' ').forEach(function(name) { - if (!events[name]) { - events[name] = []; - } - events[name].push(handler); - }); - return this; - }, - trigger: function(name, args) { - var handlers = events[name] || []; - handlers.every(function(handler) { - return self.handleUndefinedResult(handler, true)(args); - }); - return this; - } - }; - }; - - return self; -}]); - -/* HTML templates */ -tagsInput.run(["$templateCache", function($templateCache) { - $templateCache.put('ngTagsInput/tags-input.html', - "
" - ); - - $templateCache.put('ngTagsInput/tag-item.html', - " " - ); - - $templateCache.put('ngTagsInput/auto-complete.html', - "
" - ); - - $templateCache.put('ngTagsInput/auto-complete-match.html', - "" - ); -}]); - -}()); \ No newline at end of file