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)
# get contenteditable
for element in lookupForm.find('[contenteditable]')
name = $(element).data('name')
if name
param[name] = $(element).ceg()
# get form elements
array = lookupForm.serializeArray()

View file

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

View file

@ -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('<form class="edit"></form>')
@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
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'],
},
},
configure_attributes = [
{ name: 'customer_id', display: 'Recipients', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organisation/Company', minLengt: 2, disableCreateUser: false },
]
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
)
@ -873,13 +866,16 @@ class Edit extends App.Controller
console.log "add recipient", e
# 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()

View file

@ -2,38 +2,31 @@ class App.WidgetTextModule extends App.Controller
constructor: ->
super
@lastData = {}
customItemTemplate = "<div><span />&nbsp;<small /></div>"
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

View file

@ -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, '<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
var updatePlaceholder = function(target, type) {
var options = target.data('ce.options')
var text = target.text().trim()
var placeholder = '<span class="placeholder">' + options.placeholder + '</span>'
Plugin.prototype.updatePlaceholder = function(type) {
var text = this.$element.text().trim()
var placeholder = '<span class="placeholder">' + this.options.placeholder + '</span>'
// 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(/<br>/g, "\n") // new line as br
text = text.replace(/<div>/g, "\n") // in some caes, new line als div
text = $("<div>" + text + "</div>").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()
$.fn[pluginName] = function ( options ) {
return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName,
new Plugin( this, options ));
}
});
}
// disable multi line
if ( !options.multiline ) {
this.bind('keydown', function (e) {
switch ( e.keyCode ) {
case 13: // enter
e.preventDefault()
break;
}
})
}
// get correct val if textbox
$.fn.ceg = function() {
var plugin = $.data(this[0], 'plugin_' + pluginName)
return plugin.value()
}
}(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-edit is-public"></div>
<div class="ticket-edit"></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="vertical center edit-controls">
<%- 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="gray <%- @type %> channel icon"></div>
<div class="gray <%- @article.type %> channel icon"></div>
</div>
<div class="pop-selector hide">
<div class="horizontal">
@ -16,48 +18,7 @@
</div>
</div>
</div>
<div class="u-positionOrigin zIndex-7 edit-control-item">
<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="visibility-toggle zIndex-7 u-clickable edit-control-item" style="display: block;">
<div class="internal-visibility centered" title="<%- @T("unset internal") %>">
<div class="internal visibility icon"></div>
</div>
@ -67,20 +28,76 @@
</div>
</div>
<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="text-bubble">
<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) -->
<div class="article-attachment js-attachment u-unclickable">
<span class="bubble-placeholder-hint">Antwort eingeben oder</span>
<span class="u-highlight u-clickable edit-upload-button js-attachment-text">
<div class="shortcut dropdown">
<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..
<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>
</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 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>
</form>

View file

@ -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;