From 14f636fec2d227ee61274ef11b899346bd141361 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 2 Oct 2014 08:37:24 +0200 Subject: [PATCH] Init version of new text modules. --- .../_application_controller_form.js.coffee | 6 + .../app/controllers/layout_ref.js.coffee | 4 +- .../app/controllers/ticket_zoom.js.coffee | 458 ++++++++---------- .../controllers/widget/text_module.js.coffee | 53 +- .../app/lib/base/jquery.contenteditable.js | 325 +++++++------ .../app/lib/base/jquery.textmodule.js | 297 ++++++++++++ .../javascripts/app/views/ticket_zoom.jst.eco | 2 +- .../app/views/ticket_zoom/edit.jst.eco | 121 +++-- app/assets/stylesheets/zzz.css.erb | 3 +- 9 files changed, 777 insertions(+), 492 deletions(-) create mode 100644 app/assets/javascripts/app/lib/base/jquery.textmodule.js diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee index 4ff04dfff..37cd19d46 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee @@ -1599,6 +1599,12 @@ class App.ControllerForm extends App.Controller lookupForm = @findForm(form) + # get contenteditable + for element in lookupForm.find('[contenteditable]') + name = $(element).data('name') + if name + param[name] = $(element).ceg() + # get form elements array = lookupForm.serializeArray() diff --git a/app/assets/javascripts/app/controllers/layout_ref.js.coffee b/app/assets/javascripts/app/controllers/layout_ref.js.coffee index 875932637..96856ae34 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.js.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.js.coffee @@ -163,6 +163,8 @@ class LayoutRefCommunicationReply extends App.ControllerContent maxlength: 2500 }) + @$('[contenteditable]').textmodule() + detect_empty_textarea: => if !@textarea.text() @add_textarea_catcher() @@ -172,7 +174,7 @@ class LayoutRefCommunicationReply extends App.ControllerContent open_textarea: (event, withoutAnimation) => if !@ticketEdit.hasClass('is-open') duration = 300 - + if withoutAnimation duration = 0 diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee index 2ba1146dd..d1a46b319 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee @@ -88,9 +88,6 @@ class App.TicketZoom extends App.Controller if newTicketRaw.updated_by_id isnt @Session.get('id') App.TaskManager.notify( @task_key ) - # rerender edit box - @editDone = false - # remember current data @ticketUpdatedAtLastCall = newTicketRaw.updated_at @@ -163,6 +160,15 @@ class App.TicketZoom extends App.Controller el: @el.find('.ticket-meta') ) + new Edit( + ticket: @ticket + el: @el.find('.ticket-edit') + #el: @el.find('.edit') + form_meta: @form_meta + defaults: @taskGet('article') + ui: @ + ) + editTicket = (el) => el.append('
') @editEl = el @@ -170,7 +176,7 @@ class App.TicketZoom extends App.Controller reset = (e) => e.preventDefault() - App.TaskManager.update( @task_key, { 'state': {} }) + @taskReset() show(@ticket) show = (ticket) => @@ -202,7 +208,7 @@ class App.TicketZoom extends App.Controller form.find('[name="' + fieldNameToChange + '"]').replaceWith( newElement ) defaults = ticket.attributes() - task_state = App.TaskManager.get(@task_key).state || {} + task_state = @taskGet('ticket') modelDiff = @getDiff( defaults, task_state ) #if @isRole('Customer') # delete defaults['state_id'] @@ -351,17 +357,13 @@ class App.TicketZoom extends App.Controller items: items ) - @ArticleView() - - if force || !@editDone - # reset form on force reload - if force && _.isEmpty( App.TaskManager.get(@task_key).state ) - App.TaskManager.update( @task_key, { 'state': {} }) - @editDone = true - - # rerender widget if it hasn't changed - if !@editWidget || _.isEmpty( App.TaskManager.get(@task_key).state ) - @editWidget = @Edit() + # show article + new ArticleView( + ticket: @ticket + ticket_article_ids: @ticket_article_ids + el: @el.find('.ticket-article') + ui: @ + ) # scroll to article if given if @article_id && document.getElementById( 'article-' + @article_id ) @@ -376,40 +378,30 @@ class App.TicketZoom extends App.Controller @autosaveStart() - - ArticleView: => - # show article - new ArticleView( - ticket: @ticket - ticket_article_ids: @ticket_article_ids - el: @el.find('.ticket-article') - ui: @ - ) - - Edit: => - # show edit - new Edit( - ticket: @ticket - el: @el.find('.ticket-edit') - #el: @el.find('.edit') - form_meta: @form_meta - task_key: @task_key - ui: @ - ) - autosaveStop: => @autosaveLast = {} @clearInterval( 'autosave' ) autosaveStart: => if !@autosaveLast - @autosaveLast = App.TaskManager.get(@task_key).state || {} + @autosaveLast = @taskGet() update = => - currentStore = @ticket.attributes() - currentParams = @formParam( @el.find('.edit') ) + #console.log('AR', @formParam( @el.find('.article-add') ) ) + currentStore = + ticket: @ticket.attributes() + article: { + type: '' + body: '' + internal: '' + } + currentParams = + ticket: @formParam( @el.find('.edit') ) + article: @formParam( @el.find('.article-add') ) # get diff of model - modelDiff = @getDiff( currentStore, currentParams ) + modelDiff = + ticket: @getDiff( currentStore.ticket, currentParams.ticket ) + article: @getDiff( currentStore.article, currentParams.article ) #console.log('modelDiff', modelDiff) # get diff of last save @@ -420,9 +412,9 @@ class App.TicketZoom extends App.Controller console.log('model DIFF ', modelDiff) @autosaveLast = clone(currentParams) - @markFormDiff( modelDiff ) + @markFormDiff( modelDiff.ticket ) - App.TaskManager.update( @task_key, { 'state': modelDiff }) + @taskUpdateAll( modelDiff ) @interval( update, 3000, 'autosave' ) getDiff: (model, params) => @@ -584,11 +576,35 @@ class App.TicketZoom extends App.Controller done: (r) => # reset form after save - App.TaskManager.update( @task_key, { 'state': {} }) + @taskReset() @fetch( ticket.id, true ) ) + taskGet: (area) => + @localTaskData = App.TaskManager.get(@task_key).state || {} + if area + if !@localTaskData[area] + @localTaskData[area] = {} + return @localTaskData[area] + if !@localTaskData + @localTaskData = {} + @localTaskData + + taskUpdate: (area, data) => + @localTaskData[area] = data + App.TaskManager.update( @task_key, { 'state': @localTaskData }) + + taskUpdateAll: (data) => + @localTaskData = data + App.TaskManager.update( @task_key, { 'state': @localTaskData }) + + taskReset: (area, data) => + @localTaskData = + ticket: {} + article: {} + App.TaskManager.update( @task_key, { 'state': @localTaskData }) + class TicketTitle extends App.Controller events: 'blur .ticket-title-update': 'update' @@ -653,38 +669,53 @@ class TicketMeta extends App.Controller class Edit extends App.Controller elements: - 'textarea' : 'textarea' - '.edit-control-item' : 'editControlItem' - '.edit-controls': 'editControls' - '.recipient-picker': 'recipientPicker' - '.recipient-list': 'recipientList' - '.recipient-list .list-arrow': 'recipientListArrow' - '.js-attachment': 'attachmentHolder' - '.js-attachment-text': 'attachmentText' - '.bubble-placeholder-hint': 'bubblePlaceholderHint' + '.js-textarea' : 'textarea' + '.attachmentPlaceholder': 'attachmentPlaceholder' + '.attachmentPlaceholder-inputHolder': 'attachmentInputHolder' + '.attachmentPlaceholder-hint': 'attachmentHint' + '.article-add': 'ticketEdit' + '.attachments': 'attachmentsHolder' + '.attachmentUpload': 'attachmentUpload' + '.attachmentUpload-progressBar':'progressBar' + '.js-percentage': 'progressText' + #'.edit-control-item' : 'editControlItem' + #'.edit-controls': 'editControls' + #'.recipient-picker': 'recipientPicker' + #'.recipient-list': 'recipientList' + #'.recipient-list .list-arrow': 'recipientListArrow' events: - 'click .submit': 'update' + #'click .submit': 'update' 'click [data-type="reset"]': 'reset' - 'click .visibility-toggle': 'toggle_visibility' + 'click .visibility-toggle': 'toggleVisibility' 'click .pop-selectable': 'selectArticleType' 'click .pop-selected': 'showSelectableArticleType' - 'focus textarea': 'open_textarea' - 'input textarea': 'detect_empty_textarea' 'click .recipient-picker': 'toggle_recipients' 'click .recipient-list': 'stopPropagation' 'click .list-entry-type div': 'change_type' 'submit .recipient-list form': 'add_recipient' + 'focus .js-textarea': 'open_textarea' + 'input .js-textarea': 'detect_empty_textarea' + 'dragenter': 'onDragenter' + 'dragleave': 'onDragleave' + 'drop': 'onFileDrop' + 'change input[type=file]': 'onFilePick' constructor: -> super @textareaHeight = open: 148 - closed: 38 + closed: 20 + + @dragEventCounter = 0 + @attachments = [] @render() + if @textarea.text().trim() + @ticketEdit.addClass('is-open') + stopPropagation: (e) -> e.stopPropagation() @@ -729,74 +760,36 @@ class Edit extends App.Controller icon: 'note' }, ] - + console.log('DEvvvvvV', @defaults) @html App.view('ticket_zoom/edit')( ticket: ticket - type: @type articleTypes: articleTypes + article: @defaults isCustomer: @isRole('Customer') ) - @form_id = App.ControllerForm.formId() - defaults = ticket.attributes() - if @isRole('Customer') - delete defaults['state_id'] - delete defaults['state'] - if !_.isEmpty( App.TaskManager.get(@task_key).state ) - defaults = App.TaskManager.get(@task_key).state + configure_attributes = [ + { name: 'customer_id', display: 'Recipients', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organisation/Company', minLengt: 2, disableCreateUser: false }, + ] - new App.ControllerForm( - el: @el.find('.form-article-update') - form_id: @form_id - model: App.TicketArticle - screen: 'edit' - filter: - type_id: [1,9,5] - params: defaults - dependency: [ - { - bind: { - name: 'type_id' - relation: 'TicketArticleType' - value: ['email'] - }, - change: { - action: 'show' - name: ['to', 'cc'], - }, - }, - { - bind: { - name: 'type_id' - relation: 'TicketArticleType' - value: ['note', 'phone', 'twitter status'] - }, - change: { - action: 'hide' - name: ['to', 'cc'], - }, - }, - { - bind: { - name: 'type_id' - relation: 'TicketArticleType' - value: ['twitter direct-message'] - }, - change: { - action: 'show' - name: ['to'], - }, - }, - ] + controller = new App.ControllerForm( + el: @$('.recipients') + model: + configure_attributes: configure_attributes, ) - # start auto save - #@autosaveStart() + @$('[data-name="body"]').ce({ + mode: 'textonly' + multiline: true + maxlength: 2500 + }) + + @form_id = App.ControllerForm.formId() # show text module UI if !@isRole('Customer') textModule = new App.WidgetTextModule( - el: @textarea + el: @el data: ticket: ticket ) @@ -871,15 +864,18 @@ class Edit extends App.Controller e.stopPropagation() e.preventDefault() console.log "add recipient", e - # store recipient + # store recipient - toggle_visibility: -> - if @el.hasClass('is-public') - @el.removeClass('is-public') - @el.addClass('is-internal') + toggleVisibility: -> + item = @$('.article-add') + if item.hasClass('is-public') + item.removeClass('is-public') + item.addClass('is-internal') + @$('[name="internal"]').val('true') else - @el.addClass('is-public') - @el.removeClass('is-internal') + item.addClass('is-public') + item.removeClass('is-internal') + @$('[name="internal"]').val('') showSelectableArticleType: => @el.find('.pop-selector').removeClass('hide') @@ -903,59 +899,60 @@ class Edit extends App.Controller if @type typeIcon.removeClass @type @type = type + @$('[name="type"]').val(type) typeIcon.addClass @type detect_empty_textarea: => - if !@textarea.val() + if !@textarea.text().trim() @add_textarea_catcher() else @remove_textarea_catcher() open_textarea: => - if !@textareaCatcher and !@textarea.val() - @el.addClass('is-open') + console.log('OT', @textareaCatcher , @textarea.text().trim() , @attachments.length) + if !@textareaCatcher and !@textarea.text().trim() and !@attachments.length + @ticketEdit.addClass('is-open') @textarea.velocity properties: - height: "#{ @textareaHeight.open - 38 }px" + minHeight: "#{ @textareaHeight.open - 38 }px" marginBottom: 38 options: duration: 300 easing: 'easeOutQuad' + complete: => @add_textarea_catcher() # scroll to bottom - @textarea.velocity "scroll", - container: @textarea.scrollParent() - offset: 99999 - duration: 300 - easing: 'easeOutQuad' - queue: false + # @textarea.velocity "scroll", + # container: @textarea.scrollParent() + # offset: 99999 + # duration: 300 + # easing: 'easeOutQuad' + # queue: false - @editControlItem.velocity "transition.slideRightIn", - duration: 300 - stagger: 50 - drag: true + # @editControlItem.velocity "transition.slideRightIn", + # duration: 300 + # stagger: 50 + # drag: true # move attachment text to the left bottom (bottom happens automatically) - @attachmentHolder.velocity + @attachmentPlaceholder.velocity properties: - translateX: -@attachmentText.position().left + "px" + translateX: -@attachmentInputHolder.position().left + "px" options: duration: 300 easing: 'easeOutQuad' - @bubblePlaceholderHint.velocity + @attachmentHint.velocity properties: opacity: 0 options: duration: 300 - @add_textarea_catcher() - add_textarea_catcher: -> @textareaCatcher = new App.clickCatcher - holder: @el.offsetParent() + holder: @ticketEdit.offsetParent() callback: @close_textarea zIndexScale: 4 @@ -966,168 +963,107 @@ class Edit extends App.Controller close_textarea: => @remove_textarea_catcher() - if !@textarea.val() + if !@textarea.text().trim() && !@attachments.length @textarea.velocity properties: - height: "#{ @textareaHeight.closed }px" + minHeight: "#{ @textareaHeight.closed }px" marginBottom: 0 options: duration: 300 easing: 'easeOutQuad' - complete: => @el.removeClass('is-open') + complete: => @ticketEdit.removeClass('is-open') - @attachmentHolder.velocity + @attachmentPlaceholder.velocity properties: translateX: 0 options: duration: 300 easing: 'easeOutQuad' - @bubblePlaceholderHint.velocity + @attachmentHint.velocity properties: opacity: 1 options: duration: 300 - @editControlItem.css('display', 'none') + # @editControlItem.css('display', 'none') - autosaveStop: => - @clearInterval( 'autosave' ) + onDragenter: (event) => + # on the first event, + # open textarea (it will only open if its closed) + @open_textarea() if @dragEventCounter is 0 - autosaveStart: => - @autosaveLast = _.clone( @ui.formDefault ) - update = => - currentData = @formParam( @el.find('.ticket-update') ) - diff = difference( @autosaveLast, currentData ) - if !@autosaveLast || ( diff && !_.isEmpty( diff ) ) - @autosaveLast = currentData - @log 'notice', 'form hash changed', diff, currentData - @el.find('.edit').addClass('form-changed') - @el.find('.edit').find('.reset-message').show() - @el.find('.edit').find('.reset-message').removeClass('hide') - App.TaskManager.update( @task_key, { 'state': currentData }) - @interval( update, 3000, 'autosave' ) + @dragEventCounter++ + @ticketEdit.addClass('is-dropTarget') - update: (e) => - e.preventDefault() - #@autosaveStop() - params = @formParam(e.target) + onDragleave: (event) => + @dragEventCounter-- - # get ticket - ticket = App.Ticket.fullLocal( @ticket.id ) + @ticketEdit.removeClass('is-dropTarget') if @dragEventCounter is 0 - @log 'notice', 'update', params, ticket + onFileDrop: (event) => + event.preventDefault() + event.stopPropagation() + files = event.originalEvent.dataTransfer.files + @ticketEdit.removeClass('is-dropTarget') - # update local ticket + @queueUpload(files) - # create local article + onFilePick: (event) => + @open_textarea() + @queueUpload(event.target.files) + queueUpload: (files) -> + @uploadQueue ?= [] - # find sender_id - if @isRole('Customer') - sender = App.TicketArticleSender.findByAttribute( 'name', 'Customer' ) - type = App.TicketArticleType.findByAttribute( 'name', 'web' ) - params.type_id = type.id - params.sender_id = sender.id - else - sender = App.TicketArticleSender.findByAttribute( 'name', 'Agent' ) - type = App.TicketArticleType.find( params['type_id'] ) - params.sender_id = sender.id + # add files + for file in files + @uploadQueue.push(file) - # update ticket - for key, value of params - ticket[key] = value + @workOfUploadQueue() - # check owner assignment - if !@isRole('Customer') - if !ticket['owner_id'] - ticket['owner_id'] = 1 - - # check if title exists - if !ticket['title'] - alert( App.i18n.translateContent('Title needed') ) + workOfUploadQueue: => + if !@uploadQueue.length return - # validate email params - if type.name is 'email' + file = @uploadQueue.shift() + # console.log "working of", file, "from", @uploadQueue + @fakeUpload file.name, file.size, @workOfUploadQueue - # check if recipient exists - if !params['to'] && !params['cc'] - alert( App.i18n.translateContent('Need recipient in "To" or "Cc".') ) - return + humanFileSize: (size) => + i = Math.floor( Math.log(size) / Math.log(1024) ) + return ( size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i] - # check if message exists - if !params['body'] - alert( App.i18n.translateContent('Text needed') ) - return + updateUploadProgress: (progress) => + @progressBar.width(progress + "%") + @progressText.text(progress) - # check attachment - if params['body'] - attachmentTranslated = App.i18n.translateContent('Attachment') - attachmentTranslatedRegExp = new RegExp( attachmentTranslated, 'i' ) - if params['body'].match(/attachment/i) || params['body'].match( attachmentTranslatedRegExp ) - if !confirm( App.i18n.translateContent('You use attachment in text but no attachment is attached. Do you want to continue?') ) - #@autosaveStart() - return + if progress is 100 + @attachmentPlaceholder.removeClass('hide') + @attachmentUpload.addClass('hide') - # submit ticket & article - @log 'notice', 'update ticket', ticket + fakeUpload: (fileName, fileSize, callback) -> + @attachmentPlaceholder.addClass('hide') + @attachmentUpload.removeClass('hide') - # disable form - @formDisable(e) + progress = 0; + duration = fileSize / 1024 - # validate ticket - errors = ticket.validate( - screen: 'edit' - ) - if errors - @log 'error', 'update', errors + for i in [0..100] + setTimeout @updateUploadProgress, i*duration/100 , i - @log 'error', errors - @formValidate( - form: e.target - errors: errors - screen: 'edit' - ) - @formEnable(e) - #@autosaveStart() - return + setTimeout (=> + callback() + @renderAttachment(fileName, fileSize) + ), duration - # validate article - articleAttributes = App.TicketArticle.attributesGet( 'edit' ) - if params['body'] || ( articleAttributes['body'] && articleAttributes['body']['null'] is false ) - article = new App.TicketArticle - params.from = @Session.get().displayName() - params.ticket_id = ticket.id - params.form_id = @form_id + renderAttachment: (fileName, fileSize) => + @attachments.push([fileName, fileSize]) + @attachmentsHolder.append App.view('ticket_zoom/attachment') + fileName: fileName + fileSize: @humanFileSize(fileSize) - if !params['internal'] - params['internal'] = false - - @log 'notice', 'update article', params, sender - article.load(params) - errors = article.validate() - if errors - @log 'error', 'update article', errors - @formValidate( - form: e.target - errors: errors - screen: 'edit' - ) - @formEnable(e) - @autosaveStart() - return - - ticket.article = article - ticket.save( - done: (r) => - - # reset form after save - App.TaskManager.update( @task_key, { 'state': {} }) - - @ui.fetch( ticket.id, true ) - ) reset: (e) => e.preventDefault() diff --git a/app/assets/javascripts/app/controllers/widget/text_module.js.coffee b/app/assets/javascripts/app/controllers/widget/text_module.js.coffee index d5bef5496..e3d8560f3 100644 --- a/app/assets/javascripts/app/controllers/widget/text_module.js.coffee +++ b/app/assets/javascripts/app/controllers/widget/text_module.js.coffee @@ -2,38 +2,31 @@ class App.WidgetTextModule extends App.Controller constructor: -> super - @lastData = {} - customItemTemplate = "
 
" - elementFactory = (element, e) -> - template = $(customItemTemplate).find('span') - .text(e.val).end() - .find('small') - .text("(" + e.keywords + ")").end() - element.append(template) - - @el.parent().find('textarea').sew( - values: @reload(@data) - token: '::' - elementFactory: elementFactory - ) + # remember instances + @bindElements = [] + if @selector + @bindElements = @$( @selector ).textmodule() + else + @bindElements = @$('[contenteditable]').textmodule() + @update() @subscribeId = App.TextModule.subscribe(@update, initFetch: true ) release: => App.TextModule.unsubscribe(@subscribeId) - reload: (data = false) => - if data - @lastData['data'] = data + reload: (data) => + return if !data @update() update: => - all = App.TextModule.all() - values = [{val: '-', keywords: '-'}] - ui = @lastData || @ - for item in all + allRaw = App.TextModule.all() + all = [] + ui = @data || @ + for item in allRaw if item.active is true - contentNew = item.content.replace( /<%=\s{0,2}(.+?)\s{0,2}%>/g, ( all, key ) -> + attributes = item.attributes() + attributes.content = attributes.content.replace( /<%=\s{0,2}(.+?)\s{0,2}%>/g, ( index, key ) -> key = key.replace( /@/g, 'ui.data.' ) varString = "#{key}" + '' # console.log( "tag replacement env: ", ui.data) @@ -45,16 +38,10 @@ class App.WidgetTextModule extends App.Controller key = '' return key ) - value = { val: contentNew, keywords: item.keywords || item.name } - values.push value - - if values.length isnt 1 - values.shift() + all.push attributes # set new data - if @el[0] - if $(@el[0]).data() - if $(@el[0]).data().plugin_sew - $(@el[0]).data().plugin_sew.options.values = values - - return values + if @bindElements[0] + for element in @bindElements + if $(element).data().plugin_textmodule + $(element).data().plugin_textmodule.collection = all diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js index b0dbf14da..d5a1e9d22 100644 --- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js +++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js @@ -9,7 +9,8 @@ # */ - var DEFAULTS = { + var pluginName = 'ce', + defaults = { mode: 'richtext', multiline: true, allowKey: { @@ -38,24 +39,157 @@ 73: true, // i 85: true, // u } + }; + + function Plugin( element, options ) { + this.element = element; + this.$element = $(element) + + this.options = $.extend( {}, defaults, options) ; + + this._defaults = defaults; + this._name = pluginName; + + this.preventInput = false + + this.init(); } + Plugin.prototype.init = function () { + // process placeholder + if ( this.options.placeholder ) { + this.updatePlaceholder( 'add' ) + this.$element.on('focus', $.proxy(function (e) { + this.updatePlaceholder( 'remove' ) + }, this)).on('blur', $.proxy(function (e) { + this.updatePlaceholder( 'add' ) + }, this)) + } + + // maxlength check + //this.options.maxlength = 10 + if ( this.options.maxlength ) { + this.$element.on('keydown', $.proxy(function (e) { + console.log('maxlength', e.keyCode, this.allowKey(e)) + // check control key + if ( this.allowKey(e) ) { + this.maxLengthOk() + } + // check type ahead key + else { + if ( !this.maxLengthOk( true ) ) { + e.preventDefault() + } + } + }, this)).on('keyup', $.proxy(function (e) { + // check control key + if ( this.allowKey(e) ) { + this.maxLengthOk() + } + // check type ahead key + else { + if ( !this.maxLengthOk( true ) ) { + e.preventDefault() + } + } + }, this)).on('focus', $.proxy(function (e) { + this.maxLengthOk() + }, this)).on('blur', $.proxy(function (e) { + this.maxLengthOk() + }, this)) + } + + // handle enter + this.$element.on('keydown', $.proxy(function (e) { + console.log('keydown', e.keyCode) + if (this.preventInput) { + console.log('preventInput', this.preventInput) + return + } + + // trap the return key being pressed + if (e.keyCode === 13) { + // disbale multi line + if ( !this.options.multiline ) { + e.preventDefault() + return + } + // limit check + if ( !this.maxLengthOk( true ) ) { + e.preventDefault() + return + } + + if ( this.options.mode === 'textonly' ) { + document.execCommand('insertHTML', false, "\n") + } + else { + document.execCommand('insertHTML', false, '
') + } + // prevent the default behaviour of return key pressed + return false + } + }, this)) + + // just paste text + if ( this.options.mode === 'textonly' ) { + this.$element.on('paste', $.proxy(function (e) { + var text = (e.originalEvent || e).clipboardData.getData('text/plain') + var overlimit = false + if (text) { + + // replace new lines + if ( !this.options.multiline ) { + text = text.replace(/\n/g, '') + text = text.replace(/\r/g, '') + text = text.replace(/\t/g, '') + } + + // limit length, limit paste string + if ( this.options.maxlength ) { + var pasteLength = text.length + var currentLength = this.$element.text().length + var overSize = ( currentLength + pasteLength ) - this.options.maxlength + if ( overSize > 0 ) { + text = text.substr( 0, pasteLength - overSize ) + overlimit = true + } + } + + // insert new text + e.preventDefault() + document.execCommand('inserttext', false, text) + this.maxLengthOk( overlimit ) + } + + }, this)) + } + + // disable rich text b/u/i + if ( this.options.mode === 'textonly' ) { + this.$element.on('keydown', $.proxy(function (e) { + if ( this.richTextKey(e) ) { + e.preventDefault() + } + }, this)) + } + }; + // add/remove placeholder - var updatePlaceholder = function(target, type) { - var options = target.data('ce.options') - var text = target.text().trim() - var placeholder = '' + options.placeholder + '' + Plugin.prototype.updatePlaceholder = function(type) { + var text = this.$element.text().trim() + var placeholder = '' + this.options.placeholder + '' // add placholder if no text exists if ( type === 'add') { if ( !text ) { - target.html( placeholder ) + this.$element.html( placeholder ) } } // empty placeholder text else { - if ( text === options.placeholder ) { + if ( text === this.options.placeholder ) { setTimeout(function(){ document.execCommand('selectAll', false, ''); document.execCommand('delete', false, ''); @@ -66,178 +200,85 @@ } } - // max length check - var maxLengthOk = function(field, typeAhead) { - var options = field.data('ce.options') - if (!options) { - return true + // disable/enable input + Plugin.prototype.input = function(type) { + if (type === 'off') { + this.preventInput = true } + else { + this.preventInput = false + } + } - var length = field.text().length + // max length check + Plugin.prototype.maxLengthOk = function(typeAhead) { + var length = this.$element.text().length if (typeAhead) { length = length + 1 } - if ( length > options.maxlength ) { - field.addClass('invalid') - setTimeout(function(){ - field.removeClass('invalid') - }, 1000); + if ( length > this.options.maxlength ) { + this.$element.addClass('invalid') + setTimeout($.proxy(function(){ + this.$element.removeClass('invalid') + }, this), 1000) + return false } return true } // check if key is allowed, even if length limit is reached - var allowKey = function(e) { - var options = $(e.target).data('ce.options') - - if ( options.allowKey[ e.keyCode ] ) { + Plugin.prototype.allowKey = function(e) { + if ( this.options.allowKey[ e.keyCode ] ) { return true } - if ( ( e.ctrlKey || e.metaKey ) && options.extraAllowKey[ e.keyCode ] ) { + if ( ( e.ctrlKey || e.metaKey ) && this.options.extraAllowKey[ e.keyCode ] ) { return true } return false } // check if rich text key is pressed - var richTextKey = function(e) { - var options = $(e.target).data('ce.options') - - if ( ( e.ctrlKey || e.metaKey ) && options.richTextFormatKey[ e.keyCode ] ) { + Plugin.prototype.richTextKey = function(e) { + if ( ( e.ctrlKey || e.metaKey ) && this.options.richTextFormatKey[ e.keyCode ] ) { return true } return false } - // get correct val if textbox - $.fn.ceg = function(option) { - var options = this.data('ce.options') - - updatePlaceholder( this, 'remove' ) + // get value + Plugin.prototype.value = function() { + this.updatePlaceholder( 'remove' ) // get text - if ( options.mode === 'textonly' ) { + if ( this.options.mode === 'textonly' ) { // strip html signes if multi line exists - if ( options.multiline ) { - text = this.html() + if ( this.options.multiline ) { + text = this.$element.html() text = text.replace(/
/g, "\n") // new line as br text = text.replace(/
/g, "\n") // in some caes, new line als div text = $("
" + text + "
").text().trim() return text } - return this.text().trim() + return this.$element.text().trim() } - return this.html().trim() + return this.$element.html().trim() } - $.fn.ce = function(option) { - var options = $.extend({}, DEFAULTS, option) - options.placeholder = options.placeholder || this.data('placeholder') - - // store options - this.data('ce.options', options) - - // process placeholder - if ( options.placeholder ) { - updatePlaceholder( this, 'add' ) - this.bind('focus', function (e) { - updatePlaceholder( $(e.target), 'remove' ) - }).bind('blur', function (e) { - updatePlaceholder( $(e.target), 'add' ) - }) - } - - // maxlength check - if ( options.maxlength ) { - this.bind('keydown', function (e) { - - // check control key - if ( allowKey(e) ) { - maxLengthOk( $(e.target) ) - } - - // check type ahead key - else { - if ( !maxLengthOk( $(e.target), true ) ) { - e.preventDefault() - } - } - }).bind('keyup', function (e) { - - // check control key - if ( allowKey(e) ) { - maxLengthOk( $(e.target) ) - } - - // check type ahead key - else { - if ( !maxLengthOk( $(e.target), true ) ) { - e.preventDefault() - } - } - }).bind('focus', function (e) { - maxLengthOk( $(e.target) ) - }).bind('blur', function (e) { - maxLengthOk( $(e.target) ) - }) - } - - // just paste text - if ( options.mode === 'textonly' ) { - this.bind('paste', function (e) { - var text = (e.originalEvent || e).clipboardData.getData('text/plain') - var overlimit = false - if (text) { - - // replace new lines - if ( !options.multiline ) { - text = text.replace(/\n/g, '') - text = text.replace(/\r/g, '') - text = text.replace(/\t/g, '') - } - - // limit length, limit paste string - if ( options.maxlength ) { - var pasteLength = text.length - var currentLength = $(e.target).text().length - var overSize = ( currentLength + pasteLength ) - options.maxlength - if ( overSize > 0 ) { - text = text.substr( 0, pasteLength - overSize ) - overlimit = true - } - } - - // insert new text - e.preventDefault() - document.execCommand('inserttext', false, text) - maxLengthOk( $(e.target), overlimit ) - } - - }); - } - - // disable rich text b/u/i - if ( options.mode === 'textonly' ) { - this.bind('keydown', function (e) { - if ( richTextKey(e) ) { - e.preventDefault() - } - }); - } - - // disable multi line - if ( !options.multiline ) { - this.bind('keydown', function (e) { - switch ( e.keyCode ) { - case 13: // enter - e.preventDefault() - break; - } - }) - } + $.fn[pluginName] = function ( options ) { + return this.each(function () { + if (!$.data(this, 'plugin_' + pluginName)) { + $.data(this, 'plugin_' + pluginName, + new Plugin( this, options )); + } + }); + } + // get correct val if textbox + $.fn.ceg = function() { + var plugin = $.data(this[0], 'plugin_' + pluginName) + return plugin.value() } }(jQuery)); diff --git a/app/assets/javascripts/app/lib/base/jquery.textmodule.js b/app/assets/javascripts/app/lib/base/jquery.textmodule.js new file mode 100644 index 000000000..2854ad0f5 --- /dev/null +++ b/app/assets/javascripts/app/lib/base/jquery.textmodule.js @@ -0,0 +1,297 @@ +(function ($, window, undefined) { + +/* +# mode: textonly/richtext / disable b/i/u/enter + strip on paste +# pasteOnlyText: true +# maxlength: 123 +# multiline: true / disable enter + strip on paste +# placeholder: 'some placeholder' +# +*/ + + var pluginName = 'textmodule', + defaults = {} + + function Plugin( element, options ) { + this.element = element + this.$element = $(element) + + this.options = $.extend( {}, defaults, options) + + this._defaults = defaults + this._name = pluginName + + this.collection = [] + this.active = false + this.buffer = '' + + // check if ce exists + if ( $.data(element, 'plugin_ce') ) { + this.ce = $.data(element, 'plugin_ce') + } + + this.init(); + } + + Plugin.prototype.init = function () { + this.baseTemplate() + + this.$element.on('keydown', $.proxy(function (e) { + + // esc + if ( e.keyCode === 27 ) { + this.close() + } + + // navigate through widget + if ( this.isActive() ) { + console.log('WIDGET IS OPEN', e.keyCode) + + // enter + if ( e.keyCode === 13 ) { + e.preventDefault() + var id = this.$widget.find('.dropdown-menu li.active a').data('id') + console.log('ID', id) + this.take(id) + } + + // arrow keys + if ( e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40 ) { + e.preventDefault() + } + + // up + if ( e.keyCode === 38 ) { + if ( !this.$widget.find('.dropdown-menu li.active')[0] ) { + var top = this.$widget.find('.dropdown-menu li').last().addClass('active').position().top + this.$widget.find('.dropdown-menu').scrollTop( top ); + } + else { + var prev = this.$widget.find('.dropdown-menu li.active').removeClass('active').prev() + var top = 300 + if ( prev[0] ) { + top = prev.addClass('active').position().top + } + this.$widget.find('.dropdown-menu').scrollTop( top ); + } + } + + // down + if ( e.keyCode === 40 ) { + if ( !this.$widget.find('.dropdown-menu li.active')[0] ) { + var top = this.$widget.find('.dropdown-menu li').first().addClass('active').position().top + this.$widget.find('.dropdown-menu').scrollTop( top ); + + } + else { + var next = this.$widget.find('.dropdown-menu li.active').removeClass('active').next() + var top = 300 + if ( next[0] ) { + top = next.addClass('active').position().top + } + console.log('scrollTop', top, top-30) + this.$widget.find('.dropdown-menu').scrollTop( top ); + + } + } + + } + }, this )) + + this.$element.on('keydown', $.proxy(function (e) { + + // backspace + if ( e.keyCode === 8 && this.buffer ) { + if ( this.buffer === '::' ) { + this.close() + } + this.buffer = this.buffer.substr( 0, this.buffer.length-1 ) + console.log('BS', this.buffer) + this.result( this.buffer.substr(2,this.buffer.length) ) + } + }, this )) + + this.$element.on('keypress', $.proxy(function (e) { + var value = this.$element.text() + console.log('BUFF', this.buffer, e.keyCode, String.fromCharCode(e.which) ) + a = $.proxy(function() { + + // shift + if ( e.keyCode === 16 ) { + return + } + + // enter : + if ( e.keyCode === 58 ) { + this.buffer = this.buffer + ':' + } + + if ( this.buffer && this.buffer.substr(0,2) === '::' ) { + + + var sign = String.fromCharCode(e.which) + if ( e.keyCode !== 58 ) { + this.buffer = this.buffer + sign + } + console.log('BUFF HINT', this.buffer, this.buffer.length, e.which) + + this.result( this.buffer.substr(2,this.buffer.length) ) + + if (!this.isActive()) { + this.open() + } + + } + }, this) + setTimeout(a, 400); + + }, this)).on('focus', $.proxy(function (e) { + this.close() + }, this)).on('blur', $.proxy(function (e) { + this.close() + }, this)) + + }; + + // create base template + Plugin.prototype.baseTemplate = function() { + this.$element.after('') + this.$widget = this.$element.next() + this.updatePosition() + } + + // get cursor position + Plugin.prototype.getCaretPosition = function() { + document.execCommand('insertHTML', false, ''); + var hiddenNode = document.getElementById('hidden'); + if (!hiddenNode) { + return 0; + } + var position = $(hiddenNode).position() + hiddenNode.parentNode.removeChild(hiddenNode) + return position + } + + // update widget position + Plugin.prototype.updatePosition = function() { + this.$widget.find('.dropdown-menu').scrollTop( 300 ); + var position = this.getCaretPosition() + var heightTextarea = this.$element.height() + var widgetHeight = this.$widget.find('ul').height() + 40 + console.log('position', position) + console.log('heightTextarea', heightTextarea) + console.log('widgetHeight', widgetHeight) + this.$widget.css('top', position.top - heightTextarea - widgetHeight) + if ( !this.isActive() ) { + this.$widget.css('left', position.left) + } + } + + // open widget + Plugin.prototype.open = function() { + this.active = true + if (this.ce) { + this.ce.input('off') + } + this.$widget.addClass('open') + } + + // close widget + Plugin.prototype.close = function() { + this.active = false + this.cutInput() + if (this.ce) { + this.ce.input('on') + } + this.$widget.removeClass('open') + } + + // check if widget is active/open + Plugin.prototype.isActive = function() { + return this.active + } + + // select text module and insert into text + Plugin.prototype.take = function(id) { + if (!id) { + this.close() + return + } + for (var i = 0; i < this.collection.length; i++) { + var item = this.collection[i] + if ( item.id == id ) { + var content = item.content + "\n" + this.cutInput() + document.execCommand('insertHTML', false, content) + this.close() + return + } + } + return + } + + // cut out search string from text + Plugin.prototype.cutInput = function() { + if (!this.buffer) { + return + } + var sel = window.getSelection(); + var range = sel.getRangeAt(0); + var clone = range.cloneRange(); + clone.setStart(range.startContainer, range.startOffset - this.buffer.length); + clone.setEnd(range.startContainer, range.startOffset); + clone.deleteContents(); + this.buffer = '' + } + + // render result + Plugin.prototype.result = function(term) { + + var result = _.filter( this.collection, function(item) { + reg = new RegExp( term, 'i' ) + if ( item.name && item.name.match( reg ) ) { + return item + } + if ( item.keywords && item.keywords.match( reg ) ) { + return item + } + return + }) + + this.$widget.find('ul').html('') + console.log('result', term, result) + for (var i = 0; i < result.length; i++) { + var item = result[i] + template = "
  • " + item.name + if (item.keywords) { + template = template + " (" + item.keywords + ")" + } + template = template + "
  • " + this.$widget.find('ul').append(template) + } + if ( !result[0] ) { + this.$widget.find('ul').append("
  • -
  • ") + } + this.$widget.find('ul li').on( + 'click', + function(e) { + console.log(31231) + e.preventDefault() + var id = $(e.target).data('id') + console.log('99', id) + } + ) + this.updatePosition() + } + + + $.fn[pluginName] = function ( options ) { + return this.each(function () { + if (!$.data(this, 'plugin_' + pluginName)) { + $.data(this, 'plugin_' + pluginName, + new Plugin( this, options )); + } + }); + } + +}(jQuery, window)); diff --git a/app/assets/javascripts/app/views/ticket_zoom.jst.eco b/app/assets/javascripts/app/views/ticket_zoom.jst.eco index 75d63c75e..c60a02b1c 100644 --- a/app/assets/javascripts/app/views/ticket_zoom.jst.eco +++ b/app/assets/javascripts/app/views/ticket_zoom.jst.eco @@ -24,7 +24,7 @@
    -
    +
    diff --git a/app/assets/javascripts/app/views/ticket_zoom/edit.jst.eco b/app/assets/javascripts/app/views/ticket_zoom/edit.jst.eco index f01848e59..97b24d3b7 100644 --- a/app/assets/javascripts/app/views/ticket_zoom/edit.jst.eco +++ b/app/assets/javascripts/app/views/ticket_zoom/edit.jst.eco @@ -1,10 +1,12 @@ -
    + + +
    <%- App.User.fullLocal( @S('id') ).avatar(false, 'right', 'zIndex-5') %> -
    +
    -
    +
    @@ -16,48 +18,7 @@
    -
    -
    -
    -
    3
    -
    -
    -
    -
    - <%- @T('Recipients') %> -
    <%- @T('type') %>
    -
    -
    -
    -
    Hans Peter Baxxter
    -
    -
    To
    -
    Cc
    -
    -
    -
    -
    -
    Julia Maier
    -
    -
    To
    -
    Cc
    -
    -
    -
    -
    -
    Remo Batlogg
    -
    -
    To
    -
    Cc
    -
    -
    - - - - -
    -
    -
    +
    ">
    @@ -67,20 +28,76 @@
    +
    - +
    <%= @article.body %>
    -
    - Antwort eingeben oder - - Dateien wählen.. - - + + +
    + +
    +
    + Antwort eingeben oder + + Dateien wählen.. + + +
    +
    +
    +
    + <%- @T(' Uploading ') %> (0%) ... +
    +
    +
    <%- @T('Cancel Upload') %> +
    +
    +
    +
    +
    +
    +
    + <%- @T('Drop Files here') %> +
    +
    \ No newline at end of file diff --git a/app/assets/stylesheets/zzz.css.erb b/app/assets/stylesheets/zzz.css.erb index b12ebac80..92020ad63 100644 --- a/app/assets/stylesheets/zzz.css.erb +++ b/app/assets/stylesheets/zzz.css.erb @@ -2508,7 +2508,6 @@ footer { padding: 5px; border-radius: 8px; margin: -5px; - overflow: hidden; } .is-internal .internal-border { @@ -2844,7 +2843,7 @@ footer { .ticket-edit textarea, .ticketEdit-body { width: 100%; - /*height: 38px;*/ + position: relative; min-height: 20px; vertical-align: bottom; border: none;