article_view.coffee 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. class App.TicketZoomArticleView extends App.Controller
  2. constructor: ->
  3. super
  4. @article_controller = {}
  5. execute: (params) ->
  6. all = []
  7. for ticket_article_id in params.ticket_article_ids
  8. if !@article_controller[ticket_article_id]
  9. el = $('<div></div>')
  10. @article_controller[ticket_article_id] = new ArticleViewItem(
  11. ticket: @ticket
  12. ticket_article_id: ticket_article_id
  13. el: el
  14. ui: @ui
  15. highligher: @highligher
  16. )
  17. all.push el
  18. @el.append( all )
  19. class ArticleViewItem extends App.Controller
  20. hasChangedAttributes: ['from', 'to', 'cc', 'subject', 'body', 'preferences']
  21. elements:
  22. '.textBubble-content': 'textBubbleContent'
  23. '.textBubble-overflowContainer': 'textBubbleOverflowContainer'
  24. events:
  25. 'click .show_toogle': 'show_toogle'
  26. 'click .textBubble': 'toggle_meta_with_delay'
  27. 'click .textBubble a': 'stopPropagation'
  28. 'click .js-unfold': 'unfold'
  29. constructor: ->
  30. super
  31. @seeMore = false
  32. @render()
  33. # set expand of text area only once
  34. @bind(
  35. 'ui::ticket::shown'
  36. (data) =>
  37. return if data.ticket_id.toString() isnt @ticket.id.toString()
  38. # set highlighter
  39. @setHighlighter()
  40. # set see more
  41. @setSeeMore()
  42. )
  43. # subscribe to changes
  44. @subscribeId = App.TicketArticle.full(@ticket_article_id, @render, false, true)
  45. release: =>
  46. App.TicketArticle.unsubscribe(@subscribeId)
  47. setHighlighter: =>
  48. return if @el.is(':hidden')
  49. # use delay do no ui blocking
  50. #@highligher.loadHighlights(@ticket_article_id)
  51. d = =>
  52. @highligher.loadHighlights(@ticket_article_id)
  53. @delay(d, 200)
  54. hasChanged: (article) =>
  55. # if no last article exists, remember it and return true
  56. if !@articleAttributesLastUpdate
  57. @articleAttributesLastUpdate = {}
  58. for item in @hasChangedAttributes
  59. @articleAttributesLastUpdate[item] = article[item]
  60. return true
  61. # compare last and current article attributes
  62. articleAttributesLastUpdateCheck = {}
  63. for item in @hasChangedAttributes
  64. articleAttributesLastUpdateCheck[item] = article[item]
  65. diff = difference(@articleAttributesLastUpdate, articleAttributesLastUpdateCheck)
  66. return false if !diff || _.isEmpty( diff )
  67. @articleAttributesLastUpdate = articleAttributesLastUpdateCheck
  68. true
  69. render: (article) =>
  70. # get articles
  71. @article = App.TicketArticle.fullLocal( @ticket_article_id )
  72. # set @el attributes
  73. if !article
  74. @el.addClass("ticket-article-item #{@article.sender.name.toLowerCase()}")
  75. @el.attr('data-id', @article.id)
  76. @el.attr('id', "article-#{@article.id}")
  77. # set internal change directly in dom, without rerender while article
  78. if !article || ( @lastArticle && @lastArticle.internal isnt @article.internal )
  79. if @article.internal is true
  80. @el.addClass('is-internal')
  81. else
  82. @el.removeClass('is-internal')
  83. # check if rerender is needed
  84. if !@hasChanged(@article)
  85. @lastArticle = @article.attributes()
  86. return
  87. # prepare html body
  88. signatureDetected = false
  89. if @article.content_type is 'text/html'
  90. @article['html'] = @article.body
  91. else
  92. # check if signature got detected in backend
  93. body = @article.body
  94. if @article.preferences && @article.preferences.signature_detection
  95. signatureDetected = '########SIGNATURE########'
  96. body = body.split("\n")
  97. body.splice(@article.preferences.signature_detection, 0, signatureDetected)
  98. body = body.join("\n")
  99. body = App.Utils.textCleanup(body)
  100. @article['html'] = App.Utils.text2html(body)
  101. if signatureDetected
  102. @article['html'] = @article['html'].replace(signatureDetected, '<span class="js-signatureMarker"></span>')
  103. else
  104. @article['html'] = App.Utils.signatureIdentify( @article['html'] )
  105. @html App.view('ticket_zoom/article_view')(
  106. ticket: @ticket
  107. article: @article
  108. isCustomer: @isRole('Customer')
  109. )
  110. new App.WidgetAvatar(
  111. el: @$('.js-avatar')
  112. user_id: @article.created_by_id
  113. size: 40
  114. )
  115. new App.TicketZoomArticleActions(
  116. el: @$('.js-article-actions')
  117. ticket: @ticket
  118. article: @article
  119. )
  120. # set see more
  121. @shown = false
  122. a = =>
  123. @setSeeMore()
  124. @delay( a, 50 )
  125. # set highlighter
  126. @setHighlighter()
  127. # set see more options
  128. setSeeMore: =>
  129. return if @el.is(':hidden')
  130. return if @shown
  131. @shown = true
  132. maxHeight = 560
  133. bubbleContent = @textBubbleContent
  134. bubbleOvervlowContainer = @textBubbleOverflowContainer
  135. # expand if see more is already clicked
  136. if @seeMore
  137. bubbleContent.css('height', 'auto')
  138. bubbleOvervlowContainer.addClass('hide')
  139. return
  140. # reset bubble heigth and "see more" opacity
  141. bubbleContent.css('height', '')
  142. bubbleOvervlowContainer.css('opacity', '')
  143. # remember offset of "see more"
  144. offsetTop = bubbleContent.find('.js-signatureMarker').position()
  145. # remember bubble heigth
  146. heigth = bubbleContent.height()
  147. if offsetTop && heigth
  148. bubbleContent.attr('data-height', heigth)
  149. bubbleContent.css('height', "#{offsetTop.top + 30}px")
  150. bubbleOvervlowContainer.removeClass('hide')
  151. else if heigth > maxHeight
  152. bubbleContent.attr('data-height', heigth)
  153. bubbleContent.css('height', "#{maxHeight}px")
  154. bubbleOvervlowContainer.removeClass('hide')
  155. else
  156. bubbleOvervlowContainer.addClass('hide')
  157. show_toogle: (e) ->
  158. e.stopPropagation()
  159. e.preventDefault()
  160. #$(e.target).hide()
  161. if $(e.target).next('div')[0]
  162. if $(e.target).next('div').hasClass('hide')
  163. $(e.target).next('div').removeClass('hide')
  164. $(e.target).text( App.i18n.translateContent('Fold in') )
  165. else
  166. $(e.target).text( App.i18n.translateContent('See more') )
  167. $(e.target).next('div').addClass('hide')
  168. stopPropagation: (e) ->
  169. e.stopPropagation()
  170. toggle_meta_with_delay: (e) =>
  171. # allow double click select
  172. # by adding a delay to the toggle
  173. if @lastClick and +new Date - @lastClick < 100
  174. clearTimeout(@toggleMetaTimeout)
  175. else
  176. @toggleMetaTimeout = setTimeout(@toggle_meta, 100, e)
  177. @lastClick = +new Date
  178. toggle_meta: (e) =>
  179. e.preventDefault()
  180. animSpeed = 300
  181. article = $(e.target).closest('.ticket-article-item')
  182. metaTopClip = article.find('.article-meta-clip.top')
  183. metaBottomClip = article.find('.article-meta-clip.bottom')
  184. metaTop = article.find('.article-content-meta.top')
  185. metaBottom = article.find('.article-content-meta.bottom')
  186. if @elementContainsSelection( article.get(0) )
  187. @stopPropagation(e)
  188. return false
  189. if !metaTop.hasClass('hide')
  190. article.removeClass('state--folde-out')
  191. # scroll back up
  192. article.velocity 'scroll',
  193. container: article.scrollParent()
  194. offset: -article.offset().top - metaTop.outerHeight()
  195. duration: animSpeed
  196. easing: 'easeOutQuad'
  197. metaTop.velocity
  198. properties:
  199. translateY: 0
  200. opacity: [ 0, 1 ]
  201. options:
  202. speed: animSpeed
  203. easing: 'easeOutQuad'
  204. complete: -> metaTop.addClass('hide')
  205. metaBottom.velocity
  206. properties:
  207. translateY: [ -metaBottom.outerHeight(), 0 ]
  208. opacity: [ 0, 1 ]
  209. options:
  210. speed: animSpeed
  211. easing: 'easeOutQuad'
  212. complete: -> metaBottom.addClass('hide')
  213. metaTopClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
  214. metaBottomClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
  215. else
  216. article.addClass('state--folde-out')
  217. metaBottom.removeClass('hide')
  218. metaTop.removeClass('hide')
  219. # balance out the top meta height by scrolling down
  220. article.velocity('scroll',
  221. container: article.scrollParent()
  222. offset: -article.offset().top + metaTop.outerHeight()
  223. duration: animSpeed
  224. easing: 'easeOutQuad'
  225. )
  226. metaTop.velocity
  227. properties:
  228. translateY: [ 0, metaTop.outerHeight() ]
  229. opacity: [ 1, 0 ]
  230. options:
  231. speed: animSpeed
  232. easing: 'easeOutQuad'
  233. metaBottom.velocity
  234. properties:
  235. translateY: [ 0, -metaBottom.outerHeight() ]
  236. opacity: [ 1, 0 ]
  237. options:
  238. speed: animSpeed
  239. easing: 'easeOutQuad'
  240. metaTopClip.velocity({ height: metaTop.outerHeight() }, animSpeed, 'easeOutQuad')
  241. metaBottomClip.velocity({ height: metaBottom.outerHeight() }, animSpeed, 'easeOutQuad')
  242. unfold: (e) ->
  243. e.preventDefault()
  244. e.stopPropagation()
  245. @seeMore = true
  246. bubbleContent = @textBubbleContent
  247. bubbleOvervlowContainer = @textBubbleOverflowContainer
  248. bubbleOvervlowContainer.velocity
  249. properties:
  250. opacity: 0
  251. options:
  252. duration: 300
  253. bubbleContent.velocity
  254. properties:
  255. height: bubbleContent.attr('data-height')
  256. options:
  257. duration: 300
  258. complete: -> bubbleOvervlowContainer.addClass('hide')
  259. isOrContains: (node, container) ->
  260. while node
  261. if node is container
  262. return true
  263. node = node.parentNode
  264. false
  265. elementContainsSelection: (el) ->
  266. sel = window.getSelection()
  267. if sel.rangeCount > 0 && sel.toString()
  268. for i in [0..sel.rangeCount-1]
  269. if !@isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)
  270. return false
  271. return true
  272. false