mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'track_changes_integration'
This commit is contained in:
commit
9851321429
21 changed files with 736 additions and 1942 deletions
|
@ -84,6 +84,7 @@ module.exports = (grunt) ->
|
||||||
paths:
|
paths:
|
||||||
"underscore": "libs/underscore"
|
"underscore": "libs/underscore"
|
||||||
"jquery": "libs/jquery"
|
"jquery": "libs/jquery"
|
||||||
|
"moment": "libs/moment"
|
||||||
shim:
|
shim:
|
||||||
"libs/backbone":
|
"libs/backbone":
|
||||||
deps: ["libs/underscore"]
|
deps: ["libs/underscore"]
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
request = require "request"
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
|
||||||
|
module.exports = TrackChangesController =
|
||||||
|
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
|
||||||
|
url = settings.apis.trackchanges.url + req.url
|
||||||
|
logger.log url: url, "proxying to track-changes api"
|
||||||
|
getReq = request.get(url)
|
||||||
|
getReq.pipe(res)
|
||||||
|
getReq.on "error", (error) ->
|
||||||
|
logger.error err: error, "track-changes API error"
|
||||||
|
next(error)
|
|
@ -32,6 +32,7 @@ CompileController = require("./Features/Compile/CompileController")
|
||||||
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
||||||
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
||||||
FileStoreController = require("./Features/FileStore/FileStoreController")
|
FileStoreController = require("./Features/FileStore/FileStoreController")
|
||||||
|
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
|
|
||||||
httpAuth = require('express').basicAuth (user, pass)->
|
httpAuth = require('express').basicAuth (user, pass)->
|
||||||
|
@ -122,6 +123,9 @@ module.exports = class Router
|
||||||
app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
|
app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
|
||||||
app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
|
app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
|
||||||
|
|
||||||
|
app.get "/project/:Project_id/doc/:doc_id/updates", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
|
||||||
|
app.get "/project/:Project_id/doc/:doc_id/diff", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
|
||||||
|
|
||||||
app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
|
app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
|
||||||
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
|
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,8 @@ block content
|
||||||
window.requirejs = {
|
window.requirejs = {
|
||||||
"paths" : {
|
"paths" : {
|
||||||
"underscore": "libs/underscore",
|
"underscore": "libs/underscore",
|
||||||
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"
|
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML",
|
||||||
|
"moment": "libs/moment"
|
||||||
},
|
},
|
||||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}",
|
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}",
|
||||||
"waitSeconds": 0,
|
"waitSeconds": 0,
|
||||||
|
|
|
@ -147,10 +147,11 @@
|
||||||
#editorArea(style='display: none;')
|
#editorArea(style='display: none;')
|
||||||
#editorSplitter
|
#editorSplitter
|
||||||
#leftEditorPanel.ui-layout-center
|
#leftEditorPanel.ui-layout-center
|
||||||
#editor
|
#editorWrapper
|
||||||
#undoConflictWarning(style="display: none")
|
#editor
|
||||||
| <strong>Watch out!</strong> We had to undo some of your collaborators changes before we could undo yours.
|
#undoConflictWarning(style="display: none")
|
||||||
a(href="#").js-hide Hide
|
| <strong>Watch out!</strong> We had to undo some of your collaborators changes before we could undo yours.
|
||||||
|
a(href="#").js-hide Hide
|
||||||
#rightEditorPanel.ui-layout-east
|
#rightEditorPanel.ui-layout-east
|
||||||
|
|
||||||
script(type="text/template")#loadingIndicatorTemplate
|
script(type="text/template")#loadingIndicatorTemplate
|
||||||
|
@ -403,11 +404,6 @@
|
||||||
div(class='version-message') {{message}}
|
div(class='version-message') {{message}}
|
||||||
div(class='version-date') {{date}}
|
div(class='version-date') {{date}}
|
||||||
|
|
||||||
script(type='text/template')#autoCompleteSuggestionTemplate
|
|
||||||
li
|
|
||||||
strong {{base}}
|
|
||||||
| {{completion}}
|
|
||||||
|
|
||||||
script(type='text/template')#versionListTemplate
|
script(type='text/template')#versionListTemplate
|
||||||
ul#version-list.nav.nav-pills.nav-stacked
|
ul#version-list.nav.nav-pills.nav-stacked
|
||||||
li.loading Loading...
|
li.loading Loading...
|
||||||
|
@ -427,6 +423,32 @@
|
||||||
div
|
div
|
||||||
a(href="#", title='Show Hot Keys List') Hot keys
|
a(href="#", title='Show Hot Keys List') Hot keys
|
||||||
|
|
||||||
|
script(type='text/template')#trackChangesPanelTemplate
|
||||||
|
#trackChangesPanel
|
||||||
|
.track-changes-side-bar
|
||||||
|
.track-changes-header
|
||||||
|
h3 Recent changes
|
||||||
|
a(href="#").track-changes-close
|
||||||
|
i.icon-remove
|
||||||
|
.change-list-area
|
||||||
|
.track-changes-diff
|
||||||
|
|
||||||
|
script(type='text/template')#changeListItemTemplate
|
||||||
|
div(class='change-selectors')
|
||||||
|
input(type="radio",name="fromVersion").change-selector-from
|
||||||
|
input(type="radio",name="toVersion").change-selector-to
|
||||||
|
|
||||||
|
div(class='change-description')
|
||||||
|
div(class='change-date') {{date}}
|
||||||
|
div(class='change-name')
|
||||||
|
div.color-square(style="background-color: hsl({{hue}}, 100%, 70%);")
|
||||||
|
span {{name}}
|
||||||
|
|
||||||
|
script(type='text/template')#changeListTemplate
|
||||||
|
ul.change-list.nav.nav-pills.nav-stacked
|
||||||
|
li.loading-changes Loading...
|
||||||
|
li.empty-message You haven't made any changes yet!
|
||||||
|
|
||||||
script(type='text/template')#hotKeysListTemplate
|
script(type='text/template')#hotKeysListTemplate
|
||||||
.hotkeys
|
.hotkeys
|
||||||
h3 Common
|
h3 Common
|
||||||
|
|
|
@ -73,7 +73,7 @@ define [
|
||||||
if @currentViewState != @viewOptions.splitView
|
if @currentViewState != @viewOptions.splitView
|
||||||
@currentViewState = @viewOptions.splitView
|
@currentViewState = @viewOptions.splitView
|
||||||
@leftPanel.prepend(
|
@leftPanel.prepend(
|
||||||
@editorPanel.find("#editor")
|
@editorPanel.find("#editorWrapper")
|
||||||
)
|
)
|
||||||
splitter = @editorPanel.find("#editorSplitter")
|
splitter = @editorPanel.find("#editorSplitter")
|
||||||
splitter.show()
|
splitter.show()
|
||||||
|
@ -84,7 +84,7 @@ define [
|
||||||
@_saveSplitterState()
|
@_saveSplitterState()
|
||||||
@currentViewState = @viewOptions.flatView
|
@currentViewState = @viewOptions.flatView
|
||||||
@editorPanel.prepend(
|
@editorPanel.prepend(
|
||||||
@editorPanel.find("#editor")
|
@editorPanel.find("#editorWrapper")
|
||||||
)
|
)
|
||||||
@editorPanel.find("#editorSplitter").hide()
|
@editorPanel.find("#editorSplitter").hide()
|
||||||
@aceEditor.resize(true)
|
@aceEditor.resize(true)
|
||||||
|
@ -352,3 +352,9 @@ define [
|
||||||
getCurrentDocId: () ->
|
getCurrentDocId: () ->
|
||||||
@current_doc_id
|
@current_doc_id
|
||||||
|
|
||||||
|
show: () ->
|
||||||
|
$("#editor").show()
|
||||||
|
|
||||||
|
hide: () ->
|
||||||
|
$("#editor").hide()
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ define [
|
||||||
"file-view/FileViewManager"
|
"file-view/FileViewManager"
|
||||||
"tour/IdeTour"
|
"tour/IdeTour"
|
||||||
"analytics/AnalyticsManager"
|
"analytics/AnalyticsManager"
|
||||||
|
"track-changes/TrackChangesManager"
|
||||||
"ace/ace"
|
"ace/ace"
|
||||||
"libs/jquery.color"
|
"libs/jquery.color"
|
||||||
"libs/jquery-layout"
|
"libs/jquery-layout"
|
||||||
|
@ -56,7 +57,8 @@ define [
|
||||||
BackspaceHighjack,
|
BackspaceHighjack,
|
||||||
FileViewManager,
|
FileViewManager,
|
||||||
IdeTour,
|
IdeTour,
|
||||||
AnalyticsManager
|
AnalyticsManager,
|
||||||
|
TrackChangesManager
|
||||||
) ->
|
) ->
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,6 +119,7 @@ define [
|
||||||
@cursorManager = new CursorManager(@)
|
@cursorManager = new CursorManager(@)
|
||||||
@fileViewManager = new FileViewManager(@)
|
@fileViewManager = new FileViewManager(@)
|
||||||
@analyticsManager = new AnalyticsManager(@)
|
@analyticsManager = new AnalyticsManager(@)
|
||||||
|
@trackChangesManager = new TrackChangesManager(@)
|
||||||
|
|
||||||
@setLoadingMessage("Connecting")
|
@setLoadingMessage("Connecting")
|
||||||
firstConnect = true
|
firstConnect = true
|
||||||
|
|
|
@ -1,7 +1,38 @@
|
||||||
define [
|
define [
|
||||||
|
"libs/md5"
|
||||||
"libs/backbone"
|
"libs/backbone"
|
||||||
], () ->
|
], () ->
|
||||||
User = Backbone.Model.extend {}, {
|
User = Backbone.Model.extend {
|
||||||
|
gravatarUrl: (size = 32) ->
|
||||||
|
email = @get("email").trim().toLowerCase()
|
||||||
|
hash = CryptoJS.MD5(email)
|
||||||
|
return "//www.gravatar.com/avatar/#{hash}.jpg?size=#{size}&d=mm"
|
||||||
|
|
||||||
|
OWNER_HUE: 200
|
||||||
|
hue: () ->
|
||||||
|
if window.user.id == @get("id")
|
||||||
|
hue = @OWNER_HUE
|
||||||
|
else
|
||||||
|
hash = CryptoJS.MD5(@get("id"))
|
||||||
|
hue = parseInt(hash.toString().slice(0,8), 16) % 320
|
||||||
|
# Avoid 20 degrees either side of the owner
|
||||||
|
if hue > @OWNER_HUE - 20
|
||||||
|
hue = hue + 40
|
||||||
|
return hue
|
||||||
|
|
||||||
|
name: () ->
|
||||||
|
if window.user.id == @get("id")
|
||||||
|
return "you"
|
||||||
|
parts = []
|
||||||
|
first_name = @get("first_name")
|
||||||
|
if first_name? and first_name.length > 0
|
||||||
|
parts.push first_name
|
||||||
|
last_name = @get("last_name")
|
||||||
|
if last_name? and last_name.length > 0
|
||||||
|
parts.push last_name
|
||||||
|
return parts.join(" ")
|
||||||
|
|
||||||
|
}, {
|
||||||
findOrBuild : (id, attributes) ->
|
findOrBuild : (id, attributes) ->
|
||||||
model = @find id
|
model = @find id
|
||||||
if !model?
|
if !model?
|
||||||
|
|
198
services/web/public/coffee/track-changes/ChangeListView.coffee
Normal file
198
services/web/public/coffee/track-changes/ChangeListView.coffee
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
define [
|
||||||
|
"moment"
|
||||||
|
"libs/mustache"
|
||||||
|
"libs/backbone"
|
||||||
|
], (moment)->
|
||||||
|
ChangeListView = Backbone.View.extend
|
||||||
|
template: $("#changeListTemplate").html()
|
||||||
|
|
||||||
|
events:
|
||||||
|
"scroll" : "loadUntilFull"
|
||||||
|
|
||||||
|
initialize: () ->
|
||||||
|
@itemViews = []
|
||||||
|
@atEndOfCollection = false
|
||||||
|
|
||||||
|
self = this
|
||||||
|
@collection.on "add", (model) ->
|
||||||
|
self.addItem model
|
||||||
|
@collection.on "reset", (collection) ->
|
||||||
|
self.addItem model for model in collection.models
|
||||||
|
|
||||||
|
@selectedFromIndex = 0
|
||||||
|
@selectedToIndex = 0
|
||||||
|
|
||||||
|
@render()
|
||||||
|
@hideLoading()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@$el.html Mustache.to_html @template
|
||||||
|
@$el.css
|
||||||
|
overflow: "scroll"
|
||||||
|
this
|
||||||
|
|
||||||
|
addItem: (model) ->
|
||||||
|
index = @collection.indexOf(model)
|
||||||
|
view = new ChangeListItemView(model : model)
|
||||||
|
@itemViews.push view
|
||||||
|
elementAtIndex = @$(".change-list").children()[index]
|
||||||
|
view.$el.insertBefore(elementAtIndex)
|
||||||
|
|
||||||
|
view.on "click", (e, v) =>
|
||||||
|
@selectedToIndex = index
|
||||||
|
@selectedFromIndex = index
|
||||||
|
@resetAllSelectors()
|
||||||
|
@triggerChangeDiff()
|
||||||
|
|
||||||
|
view.on "selected:to", (e, v) =>
|
||||||
|
@selectedToIndex = index
|
||||||
|
@resetAllSelectors()
|
||||||
|
@triggerChangeDiff()
|
||||||
|
|
||||||
|
view.on "selected:from", (e, v) =>
|
||||||
|
@selectedFromIndex = index
|
||||||
|
@resetAllSelectors()
|
||||||
|
@triggerChangeDiff()
|
||||||
|
|
||||||
|
view.resetSelector(index, @selectedFromIndex, @selectedToIndex)
|
||||||
|
|
||||||
|
resetAllSelectors: () ->
|
||||||
|
for view, i in @itemViews
|
||||||
|
view.resetSelector(i, @selectedFromIndex, @selectedToIndex)
|
||||||
|
|
||||||
|
triggerChangeDiff: () ->
|
||||||
|
@trigger "change_diff", @collection.models[@selectedFromIndex], @collection.models[@selectedToIndex]
|
||||||
|
|
||||||
|
listShorterThanContainer: ->
|
||||||
|
@$el.height() > @$(".change-list").height()
|
||||||
|
|
||||||
|
atEndOfListView: ->
|
||||||
|
@$el.scrollTop() + @$el.height() >= @$(".change-list").height() - 30
|
||||||
|
|
||||||
|
loadUntilFull: (e, callback) ->
|
||||||
|
if (@listShorterThanContainer() or @atEndOfListView()) and not @atEndOfCollection and not @loading
|
||||||
|
@showLoading()
|
||||||
|
@hideEmptyMessage()
|
||||||
|
@collection.fetchNextBatch
|
||||||
|
error: =>
|
||||||
|
@hideLoading()
|
||||||
|
@showEmptyMessageIfCollectionEmpty()
|
||||||
|
callback() if callback?
|
||||||
|
success: (collection, response) =>
|
||||||
|
@hideLoading()
|
||||||
|
if response.updates.length == @collection.batchSize
|
||||||
|
@loadUntilFull(e, callback)
|
||||||
|
else
|
||||||
|
@atEndOfCollection = true
|
||||||
|
@showEmptyMessageIfCollectionEmpty()
|
||||||
|
callback() if callback?
|
||||||
|
|
||||||
|
else
|
||||||
|
callback() if callback?
|
||||||
|
|
||||||
|
showEmptyMessageIfCollectionEmpty: ()->
|
||||||
|
if @collection.isEmpty()
|
||||||
|
@$(".empty-message").show()
|
||||||
|
else
|
||||||
|
@$(".empty-message").hide()
|
||||||
|
|
||||||
|
hideEmptyMessage: () ->
|
||||||
|
@$(".empty-message").hide()
|
||||||
|
|
||||||
|
showLoading: ->
|
||||||
|
@loading = true
|
||||||
|
@$(".loading-changes").show()
|
||||||
|
|
||||||
|
hideLoading: ->
|
||||||
|
@loading = false
|
||||||
|
@$(".loading-changes").hide()
|
||||||
|
|
||||||
|
ChangeListItemView = Backbone.View.extend
|
||||||
|
tagName: "li"
|
||||||
|
|
||||||
|
events:
|
||||||
|
"click .change-description" : "onClick"
|
||||||
|
"click .change-selector-from" : "onFromSelectorClick"
|
||||||
|
"click .change-selector-to" : "onToSelectorClick"
|
||||||
|
|
||||||
|
template : $("#changeListItemTemplate").html()
|
||||||
|
|
||||||
|
initialize: ->
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@$el.html Mustache.to_html(@template, @modelView())
|
||||||
|
return this
|
||||||
|
|
||||||
|
modelView: ->
|
||||||
|
modelView = {
|
||||||
|
hue: @model.get("user").hue()
|
||||||
|
date: moment(parseInt(@model.get("end_ts"), 10)).calendar()
|
||||||
|
name: @model.get("user").name()
|
||||||
|
}
|
||||||
|
# modelView.start_ts = util.formatDate(modelView.start_ts)
|
||||||
|
# modelView.end_ts = util.formatDate(modelView.end_ts)
|
||||||
|
return modelView
|
||||||
|
|
||||||
|
onClick: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@trigger "click", e, @
|
||||||
|
|
||||||
|
onToSelectorClick: (e) ->
|
||||||
|
@trigger "selected:to", e, @
|
||||||
|
|
||||||
|
onFromSelectorClick: (e) ->
|
||||||
|
@trigger "selected:from", e, @
|
||||||
|
|
||||||
|
isSelectedFrom: () ->
|
||||||
|
@$(".change-selector-from").is(":checked")
|
||||||
|
|
||||||
|
isSelectedTo: () ->
|
||||||
|
@$(".change-selector-to").is(":checked")
|
||||||
|
|
||||||
|
hideFromSelector: () ->
|
||||||
|
@$(".change-selector-from").hide()
|
||||||
|
|
||||||
|
showFromSelector: () ->
|
||||||
|
@$(".change-selector-from").show()
|
||||||
|
|
||||||
|
hideToSelector: () ->
|
||||||
|
@$(".change-selector-to").hide()
|
||||||
|
|
||||||
|
showToSelector: () ->
|
||||||
|
@$(".change-selector-to").show()
|
||||||
|
|
||||||
|
setFromChecked: (checked) ->
|
||||||
|
@$(".change-selector-from").prop("checked", checked)
|
||||||
|
|
||||||
|
setToChecked: (checked) ->
|
||||||
|
@$(".change-selector-to").prop("checked", checked)
|
||||||
|
|
||||||
|
setSelected: () ->
|
||||||
|
@$el.addClass("selected")
|
||||||
|
|
||||||
|
setUnselected: () ->
|
||||||
|
@$el.removeClass("selected")
|
||||||
|
|
||||||
|
resetSelector: (myIndex, selectedFromIndex, selectedToIndex) ->
|
||||||
|
if myIndex >= selectedToIndex
|
||||||
|
@showFromSelector()
|
||||||
|
else
|
||||||
|
@hideFromSelector()
|
||||||
|
|
||||||
|
if myIndex <= selectedFromIndex
|
||||||
|
@showToSelector()
|
||||||
|
else
|
||||||
|
@hideToSelector()
|
||||||
|
|
||||||
|
if selectedToIndex <= myIndex <= selectedFromIndex
|
||||||
|
@setSelected()
|
||||||
|
else
|
||||||
|
@setUnselected()
|
||||||
|
|
||||||
|
@setFromChecked(myIndex == selectedFromIndex)
|
||||||
|
@setToChecked(myIndex == selectedToIndex)
|
||||||
|
|
||||||
|
|
||||||
|
return ChangeListView
|
||||||
|
|
140
services/web/public/coffee/track-changes/DiffView.coffee
Normal file
140
services/web/public/coffee/track-changes/DiffView.coffee
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
define [
|
||||||
|
"ace/ace"
|
||||||
|
"ace/mode/latex"
|
||||||
|
"ace/range"
|
||||||
|
"libs/backbone"
|
||||||
|
], (Ace, LatexMode, Range)->
|
||||||
|
DiffView = Backbone.View.extend
|
||||||
|
initialize: () ->
|
||||||
|
@model.on "change:diff", () => @render()
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
diff = @model.get("diff")
|
||||||
|
return unless diff?
|
||||||
|
@createAceEditor()
|
||||||
|
@aceEditor.setValue(@getPlainDiffContent())
|
||||||
|
@aceEditor.clearSelection()
|
||||||
|
session = @aceEditor.getSession()
|
||||||
|
session.setMode(new LatexMode.Mode())
|
||||||
|
session.setUseWrapMode(true)
|
||||||
|
@insertMarkers()
|
||||||
|
return @
|
||||||
|
|
||||||
|
createAceEditor: () ->
|
||||||
|
@$el.empty()
|
||||||
|
$editor = $("<div/>")
|
||||||
|
@$el.append($editor)
|
||||||
|
@aceEditor = Ace.edit($editor[0])
|
||||||
|
@aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
|
||||||
|
@aceEditor.setReadOnly true
|
||||||
|
@aceEditor.setShowPrintMargin(false)
|
||||||
|
|
||||||
|
@aceEditor.on "mousemove", (e) =>
|
||||||
|
position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
|
||||||
|
e.position = position
|
||||||
|
@updateVisibleNames(e)
|
||||||
|
|
||||||
|
getPlainDiffContent: () ->
|
||||||
|
content = ""
|
||||||
|
for entry in @model.get("diff") or []
|
||||||
|
content += entry.u or entry.i or entry.d or ""
|
||||||
|
return content
|
||||||
|
|
||||||
|
insertMarkers: () ->
|
||||||
|
row = 0
|
||||||
|
column = 0
|
||||||
|
for entry, i in @model.get("diff") or []
|
||||||
|
content = entry.u or entry.i or entry.d
|
||||||
|
lines = content.split("\n")
|
||||||
|
startRow = row
|
||||||
|
startColumn = column
|
||||||
|
if lines.length > 1
|
||||||
|
endRow = startRow + lines.length - 1
|
||||||
|
endColumn = lines[lines.length - 1].length
|
||||||
|
else
|
||||||
|
endRow = startRow
|
||||||
|
endColumn = startColumn + lines[0].length
|
||||||
|
row = endRow
|
||||||
|
column = endColumn
|
||||||
|
|
||||||
|
range = new Range.Range(
|
||||||
|
startRow, startColumn, endRow, endColumn
|
||||||
|
)
|
||||||
|
@addMarker(range, "change-marker-#{i}", entry)
|
||||||
|
|
||||||
|
addMarker: (range, id, entry) ->
|
||||||
|
session = @aceEditor.getSession()
|
||||||
|
markerBackLayer = @aceEditor.renderer.$markerBack
|
||||||
|
markerFrontLayer = @aceEditor.renderer.$markerFront
|
||||||
|
lineHeight = @aceEditor.renderer.lineHeight
|
||||||
|
if entry.i? or entry.d?
|
||||||
|
hue = entry.meta.user.hue()
|
||||||
|
if entry.i?
|
||||||
|
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
|
||||||
|
background-color : hsl(#{hue}, 70%, 85%);
|
||||||
|
"""
|
||||||
|
tag = "Added by #{entry.meta.user.name()}"
|
||||||
|
if entry.d?
|
||||||
|
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
|
||||||
|
background-color : hsl(#{hue}, 70%, 95%);
|
||||||
|
"""
|
||||||
|
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-foreground", true, """
|
||||||
|
height: #{Math.round(lineHeight/2) - 1}px;
|
||||||
|
border-bottom: 2px solid hsl(#{hue}, 70%, 40%);
|
||||||
|
"""
|
||||||
|
tag = "Deleted by #{entry.meta.user.name()}"
|
||||||
|
|
||||||
|
date = moment(parseInt(entry.meta.end_ts, 10)).format("Do MMM YYYY, h:mm:ss a")
|
||||||
|
tag += " on #{date}"
|
||||||
|
@_addNameTag session, id, range, tag, """
|
||||||
|
background-color : hsl(#{hue}, 70%, 95%);
|
||||||
|
"""
|
||||||
|
|
||||||
|
_addMarkerWithCustomStyle: (session, markerLayer, range, klass, foreground, style) ->
|
||||||
|
session.addMarker range, klass, (html, range, left, top, config) ->
|
||||||
|
if range.isMultiLine()
|
||||||
|
markerLayer.drawTextMarker(html, range, klass, config, style)
|
||||||
|
else
|
||||||
|
markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
|
||||||
|
, foreground
|
||||||
|
|
||||||
|
_addNameTag: (session, id, range, content, style) ->
|
||||||
|
@nameMarkers ||= []
|
||||||
|
@nameMarkers.push
|
||||||
|
range: range
|
||||||
|
id: id
|
||||||
|
startRange = new Range.Range(
|
||||||
|
range.start.row, range.start.column
|
||||||
|
range.start.row, range.start.column + 1
|
||||||
|
)
|
||||||
|
session.addMarker startRange, "change-name-marker", (html, range, left, top, config) ->
|
||||||
|
html.push """
|
||||||
|
<div
|
||||||
|
id = '#{id}'
|
||||||
|
class = 'change-name-marker'
|
||||||
|
style = '
|
||||||
|
height: #{config.lineHeight}px;
|
||||||
|
top: #{top}px;
|
||||||
|
left: #{left}px;
|
||||||
|
'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="name" style="
|
||||||
|
display: none;
|
||||||
|
bottom: #{config.lineHeight + 3}px;
|
||||||
|
#{style}
|
||||||
|
">#{content}</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
, true
|
||||||
|
|
||||||
|
updateVisibleNames: (e) ->
|
||||||
|
for marker in @nameMarkers or []
|
||||||
|
if marker.range.contains(e.position.row, e.position.column)
|
||||||
|
$("##{marker.id}").find(".name").show()
|
||||||
|
else
|
||||||
|
$("##{marker.id}").find(".name").hide()
|
||||||
|
|
||||||
|
return DiffView
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
define [
|
||||||
|
"track-changes/models/ChangeList"
|
||||||
|
"track-changes/models/Diff"
|
||||||
|
"track-changes/ChangeListView"
|
||||||
|
"track-changes/DiffView"
|
||||||
|
], (ChangeList, Diff, ChangeListView, DiffView) ->
|
||||||
|
class TrackChangesManager
|
||||||
|
template: $("#trackChangesPanelTemplate").html()
|
||||||
|
|
||||||
|
constructor: (@ide) ->
|
||||||
|
@$el = $(@template)
|
||||||
|
$("#editorWrapper").append(@$el)
|
||||||
|
@hideEl()
|
||||||
|
|
||||||
|
@ide.editor.on "change:doc", () =>
|
||||||
|
@hideEl()
|
||||||
|
|
||||||
|
@$el.find(".track-changes-close").on "click", (e) =>
|
||||||
|
e.preventDefault
|
||||||
|
@hideEl()
|
||||||
|
|
||||||
|
show: () ->
|
||||||
|
@project_id = window.userSettings.project_id
|
||||||
|
@doc_id = @ide.editor.current_doc_id
|
||||||
|
@changes = new ChangeList([], doc_id: @doc_id, project_id: @project_id)
|
||||||
|
|
||||||
|
@changeListView = new ChangeListView(
|
||||||
|
collection : @changes,
|
||||||
|
el : @$el.find(".change-list-area")
|
||||||
|
)
|
||||||
|
@changeListView.render()
|
||||||
|
@changeListView.loadUntilFull()
|
||||||
|
|
||||||
|
@changeListView.on "change_diff", (fromModel, toModel) =>
|
||||||
|
@diff = new Diff({
|
||||||
|
project_id: @project_id
|
||||||
|
doc_id: @doc_id
|
||||||
|
from: fromModel.get("version")
|
||||||
|
to: toModel.get("version")
|
||||||
|
})
|
||||||
|
@diffView = new DiffView(
|
||||||
|
model: @diff
|
||||||
|
el: @$el.find(".track-changes-diff")
|
||||||
|
)
|
||||||
|
@diff.fetch()
|
||||||
|
|
||||||
|
@showEl()
|
||||||
|
|
||||||
|
showEl: ->
|
||||||
|
@ide.editor.hide()
|
||||||
|
@$el.show()
|
||||||
|
|
||||||
|
hideEl: () ->
|
||||||
|
@ide.editor.show()
|
||||||
|
@$el.hide()
|
||||||
|
|
||||||
|
return TrackChangesManager
|
|
@ -0,0 +1,12 @@
|
||||||
|
define [
|
||||||
|
"models/User"
|
||||||
|
"libs/backbone"
|
||||||
|
], (User)->
|
||||||
|
Change = Backbone.Model.extend
|
||||||
|
parse: (change) ->
|
||||||
|
return {
|
||||||
|
start_ts: change.meta.start_ts
|
||||||
|
end_ts: change.meta.end_ts
|
||||||
|
user: User.findOrBuild(change.meta.user.id, change.meta.user)
|
||||||
|
version: change.v
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
define [
|
||||||
|
"track-changes/models/Change"
|
||||||
|
"libs/backbone"
|
||||||
|
], (Change)->
|
||||||
|
ChangeList = Backbone.Collection.extend
|
||||||
|
model: Change
|
||||||
|
batchSize: 3
|
||||||
|
|
||||||
|
initialize: (models, @options) ->
|
||||||
|
|
||||||
|
url: () ->
|
||||||
|
url = "/project/#{@options.project_id}/doc/#{@options.doc_id}/updates?limit=#{@batchSize}"
|
||||||
|
if @models.length > 0
|
||||||
|
last = @models[@models.length - 1]
|
||||||
|
url += "&to=#{last.get("version") - 1}"
|
||||||
|
return url
|
||||||
|
|
||||||
|
parse: (json) ->
|
||||||
|
return json.updates
|
||||||
|
|
||||||
|
fetchNextBatch: (options = {}) ->
|
||||||
|
options.add = true
|
||||||
|
@fetch options
|
||||||
|
|
||||||
|
|
13
services/web/public/coffee/track-changes/models/Diff.coffee
Normal file
13
services/web/public/coffee/track-changes/models/Diff.coffee
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
define [
|
||||||
|
"models/User"
|
||||||
|
"libs/backbone"
|
||||||
|
], (User) ->
|
||||||
|
Diff = Backbone.Model.extend
|
||||||
|
url: () ->
|
||||||
|
"/project/#{@get("project_id")}/doc/#{@get("doc_id")}/diff?from=#{@get("from")}&to=#{@get("to")}"
|
||||||
|
|
||||||
|
parse: (diff) ->
|
||||||
|
for entry in diff.diff
|
||||||
|
if entry.meta? and entry.meta.user?
|
||||||
|
entry.meta.user = User.findOrBuild(entry.meta.user.id, entry.meta.user)
|
||||||
|
return diff
|
19
services/web/public/js/libs/md5.js
Normal file
19
services/web/public/js/libs/md5.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
CryptoJS v3.1.2
|
||||||
|
code.google.com/p/crypto-js
|
||||||
|
(c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||||
|
code.google.com/p/crypto-js/wiki/License
|
||||||
|
*/
|
||||||
|
var CryptoJS=CryptoJS||function(s,p){var m={},l=m.lib={},n=function(){},r=l.Base={extend:function(b){n.prototype=this;var h=new n;b&&h.mixIn(b);h.hasOwnProperty("init")||(h.init=function(){h.$super.init.apply(this,arguments)});h.init.prototype=h;h.$super=this;return h},create:function(){var b=this.extend();b.init.apply(b,arguments);return b},init:function(){},mixIn:function(b){for(var h in b)b.hasOwnProperty(h)&&(this[h]=b[h]);b.hasOwnProperty("toString")&&(this.toString=b.toString)},clone:function(){return this.init.prototype.extend(this)}},
|
||||||
|
q=l.WordArray=r.extend({init:function(b,h){b=this.words=b||[];this.sigBytes=h!=p?h:4*b.length},toString:function(b){return(b||t).stringify(this)},concat:function(b){var h=this.words,a=b.words,j=this.sigBytes;b=b.sigBytes;this.clamp();if(j%4)for(var g=0;g<b;g++)h[j+g>>>2]|=(a[g>>>2]>>>24-8*(g%4)&255)<<24-8*((j+g)%4);else if(65535<a.length)for(g=0;g<b;g+=4)h[j+g>>>2]=a[g>>>2];else h.push.apply(h,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,h=this.sigBytes;b[h>>>2]&=4294967295<<
|
||||||
|
32-8*(h%4);b.length=s.ceil(h/4)},clone:function(){var b=r.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var h=[],a=0;a<b;a+=4)h.push(4294967296*s.random()|0);return new q.init(h,b)}}),v=m.enc={},t=v.Hex={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++){var k=a[j>>>2]>>>24-8*(j%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j+=2)g[j>>>3]|=parseInt(b.substr(j,
|
||||||
|
2),16)<<24-4*(j%8);return new q.init(g,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++)g.push(String.fromCharCode(a[j>>>2]>>>24-8*(j%4)&255));return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j++)g[j>>>2]|=(b.charCodeAt(j)&255)<<24-8*(j%4);return new q.init(g,a)}},u=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(g){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}},
|
||||||
|
g=l.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=u.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,g=a.words,j=a.sigBytes,k=this.blockSize,m=j/(4*k),m=b?s.ceil(m):s.max((m|0)-this._minBufferSize,0);b=m*k;j=s.min(4*b,j);if(b){for(var l=0;l<b;l+=k)this._doProcessBlock(g,l);l=g.splice(0,b);a.sigBytes-=j}return new q.init(l,j)},clone:function(){var b=r.clone.call(this);
|
||||||
|
b._data=this._data.clone();return b},_minBufferSize:0});l.Hasher=g.extend({cfg:r.extend(),init:function(b){this.cfg=this.cfg.extend(b);this.reset()},reset:function(){g.reset.call(this);this._doReset()},update:function(b){this._append(b);this._process();return this},finalize:function(b){b&&this._append(b);return this._doFinalize()},blockSize:16,_createHelper:function(b){return function(a,g){return(new b.init(g)).finalize(a)}},_createHmacHelper:function(b){return function(a,g){return(new k.HMAC.init(b,
|
||||||
|
g)).finalize(a)}}});var k=m.algo={};return m}(Math);
|
||||||
|
(function(s){function p(a,k,b,h,l,j,m){a=a+(k&b|~k&h)+l+m;return(a<<j|a>>>32-j)+k}function m(a,k,b,h,l,j,m){a=a+(k&h|b&~h)+l+m;return(a<<j|a>>>32-j)+k}function l(a,k,b,h,l,j,m){a=a+(k^b^h)+l+m;return(a<<j|a>>>32-j)+k}function n(a,k,b,h,l,j,m){a=a+(b^(k|~h))+l+m;return(a<<j|a>>>32-j)+k}for(var r=CryptoJS,q=r.lib,v=q.WordArray,t=q.Hasher,q=r.algo,a=[],u=0;64>u;u++)a[u]=4294967296*s.abs(s.sin(u+1))|0;q=q.MD5=t.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])},
|
||||||
|
_doProcessBlock:function(g,k){for(var b=0;16>b;b++){var h=k+b,w=g[h];g[h]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360}var b=this._hash.words,h=g[k+0],w=g[k+1],j=g[k+2],q=g[k+3],r=g[k+4],s=g[k+5],t=g[k+6],u=g[k+7],v=g[k+8],x=g[k+9],y=g[k+10],z=g[k+11],A=g[k+12],B=g[k+13],C=g[k+14],D=g[k+15],c=b[0],d=b[1],e=b[2],f=b[3],c=p(c,d,e,f,h,7,a[0]),f=p(f,c,d,e,w,12,a[1]),e=p(e,f,c,d,j,17,a[2]),d=p(d,e,f,c,q,22,a[3]),c=p(c,d,e,f,r,7,a[4]),f=p(f,c,d,e,s,12,a[5]),e=p(e,f,c,d,t,17,a[6]),d=p(d,e,f,c,u,22,a[7]),
|
||||||
|
c=p(c,d,e,f,v,7,a[8]),f=p(f,c,d,e,x,12,a[9]),e=p(e,f,c,d,y,17,a[10]),d=p(d,e,f,c,z,22,a[11]),c=p(c,d,e,f,A,7,a[12]),f=p(f,c,d,e,B,12,a[13]),e=p(e,f,c,d,C,17,a[14]),d=p(d,e,f,c,D,22,a[15]),c=m(c,d,e,f,w,5,a[16]),f=m(f,c,d,e,t,9,a[17]),e=m(e,f,c,d,z,14,a[18]),d=m(d,e,f,c,h,20,a[19]),c=m(c,d,e,f,s,5,a[20]),f=m(f,c,d,e,y,9,a[21]),e=m(e,f,c,d,D,14,a[22]),d=m(d,e,f,c,r,20,a[23]),c=m(c,d,e,f,x,5,a[24]),f=m(f,c,d,e,C,9,a[25]),e=m(e,f,c,d,q,14,a[26]),d=m(d,e,f,c,v,20,a[27]),c=m(c,d,e,f,B,5,a[28]),f=m(f,c,
|
||||||
|
d,e,j,9,a[29]),e=m(e,f,c,d,u,14,a[30]),d=m(d,e,f,c,A,20,a[31]),c=l(c,d,e,f,s,4,a[32]),f=l(f,c,d,e,v,11,a[33]),e=l(e,f,c,d,z,16,a[34]),d=l(d,e,f,c,C,23,a[35]),c=l(c,d,e,f,w,4,a[36]),f=l(f,c,d,e,r,11,a[37]),e=l(e,f,c,d,u,16,a[38]),d=l(d,e,f,c,y,23,a[39]),c=l(c,d,e,f,B,4,a[40]),f=l(f,c,d,e,h,11,a[41]),e=l(e,f,c,d,q,16,a[42]),d=l(d,e,f,c,t,23,a[43]),c=l(c,d,e,f,x,4,a[44]),f=l(f,c,d,e,A,11,a[45]),e=l(e,f,c,d,D,16,a[46]),d=l(d,e,f,c,j,23,a[47]),c=n(c,d,e,f,h,6,a[48]),f=n(f,c,d,e,u,10,a[49]),e=n(e,f,c,d,
|
||||||
|
C,15,a[50]),d=n(d,e,f,c,s,21,a[51]),c=n(c,d,e,f,A,6,a[52]),f=n(f,c,d,e,q,10,a[53]),e=n(e,f,c,d,y,15,a[54]),d=n(d,e,f,c,w,21,a[55]),c=n(c,d,e,f,v,6,a[56]),f=n(f,c,d,e,D,10,a[57]),e=n(e,f,c,d,t,15,a[58]),d=n(d,e,f,c,B,21,a[59]),c=n(c,d,e,f,r,6,a[60]),f=n(f,c,d,e,z,10,a[61]),e=n(e,f,c,d,j,15,a[62]),d=n(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,k=a.words,b=8*this._nDataBytes,h=8*a.sigBytes;k[h>>>5]|=128<<24-h%32;var l=s.floor(b/
|
||||||
|
4294967296);k[(h+64>>>9<<4)+15]=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360;k[(h+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(k.length+1);this._process();a=this._hash;k=a.words;for(b=0;4>b;b++)h=k[b],k[b]=(h<<8|h>>>24)&16711935|(h<<24|h>>>8)&4278255360;return a},clone:function(){var a=t.clone.call(this);a._hash=this._hash.clone();return a}});r.MD5=t._createHelper(q);r.HmacMD5=t._createHmacHelper(q)})(Math);
|
File diff suppressed because one or more lines are too long
|
@ -177,25 +177,13 @@ body.editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
#editor, #orchard {
|
#editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
#orchard-toolbar {
|
#editorWrapper {
|
||||||
height: 28px;
|
height: 100%;
|
||||||
padding: 5px;
|
width: 100%;
|
||||||
background-color: white;
|
|
||||||
border-bottom: 1px solid #aaa;
|
|
||||||
select {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#orchard-editor {
|
|
||||||
position: absolute;
|
|
||||||
top: 39px;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
#rightEditorPanel {
|
#rightEditorPanel {
|
||||||
border-left: 1px solid #aaa;
|
border-left: 1px solid #aaa;
|
||||||
|
@ -212,6 +200,10 @@ body.editor {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
#trackChangesPanel {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#undoConflictWarning {
|
#undoConflictWarning {
|
||||||
|
@ -964,18 +956,6 @@ i[class*="sprite-"] {
|
||||||
background-color: #9a2b1f \9;
|
background-color: #9a2b1f \9;
|
||||||
}
|
}
|
||||||
|
|
||||||
#orchard-editor {
|
|
||||||
background: white;
|
|
||||||
> .CodeMirror > .CodeMirror-scroll > .CodeMirror-sizer > div > .CodeMirror-lines {
|
|
||||||
background-color: white;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 390pt;
|
|
||||||
min-height: 592pt;
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#errorMessages {
|
#errorMessages {
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
|
@ -998,5 +978,3 @@ i[class*="sprite-"] {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "./orchard";
|
|
||||||
|
|
|
@ -1,236 +0,0 @@
|
||||||
.editor-font {
|
|
||||||
font-size: 11pt;
|
|
||||||
line-height: 1.4em;
|
|
||||||
font-family: "Computer Modern", serif;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Computer Modern";
|
|
||||||
src: url('../font/cmunrm.otf');
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Computer Modern";
|
|
||||||
src: url('../font/cmunrb.otf');
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Computer Modern Monospace";
|
|
||||||
src: url('../font/cmuntt.otf');
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
.CodeMirror-placeholder {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
height: auto;
|
|
||||||
background: none;
|
|
||||||
.editor-font;
|
|
||||||
|
|
||||||
.cm-command, .cm-bracket {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
.cm-math {
|
|
||||||
color: purple;
|
|
||||||
font-family: "Computer Modern Monospace", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-lines {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.oe-chapter {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.8em !important;
|
|
||||||
line-height: 2em !important;
|
|
||||||
}
|
|
||||||
.oe-section {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.5em !important;
|
|
||||||
line-height: 2em !important;
|
|
||||||
}
|
|
||||||
.oe-subsection {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.3em !important;
|
|
||||||
line-height: 2em !important;
|
|
||||||
}
|
|
||||||
.oe-widget-line {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.oe-figure {
|
|
||||||
margin: 0 5px;
|
|
||||||
padding: 12px 0 4px 0;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
.ui-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.oe-figure-image-wrapper {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.oe-figure-image-placeholder {
|
|
||||||
background-color: rgb(200,200,200);
|
|
||||||
}
|
|
||||||
.oe-figure-image-placeholder.droppable-hover {
|
|
||||||
background-color: rgb(200,200,255);
|
|
||||||
}
|
|
||||||
.oe-figure-toolbar {
|
|
||||||
display: none;
|
|
||||||
text-align: right;
|
|
||||||
padding: 4px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-title {
|
|
||||||
.CodeMirror-lines {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.oe-title-title {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 40px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
.CodeMirror {
|
|
||||||
font-size: 1.6em;
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-title-author {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
.CodeMirror {
|
|
||||||
font-size: 1.2em;
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-title-abstract {
|
|
||||||
margin-top: 40px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
.oe-title-abstract-label {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-title-abstract-content {
|
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
.CodeMirror {
|
|
||||||
font-size: 0.9em;
|
|
||||||
line-height: 1.3em;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.oe-widget {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
margin: 6px 0;
|
|
||||||
}
|
|
||||||
.oe-widget-selected {
|
|
||||||
border: 1px dashed #bbb;
|
|
||||||
.oe-figure-toolbar {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.ui-wrapper {
|
|
||||||
border: 1px dashed gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-block-math {
|
|
||||||
.MathJax_Display {
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-block-math-toolbar {
|
|
||||||
text-align: right;
|
|
||||||
padding: 4px;
|
|
||||||
z-index: 100;
|
|
||||||
.bootstrap-select.btn-group {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.CodeMirror {
|
|
||||||
font-family: "Computer Modern Monospace", monospace;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.oe-non-editable {
|
|
||||||
color: gray;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-bibliography {
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// These are needed by the citation drop down too, so
|
|
||||||
// don't wrap them in .oe-bibliography
|
|
||||||
.oe-bibliography-entry {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-bibliography-year {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.oe-bibliography-author {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.oe-bibliography-title {
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-bibliography {
|
|
||||||
.oe-bibliography-header {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.8em !important;
|
|
||||||
line-height: 2em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-bibliography-entry-wrapper {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
font-family: "Computer Modern Monospace", monospace;
|
|
||||||
color: green;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-bibliography-no-entries {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oe-bibliography-error {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.bootstrap-select.oe-inline-select {
|
|
||||||
margin: 0;
|
|
||||||
vertical-align: bottom;
|
|
||||||
> .btn {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
.editor-font;
|
|
||||||
color: rgb(2, 151, 172);
|
|
||||||
}
|
|
||||||
}
|
|
116
services/web/public/stylesheets/less/trackchanges.less
Normal file
116
services/web/public/stylesheets/less/trackchanges.less
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
@changesListWidth: 250px;
|
||||||
|
@changesListPadding: 10px;
|
||||||
|
|
||||||
|
#trackChangesPanel {
|
||||||
|
.track-changes-diff {
|
||||||
|
position: absolute;
|
||||||
|
right: @changesListWidth + 1px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0px 12px;
|
||||||
|
height: 100%;
|
||||||
|
.ace_editor {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-changes-side-bar {
|
||||||
|
border-left: 1px solid #999;
|
||||||
|
height: 100%;
|
||||||
|
width: @changesListWidth;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
.track-changes-header {
|
||||||
|
background-color: black;
|
||||||
|
h3 {
|
||||||
|
color: white;
|
||||||
|
padding-left: 8px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list-area {
|
||||||
|
overflow: scroll;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
top: 30px;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleted-change-background, .deleted-change-foreground, .inserted-change, .change-name-marker {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-name-marker {
|
||||||
|
.name {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 2px 6px;
|
||||||
|
.border-radius(3px 3px 3px 3px);
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid #999;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.change-list {
|
||||||
|
li {
|
||||||
|
padding: 6px 4px;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
.change-selectors {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 6px;
|
||||||
|
.change-selector-from {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.change-selector-to {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.change-description {
|
||||||
|
padding-left: 42px;
|
||||||
|
}
|
||||||
|
.change-name {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.color-square {
|
||||||
|
display: inline-block;
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li.selected {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,7 @@
|
||||||
@import "public/stylesheets/less/navbar.less";
|
@import "public/stylesheets/less/navbar.less";
|
||||||
@import "public/stylesheets/less/footer.less";
|
@import "public/stylesheets/less/footer.less";
|
||||||
@import "public/stylesheets/less/list.less";
|
@import "public/stylesheets/less/list.less";
|
||||||
|
@import "public/stylesheets/less/trackchanges.less";
|
||||||
|
|
||||||
|
|
||||||
@import "public/stylesheets/less/fileuploader.less";
|
@import "public/stylesheets/less/fileuploader.less";
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
chai = require('chai')
|
||||||
|
chai.should()
|
||||||
|
sinon = require("sinon")
|
||||||
|
modulePath = "../../../../app/js/Features/TrackChanges/TrackChangesController"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
|
||||||
|
describe "TrackChangesController", ->
|
||||||
|
beforeEach ->
|
||||||
|
@TrackChangesController = SandboxedModule.require modulePath, requires:
|
||||||
|
"request" : @request = {}
|
||||||
|
"settings-sharelatex": @settings = {}
|
||||||
|
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
|
||||||
|
|
||||||
|
describe "proxyToTrackChangesApi", ->
|
||||||
|
beforeEach ->
|
||||||
|
@req = { url: "/mock/url" }
|
||||||
|
@res = "mock-res"
|
||||||
|
@next = sinon.stub()
|
||||||
|
@settings.apis =
|
||||||
|
trackchanges:
|
||||||
|
url: "http://trackchanges.example.com"
|
||||||
|
@proxy =
|
||||||
|
events: {}
|
||||||
|
pipe: sinon.stub()
|
||||||
|
on: (event, handler) -> @events[event] = handler
|
||||||
|
@request.get = sinon.stub().returns @proxy
|
||||||
|
@TrackChangesController.proxyToTrackChangesApi @req, @res, @next
|
||||||
|
|
||||||
|
describe "successfully", ->
|
||||||
|
it "should call the track changes api", ->
|
||||||
|
@request.get
|
||||||
|
.calledWith("#{@settings.apis.trackchanges.url}#{@req.url}")
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should pipe the response to the client", ->
|
||||||
|
@proxy.pipe
|
||||||
|
.calledWith(@res)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "with an error", ->
|
||||||
|
beforeEach ->
|
||||||
|
@proxy.events["error"].call(@proxy, @error = new Error("oops"))
|
||||||
|
|
||||||
|
it "should pass the error up the call chain", ->
|
||||||
|
@next.calledWith(@error).should.equal true
|
||||||
|
|
Loading…
Reference in a new issue