Init version of new text modules.

This commit is contained in:
Martin Edenhofer 2014-10-02 08:37:24 +02:00
parent 83f5b6cabe
commit 14f636fec2
9 changed files with 777 additions and 492 deletions

View file

@ -1599,6 +1599,12 @@ class App.ControllerForm extends App.Controller
lookupForm = @findForm(form) lookupForm = @findForm(form)
# get contenteditable
for element in lookupForm.find('[contenteditable]')
name = $(element).data('name')
if name
param[name] = $(element).ceg()
# get form elements # get form elements
array = lookupForm.serializeArray() array = lookupForm.serializeArray()

View file

@ -163,6 +163,8 @@ class LayoutRefCommunicationReply extends App.ControllerContent
maxlength: 2500 maxlength: 2500
}) })
@$('[contenteditable]').textmodule()
detect_empty_textarea: => detect_empty_textarea: =>
if !@textarea.text() if !@textarea.text()
@add_textarea_catcher() @add_textarea_catcher()

View file

@ -88,9 +88,6 @@ class App.TicketZoom extends App.Controller
if newTicketRaw.updated_by_id isnt @Session.get('id') if newTicketRaw.updated_by_id isnt @Session.get('id')
App.TaskManager.notify( @task_key ) App.TaskManager.notify( @task_key )
# rerender edit box
@editDone = false
# remember current data # remember current data
@ticketUpdatedAtLastCall = newTicketRaw.updated_at @ticketUpdatedAtLastCall = newTicketRaw.updated_at
@ -163,6 +160,15 @@ class App.TicketZoom extends App.Controller
el: @el.find('.ticket-meta') 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) => editTicket = (el) =>
el.append('<form class="edit"></form>') el.append('<form class="edit"></form>')
@editEl = el @editEl = el
@ -170,7 +176,7 @@ class App.TicketZoom extends App.Controller
reset = (e) => reset = (e) =>
e.preventDefault() e.preventDefault()
App.TaskManager.update( @task_key, { 'state': {} }) @taskReset()
show(@ticket) show(@ticket)
show = (ticket) => show = (ticket) =>
@ -202,7 +208,7 @@ class App.TicketZoom extends App.Controller
form.find('[name="' + fieldNameToChange + '"]').replaceWith( newElement ) form.find('[name="' + fieldNameToChange + '"]').replaceWith( newElement )
defaults = ticket.attributes() defaults = ticket.attributes()
task_state = App.TaskManager.get(@task_key).state || {} task_state = @taskGet('ticket')
modelDiff = @getDiff( defaults, task_state ) modelDiff = @getDiff( defaults, task_state )
#if @isRole('Customer') #if @isRole('Customer')
# delete defaults['state_id'] # delete defaults['state_id']
@ -351,17 +357,13 @@ class App.TicketZoom extends App.Controller
items: items items: items
) )
@ArticleView() # show article
new ArticleView(
if force || !@editDone ticket: @ticket
# reset form on force reload ticket_article_ids: @ticket_article_ids
if force && _.isEmpty( App.TaskManager.get(@task_key).state ) el: @el.find('.ticket-article')
App.TaskManager.update( @task_key, { 'state': {} }) ui: @
@editDone = true )
# rerender widget if it hasn't changed
if !@editWidget || _.isEmpty( App.TaskManager.get(@task_key).state )
@editWidget = @Edit()
# scroll to article if given # scroll to article if given
if @article_id && document.getElementById( 'article-' + @article_id ) if @article_id && document.getElementById( 'article-' + @article_id )
@ -376,40 +378,30 @@ class App.TicketZoom extends App.Controller
@autosaveStart() @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: => autosaveStop: =>
@autosaveLast = {} @autosaveLast = {}
@clearInterval( 'autosave' ) @clearInterval( 'autosave' )
autosaveStart: => autosaveStart: =>
if !@autosaveLast if !@autosaveLast
@autosaveLast = App.TaskManager.get(@task_key).state || {} @autosaveLast = @taskGet()
update = => update = =>
currentStore = @ticket.attributes() #console.log('AR', @formParam( @el.find('.article-add') ) )
currentParams = @formParam( @el.find('.edit') ) currentStore =
ticket: @ticket.attributes()
article: {
type: ''
body: ''
internal: ''
}
currentParams =
ticket: @formParam( @el.find('.edit') )
article: @formParam( @el.find('.article-add') )
# get diff of model # 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) #console.log('modelDiff', modelDiff)
# get diff of last save # get diff of last save
@ -420,9 +412,9 @@ class App.TicketZoom extends App.Controller
console.log('model DIFF ', modelDiff) console.log('model DIFF ', modelDiff)
@autosaveLast = clone(currentParams) @autosaveLast = clone(currentParams)
@markFormDiff( modelDiff ) @markFormDiff( modelDiff.ticket )
App.TaskManager.update( @task_key, { 'state': modelDiff }) @taskUpdateAll( modelDiff )
@interval( update, 3000, 'autosave' ) @interval( update, 3000, 'autosave' )
getDiff: (model, params) => getDiff: (model, params) =>
@ -584,11 +576,35 @@ class App.TicketZoom extends App.Controller
done: (r) => done: (r) =>
# reset form after save # reset form after save
App.TaskManager.update( @task_key, { 'state': {} }) @taskReset()
@fetch( ticket.id, true ) @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 class TicketTitle extends App.Controller
events: events:
'blur .ticket-title-update': 'update' 'blur .ticket-title-update': 'update'
@ -653,38 +669,53 @@ class TicketMeta extends App.Controller
class Edit extends App.Controller class Edit extends App.Controller
elements: elements:
'textarea' : 'textarea' '.js-textarea' : 'textarea'
'.edit-control-item' : 'editControlItem' '.attachmentPlaceholder': 'attachmentPlaceholder'
'.edit-controls': 'editControls' '.attachmentPlaceholder-inputHolder': 'attachmentInputHolder'
'.recipient-picker': 'recipientPicker' '.attachmentPlaceholder-hint': 'attachmentHint'
'.recipient-list': 'recipientList' '.article-add': 'ticketEdit'
'.recipient-list .list-arrow': 'recipientListArrow' '.attachments': 'attachmentsHolder'
'.js-attachment': 'attachmentHolder' '.attachmentUpload': 'attachmentUpload'
'.js-attachment-text': 'attachmentText' '.attachmentUpload-progressBar':'progressBar'
'.bubble-placeholder-hint': 'bubblePlaceholderHint' '.js-percentage': 'progressText'
#'.edit-control-item' : 'editControlItem'
#'.edit-controls': 'editControls'
#'.recipient-picker': 'recipientPicker'
#'.recipient-list': 'recipientList'
#'.recipient-list .list-arrow': 'recipientListArrow'
events: events:
'click .submit': 'update' #'click .submit': 'update'
'click [data-type="reset"]': 'reset' 'click [data-type="reset"]': 'reset'
'click .visibility-toggle': 'toggle_visibility' 'click .visibility-toggle': 'toggleVisibility'
'click .pop-selectable': 'selectArticleType' 'click .pop-selectable': 'selectArticleType'
'click .pop-selected': 'showSelectableArticleType' 'click .pop-selected': 'showSelectableArticleType'
'focus textarea': 'open_textarea'
'input textarea': 'detect_empty_textarea'
'click .recipient-picker': 'toggle_recipients' 'click .recipient-picker': 'toggle_recipients'
'click .recipient-list': 'stopPropagation' 'click .recipient-list': 'stopPropagation'
'click .list-entry-type div': 'change_type' 'click .list-entry-type div': 'change_type'
'submit .recipient-list form': 'add_recipient' '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: -> constructor: ->
super super
@textareaHeight = @textareaHeight =
open: 148 open: 148
closed: 38 closed: 20
@dragEventCounter = 0
@attachments = []
@render() @render()
if @textarea.text().trim()
@ticketEdit.addClass('is-open')
stopPropagation: (e) -> stopPropagation: (e) ->
e.stopPropagation() e.stopPropagation()
@ -729,74 +760,36 @@ class Edit extends App.Controller
icon: 'note' icon: 'note'
}, },
] ]
console.log('DEvvvvvV', @defaults)
@html App.view('ticket_zoom/edit')( @html App.view('ticket_zoom/edit')(
ticket: ticket ticket: ticket
type: @type
articleTypes: articleTypes articleTypes: articleTypes
article: @defaults
isCustomer: @isRole('Customer') isCustomer: @isRole('Customer')
) )
@form_id = App.ControllerForm.formId() configure_attributes = [
defaults = ticket.attributes() { name: 'customer_id', display: 'Recipients', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organisation/Company', minLengt: 2, disableCreateUser: false },
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
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 @$('[data-name="body"]').ce({
#@autosaveStart() mode: 'textonly'
multiline: true
maxlength: 2500
})
@form_id = App.ControllerForm.formId()
# show text module UI # show text module UI
if !@isRole('Customer') if !@isRole('Customer')
textModule = new App.WidgetTextModule( textModule = new App.WidgetTextModule(
el: @textarea el: @el
data: data:
ticket: ticket ticket: ticket
) )
@ -873,13 +866,16 @@ class Edit extends App.Controller
console.log "add recipient", e console.log "add recipient", e
# store recipient # store recipient
toggle_visibility: -> toggleVisibility: ->
if @el.hasClass('is-public') item = @$('.article-add')
@el.removeClass('is-public') if item.hasClass('is-public')
@el.addClass('is-internal') item.removeClass('is-public')
item.addClass('is-internal')
@$('[name="internal"]').val('true')
else else
@el.addClass('is-public') item.addClass('is-public')
@el.removeClass('is-internal') item.removeClass('is-internal')
@$('[name="internal"]').val('')
showSelectableArticleType: => showSelectableArticleType: =>
@el.find('.pop-selector').removeClass('hide') @el.find('.pop-selector').removeClass('hide')
@ -903,59 +899,60 @@ class Edit extends App.Controller
if @type if @type
typeIcon.removeClass @type typeIcon.removeClass @type
@type = type @type = type
@$('[name="type"]').val(type)
typeIcon.addClass @type typeIcon.addClass @type
detect_empty_textarea: => detect_empty_textarea: =>
if !@textarea.val() if !@textarea.text().trim()
@add_textarea_catcher() @add_textarea_catcher()
else else
@remove_textarea_catcher() @remove_textarea_catcher()
open_textarea: => open_textarea: =>
if !@textareaCatcher and !@textarea.val() console.log('OT', @textareaCatcher , @textarea.text().trim() , @attachments.length)
@el.addClass('is-open') if !@textareaCatcher and !@textarea.text().trim() and !@attachments.length
@ticketEdit.addClass('is-open')
@textarea.velocity @textarea.velocity
properties: properties:
height: "#{ @textareaHeight.open - 38 }px" minHeight: "#{ @textareaHeight.open - 38 }px"
marginBottom: 38 marginBottom: 38
options: options:
duration: 300 duration: 300
easing: 'easeOutQuad' easing: 'easeOutQuad'
complete: => @add_textarea_catcher()
# scroll to bottom # scroll to bottom
@textarea.velocity "scroll", # @textarea.velocity "scroll",
container: @textarea.scrollParent() # container: @textarea.scrollParent()
offset: 99999 # offset: 99999
duration: 300 # duration: 300
easing: 'easeOutQuad' # easing: 'easeOutQuad'
queue: false # queue: false
@editControlItem.velocity "transition.slideRightIn", # @editControlItem.velocity "transition.slideRightIn",
duration: 300 # duration: 300
stagger: 50 # stagger: 50
drag: true # drag: true
# move attachment text to the left bottom (bottom happens automatically) # move attachment text to the left bottom (bottom happens automatically)
@attachmentHolder.velocity @attachmentPlaceholder.velocity
properties: properties:
translateX: -@attachmentText.position().left + "px" translateX: -@attachmentInputHolder.position().left + "px"
options: options:
duration: 300 duration: 300
easing: 'easeOutQuad' easing: 'easeOutQuad'
@bubblePlaceholderHint.velocity @attachmentHint.velocity
properties: properties:
opacity: 0 opacity: 0
options: options:
duration: 300 duration: 300
@add_textarea_catcher()
add_textarea_catcher: -> add_textarea_catcher: ->
@textareaCatcher = new App.clickCatcher @textareaCatcher = new App.clickCatcher
holder: @el.offsetParent() holder: @ticketEdit.offsetParent()
callback: @close_textarea callback: @close_textarea
zIndexScale: 4 zIndexScale: 4
@ -966,168 +963,107 @@ class Edit extends App.Controller
close_textarea: => close_textarea: =>
@remove_textarea_catcher() @remove_textarea_catcher()
if !@textarea.val() if !@textarea.text().trim() && !@attachments.length
@textarea.velocity @textarea.velocity
properties: properties:
height: "#{ @textareaHeight.closed }px" minHeight: "#{ @textareaHeight.closed }px"
marginBottom: 0 marginBottom: 0
options: options:
duration: 300 duration: 300
easing: 'easeOutQuad' easing: 'easeOutQuad'
complete: => @el.removeClass('is-open') complete: => @ticketEdit.removeClass('is-open')
@attachmentHolder.velocity @attachmentPlaceholder.velocity
properties: properties:
translateX: 0 translateX: 0
options: options:
duration: 300 duration: 300
easing: 'easeOutQuad' easing: 'easeOutQuad'
@bubblePlaceholderHint.velocity @attachmentHint.velocity
properties: properties:
opacity: 1 opacity: 1
options: options:
duration: 300 duration: 300
@editControlItem.css('display', 'none') # @editControlItem.css('display', 'none')
autosaveStop: => onDragenter: (event) =>
@clearInterval( 'autosave' ) # on the first event,
# open textarea (it will only open if its closed)
@open_textarea() if @dragEventCounter is 0
autosaveStart: => @dragEventCounter++
@autosaveLast = _.clone( @ui.formDefault ) @ticketEdit.addClass('is-dropTarget')
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' )
update: (e) => onDragleave: (event) =>
e.preventDefault() @dragEventCounter--
#@autosaveStop()
params = @formParam(e.target)
# get ticket @ticketEdit.removeClass('is-dropTarget') if @dragEventCounter is 0
ticket = App.Ticket.fullLocal( @ticket.id )
@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 # add files
if @isRole('Customer') for file in files
sender = App.TicketArticleSender.findByAttribute( 'name', 'Customer' ) @uploadQueue.push(file)
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
# update ticket @workOfUploadQueue()
for key, value of params
ticket[key] = value
# check owner assignment workOfUploadQueue: =>
if !@isRole('Customer') if !@uploadQueue.length
if !ticket['owner_id']
ticket['owner_id'] = 1
# check if title exists
if !ticket['title']
alert( App.i18n.translateContent('Title needed') )
return return
# validate email params file = @uploadQueue.shift()
if type.name is 'email' # console.log "working of", file, "from", @uploadQueue
@fakeUpload file.name, file.size, @workOfUploadQueue
# check if recipient exists humanFileSize: (size) =>
if !params['to'] && !params['cc'] i = Math.floor( Math.log(size) / Math.log(1024) )
alert( App.i18n.translateContent('Need recipient in "To" or "Cc".') ) return ( size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
return
# check if message exists updateUploadProgress: (progress) =>
if !params['body'] @progressBar.width(progress + "%")
alert( App.i18n.translateContent('Text needed') ) @progressText.text(progress)
return
# check attachment if progress is 100
if params['body'] @attachmentPlaceholder.removeClass('hide')
attachmentTranslated = App.i18n.translateContent('Attachment') @attachmentUpload.addClass('hide')
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
# submit ticket & article fakeUpload: (fileName, fileSize, callback) ->
@log 'notice', 'update ticket', ticket @attachmentPlaceholder.addClass('hide')
@attachmentUpload.removeClass('hide')
# disable form progress = 0;
@formDisable(e) duration = fileSize / 1024
# validate ticket for i in [0..100]
errors = ticket.validate( setTimeout @updateUploadProgress, i*duration/100 , i
screen: 'edit'
)
if errors
@log 'error', 'update', errors
@log 'error', errors setTimeout (=>
@formValidate( callback()
form: e.target @renderAttachment(fileName, fileSize)
errors: errors ), duration
screen: 'edit'
)
@formEnable(e)
#@autosaveStart()
return
# validate article renderAttachment: (fileName, fileSize) =>
articleAttributes = App.TicketArticle.attributesGet( 'edit' ) @attachments.push([fileName, fileSize])
if params['body'] || ( articleAttributes['body'] && articleAttributes['body']['null'] is false ) @attachmentsHolder.append App.view('ticket_zoom/attachment')
article = new App.TicketArticle fileName: fileName
params.from = @Session.get().displayName() fileSize: @humanFileSize(fileSize)
params.ticket_id = ticket.id
params.form_id = @form_id
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) => reset: (e) =>
e.preventDefault() e.preventDefault()

View file

@ -2,38 +2,31 @@ class App.WidgetTextModule extends App.Controller
constructor: -> constructor: ->
super super
@lastData = {} # remember instances
customItemTemplate = "<div><span />&nbsp;<small /></div>" @bindElements = []
elementFactory = (element, e) -> if @selector
template = $(customItemTemplate).find('span') @bindElements = @$( @selector ).textmodule()
.text(e.val).end() else
.find('small') @bindElements = @$('[contenteditable]').textmodule()
.text("(" + e.keywords + ")").end() @update()
element.append(template)
@el.parent().find('textarea').sew(
values: @reload(@data)
token: '::'
elementFactory: elementFactory
)
@subscribeId = App.TextModule.subscribe(@update, initFetch: true ) @subscribeId = App.TextModule.subscribe(@update, initFetch: true )
release: => release: =>
App.TextModule.unsubscribe(@subscribeId) App.TextModule.unsubscribe(@subscribeId)
reload: (data = false) => reload: (data) =>
if data return if !data
@lastData['data'] = data
@update() @update()
update: => update: =>
all = App.TextModule.all() allRaw = App.TextModule.all()
values = [{val: '-', keywords: '-'}] all = []
ui = @lastData || @ ui = @data || @
for item in all for item in allRaw
if item.active is true 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.' ) key = key.replace( /@/g, 'ui.data.' )
varString = "#{key}" + '' varString = "#{key}" + ''
# console.log( "tag replacement env: ", ui.data) # console.log( "tag replacement env: ", ui.data)
@ -45,16 +38,10 @@ class App.WidgetTextModule extends App.Controller
key = '' key = ''
return key return key
) )
value = { val: contentNew, keywords: item.keywords || item.name } all.push attributes
values.push value
if values.length isnt 1
values.shift()
# set new data # set new data
if @el[0] if @bindElements[0]
if $(@el[0]).data() for element in @bindElements
if $(@el[0]).data().plugin_sew if $(element).data().plugin_textmodule
$(@el[0]).data().plugin_sew.options.values = values $(element).data().plugin_textmodule.collection = all
return values

View file

@ -9,7 +9,8 @@
# #
*/ */
var DEFAULTS = { var pluginName = 'ce',
defaults = {
mode: 'richtext', mode: 'richtext',
multiline: true, multiline: true,
allowKey: { allowKey: {
@ -38,24 +39,157 @@
73: true, // i 73: true, // i
85: true, // u 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, '<br>')
}
// 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 // add/remove placeholder
var updatePlaceholder = function(target, type) { Plugin.prototype.updatePlaceholder = function(type) {
var options = target.data('ce.options') var text = this.$element.text().trim()
var text = target.text().trim() var placeholder = '<span class="placeholder">' + this.options.placeholder + '</span>'
var placeholder = '<span class="placeholder">' + options.placeholder + '</span>'
// add placholder if no text exists // add placholder if no text exists
if ( type === 'add') { if ( type === 'add') {
if ( !text ) { if ( !text ) {
target.html( placeholder ) this.$element.html( placeholder )
} }
} }
// empty placeholder text // empty placeholder text
else { else {
if ( text === options.placeholder ) { if ( text === this.options.placeholder ) {
setTimeout(function(){ setTimeout(function(){
document.execCommand('selectAll', false, ''); document.execCommand('selectAll', false, '');
document.execCommand('delete', false, ''); document.execCommand('delete', false, '');
@ -66,178 +200,85 @@
} }
} }
// max length check // disable/enable input
var maxLengthOk = function(field, typeAhead) { Plugin.prototype.input = function(type) {
var options = field.data('ce.options') if (type === 'off') {
if (!options) { this.preventInput = true
return 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) { if (typeAhead) {
length = length + 1 length = length + 1
} }
if ( length > options.maxlength ) { if ( length > this.options.maxlength ) {
field.addClass('invalid') this.$element.addClass('invalid')
setTimeout(function(){ setTimeout($.proxy(function(){
field.removeClass('invalid') this.$element.removeClass('invalid')
}, 1000); }, this), 1000)
return false return false
} }
return true return true
} }
// check if key is allowed, even if length limit is reached // check if key is allowed, even if length limit is reached
var allowKey = function(e) { Plugin.prototype.allowKey = function(e) {
var options = $(e.target).data('ce.options') if ( this.options.allowKey[ e.keyCode ] ) {
if ( options.allowKey[ e.keyCode ] ) {
return true return true
} }
if ( ( e.ctrlKey || e.metaKey ) && options.extraAllowKey[ e.keyCode ] ) { if ( ( e.ctrlKey || e.metaKey ) && this.options.extraAllowKey[ e.keyCode ] ) {
return true return true
} }
return false return false
} }
// check if rich text key is pressed // check if rich text key is pressed
var richTextKey = function(e) { Plugin.prototype.richTextKey = function(e) {
var options = $(e.target).data('ce.options') if ( ( e.ctrlKey || e.metaKey ) && this.options.richTextFormatKey[ e.keyCode ] ) {
if ( ( e.ctrlKey || e.metaKey ) && options.richTextFormatKey[ e.keyCode ] ) {
return true return true
} }
return false return false
} }
// get correct val if textbox // get value
$.fn.ceg = function(option) { Plugin.prototype.value = function() {
var options = this.data('ce.options') this.updatePlaceholder( 'remove' )
updatePlaceholder( this, 'remove' )
// get text // get text
if ( options.mode === 'textonly' ) { if ( this.options.mode === 'textonly' ) {
// strip html signes if multi line exists // strip html signes if multi line exists
if ( options.multiline ) { if ( this.options.multiline ) {
text = this.html() text = this.$element.html()
text = text.replace(/<br>/g, "\n") // new line as br text = text.replace(/<br>/g, "\n") // new line as br
text = text.replace(/<div>/g, "\n") // in some caes, new line als div text = text.replace(/<div>/g, "\n") // in some caes, new line als div
text = $("<div>" + text + "</div>").text().trim() text = $("<div>" + text + "</div>").text().trim()
return text return text
} }
return this.text().trim() return this.$element.text().trim()
} }
return this.html().trim() return this.$element.html().trim()
} }
$.fn.ce = function(option) { $.fn[pluginName] = function ( options ) {
var options = $.extend({}, DEFAULTS, option) return this.each(function () {
options.placeholder = options.placeholder || this.data('placeholder') if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName,
// store options new Plugin( this, 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 // get correct val if textbox
if ( !options.multiline ) { $.fn.ceg = function() {
this.bind('keydown', function (e) { var plugin = $.data(this[0], 'plugin_' + pluginName)
switch ( e.keyCode ) { return plugin.value()
case 13: // enter
e.preventDefault()
break;
}
})
}
} }
}(jQuery)); }(jQuery));

View file

@ -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('<div class="shortcut dropdown"><ul class="dropdown-menu" style="width: 360px; max-height: 200px;"><li><a>-</a></li></ul></div>')
this.$widget = this.$element.next()
this.updatePosition()
}
// get cursor position
Plugin.prototype.getCaretPosition = function() {
document.execCommand('insertHTML', false, '<span id="hidden"></span>');
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 = "<li><a href=\"#\" class=\"u-textTruncate\" data-id=" + item.id + ">" + item.name
if (item.keywords) {
template = template + " (" + item.keywords + ")"
}
template = template + "</a></li>"
this.$widget.find('ul').append(template)
}
if ( !result[0] ) {
this.$widget.find('ul').append("<li><a href='#'>-</a></li>")
}
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));

View file

@ -24,7 +24,7 @@
<div class="ticket-article"></div> <div class="ticket-article"></div>
<div class="ticket-edit is-public"></div> <div class="ticket-edit"></div>
</div> </div>
</div> </div>

View file

@ -1,10 +1,12 @@
<form class="article-add <% if @formChanged: %>form-changed<% end %>"> <form class="article-add <% if @article.internal: %>is-internal<% else: %>is-public<% end %>">
<input type="hidden" name="type" value="<%= @article.type %>">
<input type="hidden" name="internal" value="<%= @article.internal %>">
<div class="bubble-grid horizontal"> <div class="bubble-grid horizontal">
<div class="vertical center edit-controls"> <div class="vertical center edit-controls">
<%- App.User.fullLocal( @S('id') ).avatar(false, 'right', 'zIndex-5') %> <%- App.User.fullLocal( @S('id') ).avatar(false, 'right', 'zIndex-5') %>
<div class="dark pop-select zIndex-7 edit-control-item"> <div class="dark pop-select zIndex-7 edit-control-item" style="display: block;">
<div class="pop-selected u-clickable centered"> <div class="pop-selected u-clickable centered">
<div class="gray <%- @type %> channel icon"></div> <div class="gray <%- @article.type %> channel icon"></div>
</div> </div>
<div class="pop-selector hide"> <div class="pop-selector hide">
<div class="horizontal"> <div class="horizontal">
@ -16,48 +18,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="u-positionOrigin zIndex-7 edit-control-item"> <div class="visibility-toggle zIndex-7 u-clickable edit-control-item" style="display: block;">
<div class="recipient-picker u-clickable horizontal centered">
<div class="recipients icon"></div>
<div class="recipient-count">3</div>
</div>
<div class="recipient-list hide">
<div class="list-arrow"></div>
<div class="list-head horizontal">
<%- @T('Recipients') %>
<div class="align-right"><%- @T('type') %></div>
</div>
<div class="list-entry horizontal centered">
<div class="avatar" style="background-image: url(http://berta9.express.ge/31/performer/Paul%20van%20Dyk/.photo/34_paul_van_dyk_01.jpg)"></div>
<div class="list-entry-name flex">Hans Peter Baxxter</div>
<div class="list-entry-type u-clickable horizontal">
<div class="active" data-value="to">To</div>
<div data-value="cc" title="<%- @T('carbon copy') %>">Cc</div>
</div>
</div>
<div class="list-entry horizontal centered">
<div class="avatar" style="background-image: url(https://s3.amazonaws.com/uifaces/faces/twitter/adellecharles/48.jpg)"></div>
<div class="list-entry-name flex">Julia Maier</div>
<div class="list-entry-type u-clickable horizontal">
<div class="active" data-value="to">To</div>
<div data-value="cc" title="<%- @T('carbon copy') %>">Cc</div>
</div>
</div>
<div class="list-entry horizontal centered">
<div class="avatar" style="background-image: url(https://s3.amazonaws.com/uifaces/faces/twitter/sindresorhus/48.jpg)"></div>
<div class="list-entry-name flex">Remo Batlogg</div>
<div class="list-entry-type u-clickable horizontal">
<div class="active" data-value="to">To</div>
<div data-value="cc" title="<%- @T('carbon copy') %>">Cc</div>
</div>
</div>
<form class="list-edit">
<input type="email" class="list-entry" placeholder="<%- @T('Add recipients..') %>"></input>
<input type="submit" tabindex="-1"></input>
</form>
</div>
</div>
<div class="visibility-toggle zIndex-7 u-clickable edit-control-item">
<div class="internal-visibility centered" title="<%- @T("unset internal") %>"> <div class="internal-visibility centered" title="<%- @T("unset internal") %>">
<div class="internal visibility icon"></div> <div class="internal visibility icon"></div>
</div> </div>
@ -67,20 +28,76 @@
</div> </div>
</div> </div>
<div class="flex article-content zIndex-5 bubble-gap"> <div class="flex article-content zIndex-5 bubble-gap">
<!--
<label class="recipients"><%- @T('Recipients') %>
<div class="avatar" style="background-image: url(https://pbs.twimg.com/profile_images/1216362658/DSC_0084-p_normal.jpg)"></div>
<div class="avatar" style="background-image: url(https://pbs.twimg.com/profile_images/1216362658/DSC_0084-p_bigger.jpg)"></div>
</label>
-->
<div class="internal-border"> <div class="internal-border">
<div class="text-bubble"> <div class="text-bubble">
<div class="bubble-arrow"></div> <div class="bubble-arrow"></div>
<textarea rows="1"></textarea> <div class="js-textarea ticketEdit-body" contenteditable="true" data-name="body"><%= @article.body %></div>
<!-- .text-bubble grows with textarea (and expanding clone) --> <!-- .text-bubble grows with textarea (and expanding clone) -->
<div class="article-attachment js-attachment u-unclickable">
<span class="bubble-placeholder-hint">Antwort eingeben oder</span> <div class="shortcut dropdown">
<span class="u-highlight u-clickable edit-upload-button js-attachment-text"> <ul class="dropdown-menu" style="width: 240px; max-height: 200px;">
<li><a href="#">shortcut 1</a></li>
<li><a href="#">shortcut 2</a></li>
<li><a href="#">shortcut 3</a></li>
<li><a href="#">shortcut 4</a></li>
<li><a href="#">shortcut 5</a></li>
<li><a href="#">shortcut 6</a></li>
<li><a href="#">shortcut 7</a></li>
<li><a href="#">shortcut 8</a></li>
</ul>
</div>
<div class="attachments"></div>
<!--
</div>
<div class="attachment horizontal">
<div class="attachment-name u-highlight">sega-genesis-box.gif</div>
<div class="attachment-size">2.4mb</div>
<div class="attachment-delete js-delete align-right u-clickable">
<div class="delete icon"></div><%- @T('Delete File') %>
</div>
</div>
<div class="attachment horizontal">
<div class="attachment-name u-highlight">license-key.txt</div>
<div class="attachment-size">7kb</div>
<div class="attachment-delete js-delete align-right u-clickable">
<div class="delete icon"></div><%- @T('Delete File') %>
</div>
</div>
</div>-->
<div class="article-attachment u-unclickable">
<div class="attachmentPlaceholder">
<span class="attachmentPlaceholder-hint">Antwort eingeben oder</span>
<span class="attachmentPlaceholder-inputHolder u-highlight u-clickable">
Dateien wählen.. Dateien wählen..
<input multiple="multiple" type="file" name="file" style="position: absolute; right: 0px; top: 0px; font-family: Arial; font-size: 118px; margin: 0px; padding: 0px; cursor: pointer; opacity: 0;"> <input multiple="multiple" type="file" name="file" style="position: absolute; right: 0px; top: 0px; font-family: Arial; font-size: 118px; margin: 0px; padding: 0px; cursor: pointer; opacity: 0;">
</span> </span>
</div> </div>
<div class="attachmentUpload hide u-clickable">
<div class="horizontal">
<div class="u-highlight">
<%- @T(' Uploading ') %> (<span class="js-percentage">0</span>%) ...
</div>
<div class="attachmentUpload-cancel align-right js-cancel u-clickable">
<div class="delete icon"></div><%- @T('Cancel Upload') %>
</div> </div>
</div> </div>
<div class="attachmentUpload-progressBar" style="width: 0%"></div>
</div>
</div>
<div class="fit dropArea">
<div class="dropArea-inner fit centered">
<%- @T('Drop Files here') %>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</form> </form>

View file

@ -2508,7 +2508,6 @@ footer {
padding: 5px; padding: 5px;
border-radius: 8px; border-radius: 8px;
margin: -5px; margin: -5px;
overflow: hidden;
} }
.is-internal .internal-border { .is-internal .internal-border {
@ -2844,7 +2843,7 @@ footer {
.ticket-edit textarea, .ticket-edit textarea,
.ticketEdit-body { .ticketEdit-body {
width: 100%; width: 100%;
/*height: 38px;*/ position: relative;
min-height: 20px; min-height: 20px;
vertical-align: bottom; vertical-align: bottom;
border: none; border: none;