DiffView.coffee 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. define [
  2. "ace/ace"
  3. "ace/mode/latex"
  4. "ace/range"
  5. "moment"
  6. "libs/backbone"
  7. "libs/mustache"
  8. ], (Ace, LatexMode, Range, moment)->
  9. DiffView = Backbone.View.extend
  10. template: $("#trackChangesDiffTemplate").html()
  11. events:
  12. "click .restore": (e) ->
  13. e.preventDefault()
  14. @trigger "restore"
  15. "click .restore-deleted": (e) ->
  16. e.preventDefault()
  17. @$("a.restore-deleted").attr("disabled", true)
  18. @$("a.restore-deleted").text("Restoring...")
  19. @trigger "restore-deleted"
  20. initialize: () ->
  21. if !@model.get("doc").get("deleted")
  22. @model.on "change:diff", () => @render()
  23. @model.fetch()
  24. else
  25. @render()
  26. render: ->
  27. changes = @getNumberOfChanges()
  28. html = Mustache.to_html @template, {
  29. changes: "#{changes} change#{if changes == 1 then "" else "s"}"
  30. name: @model.get("doc")?.get("name")
  31. }
  32. @$el.html(html)
  33. if @model.get("doc").get("deleted")
  34. @$(".change-info").hide()
  35. @$(".deleted-info").show()
  36. else
  37. diff = @model.get("diff")
  38. return unless diff?
  39. if !@model.get("from")? or !@model.get("to")? or changes == 0
  40. @$(".restore").hide()
  41. @createAceEditor()
  42. @aceEditor.setValue(@getPlainDiffContent())
  43. @aceEditor.clearSelection()
  44. @$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
  45. @insertMarkers()
  46. @insertNameTag()
  47. @insertMoreChangeLabels()
  48. @bindToScrollEvents()
  49. @scrollToFirstChange()
  50. return @
  51. remove: () ->
  52. @$editor?.remove()
  53. @undelegateEvents()
  54. createAceEditor: () ->
  55. @$editor = @$(".track-changes-diff-editor")
  56. @$el.append(@$editor)
  57. @aceEditor = Ace.edit(@$editor[0])
  58. @aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
  59. @aceEditor.setReadOnly true
  60. @aceEditor.setShowPrintMargin(false)
  61. session = @aceEditor.getSession()
  62. session.setMode(new LatexMode.Mode())
  63. session.setUseWrapMode(true)
  64. @aceEditor.on "mousemove", (e) =>
  65. position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
  66. e.position = position
  67. @updateVisibleNames(e)
  68. bindToScrollEvents: () ->
  69. @aceEditor.getSession().on "changeScrollTop", (e) =>
  70. @updateMoreChangeLabels()
  71. getPlainDiffContent: () ->
  72. content = ""
  73. for entry in @model.get("diff") or []
  74. content += entry.u or entry.i or entry.d or ""
  75. return content
  76. getNumberOfChanges: () ->
  77. changes = 0
  78. for entry in @model.get("diff") or []
  79. changes += 1 if entry.i? or entry.d?
  80. return changes
  81. insertMarkers: () ->
  82. row = 0
  83. column = 0
  84. @entries = []
  85. for entry, i in @model.get("diff") or []
  86. content = entry.u or entry.i or entry.d
  87. content ||= ""
  88. lines = content.split("\n")
  89. startRow = row
  90. startColumn = column
  91. if lines.length > 1
  92. endRow = startRow + lines.length - 1
  93. endColumn = lines[lines.length - 1].length
  94. else
  95. endRow = startRow
  96. endColumn = startColumn + lines[0].length
  97. row = endRow
  98. column = endColumn
  99. range = new Range.Range(
  100. startRow, startColumn, endRow, endColumn
  101. )
  102. entry.range = range
  103. @addMarker(range, entry)
  104. if entry.i? or entry.d?
  105. @entries.push entry
  106. addMarker: (range, entry) ->
  107. session = @aceEditor.getSession()
  108. markerBackLayer = @aceEditor.renderer.$markerBack
  109. markerFrontLayer = @aceEditor.renderer.$markerFront
  110. lineHeight = @aceEditor.renderer.lineHeight
  111. dark = @_isDarkTheme()
  112. if entry.i? or entry.d?
  113. hue = entry.meta.user.hue()
  114. if entry.i?
  115. if dark
  116. style = "background-color : hsl(#{hue}, 100%, 28%);"
  117. else
  118. style = "background-color : hsl(#{hue}, 70%, 85%);"
  119. @_addMarkerWithCustomStyle session, markerBackLayer, range, "inserted-change-background", false, style
  120. if entry.d?
  121. if dark
  122. bgStyle = "background-color: hsl(#{hue}, 100%, 20%);"
  123. fgStyle = "border-bottom: 2px solid hsl(#{hue}, 100%, 60%);"
  124. else
  125. bgStyle = "background-color: hsl(#{hue}, 70%, 95%);"
  126. fgStyle = "border-bottom: 2px solid hsl(#{hue}, 70%, 40%);"
  127. fgStyle += "; height: #{Math.round(lineHeight/2) - 1}px;"
  128. @_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, bgStyle
  129. @_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-foreground", true, fgStyle
  130. _addMarkerWithCustomStyle: (session, markerLayer, range, klass, foreground, style) ->
  131. session.addMarker range, klass, (html, range, left, top, config) ->
  132. if range.isMultiLine()
  133. markerLayer.drawTextMarker(html, range, klass, config, style)
  134. else
  135. markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
  136. , foreground
  137. _isDarkTheme: () ->
  138. rgb = $(".ace_editor").css("background-color");
  139. [m, r, g, b] = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/)
  140. r = parseInt(r, 10)
  141. g = parseInt(g, 10)
  142. b = parseInt(b, 10)
  143. return r + g + b < 3 * 128
  144. insertNameTag: () ->
  145. @$nameTagEl = $("<div class='change-name-marker'></div>")
  146. @$nameTagEl.css({
  147. position: "absolute"
  148. })
  149. @$nameTagEl.hide()
  150. @$ace.append(@$nameTagEl)
  151. insertMoreChangeLabels: () ->
  152. @$changesBefore = $("<a class='changes-before' href='#'><span></span> <i class='icon-arrow-up'></a>")
  153. @$changesAfter = $("<a class='changes-after' href='#'><span></span> <i class='icon-arrow-down'></a>")
  154. @$ace.append(@$changesBefore)
  155. @$ace.append(@$changesAfter)
  156. @$changesBefore.on "click", () =>
  157. @gotoLastHiddenChangeBefore()
  158. @$changesAfter.on "click", () =>
  159. @gotoFirstHiddenChangeAfter()
  160. @updateMoreChangeLabels()
  161. scrollToFirstChange: () ->
  162. @aceEditor.scrollToLine(0)
  163. setTimeout () =>
  164. if @entries? and @entries[0]?
  165. row = @entries[0].range.start.row
  166. @aceEditor.scrollToLine(row, true, false)
  167. , 10
  168. _drawNameTag: (entry, position) ->
  169. @$nameTagEl.show()
  170. if entry.i?
  171. text = "Added by #{entry.meta.user.name()}"
  172. else if entry.d?
  173. text = "Deleted by #{entry.meta.user.name()}"
  174. date = moment(parseInt(entry.meta.end_ts, 10)).format("Do MMM YYYY, h:mm a")
  175. text += " on #{date}"
  176. @$nameTagEl.text(text)
  177. position = @aceEditor.renderer.textToScreenCoordinates(position.row, position.column)
  178. offset = @$ace.offset()
  179. position.pageX = position.pageX - offset.left
  180. position.pageY = position.pageY - offset.top
  181. height = @$ace.height()
  182. hue = entry.meta.user.hue()
  183. if @_isDarkTheme()
  184. css = { "background-color" : "hsl(#{hue}, 100%, 20%)"; }
  185. else
  186. css = { "background-color" : "hsl(#{hue}, 70%, 90%)"; }
  187. if position.pageX + @$nameTagEl.width() < @$ace.width()
  188. css["left"] = position.pageX
  189. css["right"] = "auto"
  190. else
  191. css["right"] = 0
  192. css["left"] = "auto"
  193. if position.pageY > 2 * @$nameTagEl.height()
  194. css["bottom"] = height - position.pageY
  195. css["top"] = "auto"
  196. else
  197. css["top"] = position.pageY + @aceEditor.renderer.lineHeight
  198. css["bottom"] = "auto"
  199. @$nameTagEl.css css
  200. _hideNameTag: () ->
  201. @$nameTagEl?.hide()
  202. updateVisibleNames: (e) ->
  203. visibleName = false
  204. for entry in @entries or []
  205. if entry.range.contains(e.position.row, e.position.column)
  206. @_drawNameTag(entry, e.position)
  207. visibleName = true
  208. break
  209. if !visibleName
  210. @_hideNameTag()
  211. updateMoreChangeLabels: () ->
  212. return if !@$changesBefore or !@$changesAfter
  213. setTimeout () =>
  214. firstRow = @aceEditor.getFirstVisibleRow()
  215. lastRow = @aceEditor.getLastVisibleRow()
  216. changesBefore = 0
  217. changesAfter = 0
  218. @lastHiddenChangeBefore = null
  219. @firstHiddenChangeAfter = null
  220. for entry in @entries or []
  221. if entry.range.start.row < firstRow
  222. changesBefore += 1
  223. @lastHiddenChangeBefore = entry
  224. if entry.range.end.row > lastRow
  225. changesAfter += 1
  226. @firstHiddenChangeAfter ||= entry
  227. if changesBefore > 0
  228. @$changesBefore.find("span").text("#{changesBefore} more change#{if changesBefore > 1 then "s" else ""} above")
  229. @$changesBefore.show()
  230. else
  231. @$changesBefore.hide()
  232. if changesAfter > 0
  233. @$changesAfter.find("span").text("#{changesAfter} more change#{if changesAfter > 1 then "s" else ""} below")
  234. @$changesAfter.show()
  235. else
  236. @$changesAfter.hide()
  237. , 100
  238. gotoLastHiddenChangeBefore: () ->
  239. return if !@lastHiddenChangeBefore
  240. @aceEditor.scrollToLine(@lastHiddenChangeBefore.range.start.row, true, false)
  241. gotoFirstHiddenChangeAfter: () ->
  242. return if !@firstHiddenChangeAfter
  243. @aceEditor.scrollToLine(@firstHiddenChangeAfter.range.end.row, true, false)
  244. resize: () ->
  245. @aceEditor?.resize()
  246. return DiffView