From e0324b7e35af436e037465eb8f8fe4429eb765a6 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Mon, 25 Apr 2016 15:14:22 +0200 Subject: [PATCH] add multiple option to userOrgSelector --- .../app/controllers/layout_ref.coffee | 16 + .../user_organization_autocompletion.coffee | 297 ++++++++++-------- .../app/views/generic/token.jst.eco | 4 + .../views/generic/user_search/input.jst.eco | 6 +- .../app/views/layout_ref/inputs.jst.eco | 16 +- app/assets/stylesheets/application.css | 1 - app/assets/stylesheets/zammad.scss | 52 ++- 7 files changed, 247 insertions(+), 145 deletions(-) create mode 100644 app/assets/javascripts/app/views/generic/token.jst.eco diff --git a/app/assets/javascripts/app/controllers/layout_ref.coffee b/app/assets/javascripts/app/controllers/layout_ref.coffee index 7b7cd8435..8813525e1 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.coffee @@ -1488,6 +1488,22 @@ class InputsRef extends App.ControllerContent @$('.searchableAjaxSelectPlaceholder').replaceWith( searchableAjaxSelectObject.element() ) + # user organization autocomplete + userOrganizationAutocomplete = new App.UserOrganizationAutocompletion + attribute: + name: 'customer_id' + display: 'Customer' + tag: 'user_autocompletion' + type: 'text' + limit: 200 + null: false + relation: 'User' + autocapitalize: false + disableCreateUser: true + multiple: true + + @$('.userOrganizationAutocompletePlaceholder').replaceWith( userOrganizationAutocomplete.element() ) + # time and timeframe @$('.js-timepicker1, .js-timepicker2').timepicker() diff --git a/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee b/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee index 0880cf9ed..d5fa67a58 100644 --- a/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee +++ b/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee @@ -4,17 +4,27 @@ class App.UserOrganizationAutocompletion extends App.Controller 'hide.bs.dropdown .js-recipientDropdown': 'hideOrganizationMembers' 'click .js-organization': 'showOrganizationMembers' 'click .js-back': 'hideOrganizationMembers' - 'click .js-user': 'selectUser' + 'click .js-user': 'onUserClick' 'click .js-userNew': 'newUser' - 'focus input': 'open' + 'focus .js-userSelect': 'onFocus' + 'click .js-userSelect': 'stopPropagation' + 'blur .js-userSelect': 'onBlur' + 'click .form-control': 'focusInput' 'click': 'stopPropagation' + 'change .js-userId': 'executeCallback' + 'click .js-remove': 'removeThisToken' elements: '.recipientList': 'recipientList' + '.js-userSelect': 'userSelect' + '.js-userId': 'userId' + '.form-control': 'formControl' constructor: (params) -> super + @lazySearch = _.debounce(@searchUser, 200, true) + @key = Math.floor( Math.random() * 999999 ).toString() if !@attribute.source @@ -23,7 +33,11 @@ class App.UserOrganizationAutocompletion extends App.Controller # set current value if @attribute.value - @setUser(@attribute.value) + if @attribute.multiple and typeof value is 'object' + for value in @attribute.value + @selectUser value, false + else + @selectUser @attribute.value, false element: => @el @@ -32,132 +46,168 @@ class App.UserOrganizationAutocompletion extends App.Controller $(window).off 'click.UserOrganizationAutocompletion' open: => - @clearDelay('close') + # prevent rebinding of keydown event + return if @el.hasClass 'open' + @el.addClass('open') $(window).on 'click.UserOrganizationAutocompletion', @close $(window).on 'keydown.UserOrganizationAutocompletion', @navigateByKeyboard close: => $(window).off 'keydown.UserOrganizationAutocompletion' - execute = => - @el.removeClass('open') - @delay(execute, 50, 'close') + @el.removeClass('open') $(window).off 'click.UserOrganizationAutocompletion' - selectUser: (e) => - userId = $(e.target).parents('.recipientList-entry').data('user-id') - if !userId - userId = $(e.target).data('user-id') - @setUser(userId) + onFocus: => + @formControl.addClass 'focus' + @open() + + focusInput: => + @userSelect.focus() if not @formControl.hasClass 'focus' + + onBlur: => + @formControl.removeClass 'focus' + + onUserClick: (e) => + userId = $(e.currentTarget).data('user-id') + @selectUser(userId) @close() - setUser: (userId) => - @el.find('[name="' + @attribute.name + '"]').val(userId).trigger('change') + selectUser: (userId) => + if @attribute.multiple and @userId.val() + # add userId to end of comma separated list + userId = _.chain( @userId.val().split(',') ).push(userId).join(',').value() + + @userSelect.val('') + @userId.val(userId).trigger('change') executeCallback: => - userId = @el.find('[name="' + @attribute.name + '"]').val() + # with @attribute.multiple this can be several user ids. + # Only work with the last one since its the newest one + userId = @userId.val().split(',').pop() + return if !userId return if !App.User.exists(userId) - user = App.User.find(userId) - name = user.displayName() - if user.email - name += " <#{user.email}>" - @el.find('[name="' + @attribute.name + '_completion"]').val(name).trigger('change') + name = App.User.find(userId).displayName() + + if @attribute.multiple + # create token + @createToken name, userId + else + @userSelect.val(name) if @callback @callback(userId) + createToken: (name, userId) => + @userSelect.before App.view('generic/token')( + name: name + value: userId + ) + + removeThisToken: (e) => + @removeToken $(e.currentTarget).parents('.token') + + removeToken: (which) => + switch which + when 'last' + token = @$('.token').last() + return if not token.size() + else + token = which + + # remove userId from input + index = @$('.token').index(token) + ids = @userId.val().split(',') + ids.splice(index, 1) + @userId.val ids.join(',') + + token.remove() + navigateByKeyboard: (e) => - - # clean input field on ESC - if e.keyCode is 27 - - # if org member selection is shown, go back to member list - if !@recipientList.hasClass('is-shown') - @hideOrganizationMembers() - return - - # empty user selection and close - $(e.target).val('').trigger('change') - - # if tab / close recipientList - if e.keyCode is 9 - @close() - - # ignore arrow keys - if e.keyCode is 37 - return - - if e.keyCode is 39 - return - - # up / select upper item - if e.keyCode is 38 - e.preventDefault() - if @recipientList.hasClass('is-shown') - if @recipientList.find('li.is-active').length is 0 - @recipientList.find('li').last().addClass('is-active') - else - if @recipientList.find('li.is-active').prev().length isnt 0 - @recipientList.find('li.is-active').removeClass('is-active').prev().addClass('is-active') - return - recipientListOrgMemeber = @$('.recipientList-organizationMembers').not('.hide') - if recipientListOrgMemeber.not('.hide').find('li.is-active').length is 0 - recipientListOrgMemeber.not('.hide').find('li').last().addClass('is-active') - else - if recipientListOrgMemeber.not('.hide').find('li.is-active').prev().length isnt 0 - recipientListOrgMemeber.not('.hide').find('li.is-active').removeClass('is-active').prev().addClass('is-active') - return - - # down / select lower item - if e.keyCode is 40 - e.preventDefault() - if @recipientList.hasClass('is-shown') - if @recipientList.find('li.is-active').length is 0 - @recipientList.find('li').first().addClass('is-active') - else - if @recipientList.find('li.is-active').next().length isnt 0 - @recipientList.find('li.is-active').removeClass('is-active').next().addClass('is-active') - return - recipientListOrgMemeber = @$('.recipientList-organizationMembers').not('.hide') - if recipientListOrgMemeber.not('.hide').find('li.is-active').length is 0 - recipientListOrgMemeber.find('li').first().addClass('is-active') - else - if recipientListOrgMemeber.not('.hide').find('li.is-active').next().length isnt 0 - recipientListOrgMemeber.not('.hide').find('li.is-active').removeClass('is-active').next().addClass('is-active') - return - - # enter / take item - if e.keyCode is 13 - e.preventDefault() - e.stopPropagation() - - # nav by org member selection - if !@recipientList.hasClass('is-shown') - recipientListOrganizationMembers = @$('.recipientList-organizationMembers').not('.hide') - if recipientListOrganizationMembers.find('.js-back.is-active').get(0) + switch e.keyCode + # clean input on esc + when 27 + # if org member selection is shown, go back to member list + if !@recipientList.hasClass('is-shown') @hideOrganizationMembers() return - userId = recipientListOrganizationMembers.find('li.is-active').data('user-id') - return if !userId - @setUser(userId) - @close() - return - # nav by user list selection - userId = @recipientList.find('li.is-active').data('user-id') - if userId - if userId is 'new' - @newUser() + # empty user selection and close + @userSelect.val('').trigger('change') + # remove last token on backspace + when 8 + if @userSelect.val() is '' + @removeToken('last') + # close on tab + when 9 then @close() + # ignore left and right + when 37, 39 then return + # up / select upper item + when 38 + e.preventDefault() + if @recipientList.hasClass('is-shown') + if @recipientList.find('li.is-active').length is 0 + @recipientList.find('li').last().addClass('is-active') + else + if @recipientList.find('li.is-active').prev().length isnt 0 + @recipientList.find('li.is-active').removeClass('is-active').prev().addClass('is-active') + return + recipientListOrgMemeber = @$('.recipientList-organizationMembers').not('.hide') + if recipientListOrgMemeber.not('.hide').find('li.is-active').length is 0 + recipientListOrgMemeber.not('.hide').find('li').last().addClass('is-active') else - @setUser(userId) - @close() + if recipientListOrgMemeber.not('.hide').find('li.is-active').prev().length isnt 0 + recipientListOrgMemeber.not('.hide').find('li.is-active').removeClass('is-active').prev().addClass('is-active') return + # down / select lower item + when 40 + e.preventDefault() + if @recipientList.hasClass('is-shown') + if @recipientList.find('li.is-active').length is 0 + @recipientList.find('li').first().addClass('is-active') + else + if @recipientList.find('li.is-active').next().length isnt 0 + @recipientList.find('li.is-active').removeClass('is-active').next().addClass('is-active') + return + recipientListOrgMemeber = @$('.recipientList-organizationMembers').not('.hide') + if recipientListOrgMemeber.not('.hide').find('li.is-active').length is 0 + recipientListOrgMemeber.find('li').first().addClass('is-active') + else + if recipientListOrgMemeber.not('.hide').find('li.is-active').next().length isnt 0 + recipientListOrgMemeber.not('.hide').find('li.is-active').removeClass('is-active').next().addClass('is-active') + return + # enter / take item + when 13 + e.preventDefault() + e.stopPropagation() - organizationId = @recipientList.find('li.is-active').data('organization-id') - return if !organizationId - @showOrganizationMembers(undefined, @recipientList.find('li.is-active')) + # nav by org member selection + if !@recipientList.hasClass('is-shown') + recipientListOrganizationMembers = @$('.recipientList-organizationMembers').not('.hide') + if recipientListOrganizationMembers.find('.js-back.is-active').get(0) + @hideOrganizationMembers() + return + userId = recipientListOrganizationMembers.find('li.is-active').data('user-id') + return if !userId + @selectUser(userId) + @close() if !@attribute.multiple + return + + # nav by user list selection + userId = @recipientList.find('li.is-active').data('user-id') + if userId + if userId is 'new' + @newUser() + else + @selectUser(userId) + @close() if !@attribute.multiple + return + + organizationId = @recipientList.find('li.is-active').data('organization-id') + return if !organizationId + @showOrganizationMembers(undefined, @recipientList.find('li.is-active')) buildOrganizationItem: (organization) -> @@ -188,33 +238,28 @@ class App.UserOrganizationAutocompletion extends App.Controller if !@attribute.disableCreateUser @recipientList.append(@buildUserNew()) - @el.find('[name="' + @attribute.name + '"]').on( - 'change', - (e) => - @executeCallback() - ) - # start search @searchTerm = '' - @el.find('[name="' + @attribute.name + '_completion"]').on( - 'keyup', - (e) => - term = $(e.target).val().trim() - return if @searchTerm is term - @searchTerm = term - @hideOrganizationMembers() + @userSelect.on 'keyup', @onKeyUp - # hide dropdown - if !term && !@attribute.disableCreateUser - @emptyResultList() - @recipientList.append(@buildUserNew()) + onKeyUp: (e) => + term = $(e.target).val().trim() + return if @searchTerm is term + @searchTerm = term - # show dropdown - if term && ( !@attribute.minLengt || @attribute.minLengt <= term.length ) - execute = => @searchUser(term) - @delay(execute, 400, 'userSearch') - ) + @hideOrganizationMembers() + + # hide dropdown + if !term + @emptyResultList() + + if !@attribute.disableCreateUser + @recipientList.append(@buildUserNew()) + + # show dropdown + if term && ( !@attribute.minLengt || @attribute.minLengt <= term.length ) + @lazySearch(term) searchUser: (term) => @ajax( @@ -249,6 +294,8 @@ class App.UserOrganizationAutocompletion extends App.Controller if !@attribute.disableCreateUser @recipientList.append(@buildUserNew()) + + @recipientList.find('.js-user').first().addClass('is-active') ) emptyResultList: => diff --git a/app/assets/javascripts/app/views/generic/token.jst.eco b/app/assets/javascripts/app/views/generic/token.jst.eco new file mode 100644 index 000000000..d5e6f42ec --- /dev/null +++ b/app/assets/javascripts/app/views/generic/token.jst.eco @@ -0,0 +1,4 @@ +
+ <%= @name %> + × +
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/user_search/input.jst.eco b/app/assets/javascripts/app/views/generic/user_search/input.jst.eco index 6cc7b4359..56a381ac5 100644 --- a/app/assets/javascripts/app/views/generic/user_search/input.jst.eco +++ b/app/assets/javascripts/app/views/generic/user_search/input.jst.eco @@ -1,6 +1,6 @@ -
- - +
+ + <%- @Icon('arrow-down', 'dropdown-arrow') %>
diff --git a/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco b/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco index 28479211d..7fc1e34c6 100644 --- a/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco @@ -34,9 +34,11 @@

A date time

- -
at
- +
+ +
at
+ +
@@ -99,9 +101,15 @@
- +
+ +

User

+
+ +
+

Checkbox

diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 16c817412..8895e83e8 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -6,7 +6,6 @@ *= require ./bootstrap.css *= require ./cropper.css *= require ./fineuploader.css - *= require ./bootstrap-tokenfield.css *= require ./font.css *= require ./svg-dimensions.css *= require ./highlighter-github.css diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 508f0fb68..32b57632c 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -1639,6 +1639,13 @@ select.form-control:not([multiple]) { } } +.select.form-group, +.user_autocompletion.form-group { + .form-control { + padding-right: 21px; + } +} + .form-control + .icon-arrow-down, .dropdown-arrow { position: absolute; @@ -5382,54 +5389,74 @@ footer { fill: white; } -.tokenfield .token { +.token { padding: 0 0 0 10px; - margin: 0 5px 7px 0; - height: 25px; - line-height: 27px; + margin: 0 5px 6px 0; + height: 26px; color: white; + border-radius: 3px; background: hsl(198,19%,72%); border: none; float: none; display: inline-flex; align-items: center; + cursor: default; } + /* + selector needs to be stronger than .token-input + in order to override input[type=text] + */ .tokenfield .token-input { vertical-align: top; - padding: 0 10px 7px 0; + padding: 0 10px 7px 5px; margin: 0; + min-width: 60px; height: 32px; display: inline-block; + border: none; + box-shadow: none; + outline: none; + flex: 1; + + &:focus { + box-shadow: none; + } } .tokenfield .token ~ .token-input { padding: 0 5px 7px 0; } - .tokenfield .token .token-label { + .token-label { padding: 0; } .tokenfield.form-control { padding: 7px 7px 0; + height: auto; + display: flex; + flex-wrap: wrap; } - .tokenfield .token .close { + .token .close, + .token-close { margin: 0; - padding: 0 9px 0 5px; + padding: 0 9px 0 12px; font-family: inherit; - font-weight: 300; - font-size: 30px; + font-weight: 100; + font-size: 28px; line-height: 1; color: white; text-shadow: none; opacity: .3; outline: none; height: auto; + cursor: pointer; } - .tokenfield .token .close:hover { + .token .close:hover, + .token-close:hover { opacity: .5; } @@ -5862,7 +5889,8 @@ footer { opacity: 1; } - .recipientList-entry:hover .recipientList-icon { + .recipientList-entry:hover .recipientList-icon, + .recipientList-entry.is-active .recipientList-icon { opacity: 1; }