mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-11 03:27:19 +00:00
Display cursor name labels
This commit is contained in:
parent
70f64ee20c
commit
afb953a489
6 changed files with 205 additions and 57 deletions
services/web
app/coffee/Features/Editor
public
coffee/app/ide
editor
online-users
stylesheets/app
test/UnitTests/coffee/Editor
|
@ -112,12 +112,18 @@ module.exports = EditorController =
|
|||
return callback(error) if error?
|
||||
client.get "last_name", (error, last_name) ->
|
||||
return callback(error) if error?
|
||||
cursorData.id = client.id
|
||||
if first_name? and last_name?
|
||||
cursorData.name = first_name + " " + last_name
|
||||
else
|
||||
cursorData.name = "Anonymous"
|
||||
EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData)
|
||||
client.get "email", (error, email) ->
|
||||
return callback(error) if error?
|
||||
client.get "user_id", (error, user_id) ->
|
||||
return callback(error) if error?
|
||||
cursorData.id = client.id
|
||||
cursorData.user_id = user_id if user_id?
|
||||
cursorData.email = email if email?
|
||||
if first_name? and last_name?
|
||||
cursorData.name = first_name + " " + last_name
|
||||
else
|
||||
cursorData.name = "Anonymous"
|
||||
EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData)
|
||||
|
||||
addUserToProject: (project_id, email, privileges, callback = (error, collaborator_added)->)->
|
||||
email = email.toLowerCase()
|
||||
|
|
|
@ -4,34 +4,139 @@ define [
|
|||
Range = require("ace/range").Range
|
||||
|
||||
class AnnotationsManager
|
||||
constructor: (@$scope, @editor) ->
|
||||
constructor: (@$scope, @editor, @element) ->
|
||||
@markerIds = []
|
||||
@labels = []
|
||||
|
||||
@$scope.annotationLabel = {
|
||||
show: false
|
||||
right: "auto"
|
||||
left: "auto"
|
||||
top: "auto"
|
||||
bottom: "auto"
|
||||
backgroundColor: "black"
|
||||
text: ""
|
||||
}
|
||||
|
||||
@$scope.$watch "annotations", (value) =>
|
||||
if value?
|
||||
@redrawAnnotations()
|
||||
@redrawAnnotations()
|
||||
|
||||
@$scope.$watch "theme", (value) =>
|
||||
@redrawAnnotations()
|
||||
|
||||
@editor.on "mousemove", (e) =>
|
||||
position = @editor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
|
||||
e.position = position
|
||||
@showAnnotationLabels(position)
|
||||
|
||||
redrawAnnotations: () ->
|
||||
console.log "REDRAWING ANNOTATIONS"
|
||||
@_clearMarkers()
|
||||
@_clearLabels()
|
||||
|
||||
for annotation in @$scope.annotations or []
|
||||
do (annotation) =>
|
||||
colorScheme = @_getColorScheme(annotation.hue)
|
||||
console.log "DRAWING ANNOTATION", annotation, colorScheme
|
||||
if annotation.cursor?
|
||||
@labels.push {
|
||||
text: annotation.text
|
||||
range: new Range(
|
||||
annotation.cursor.row, annotation.cursor.column,
|
||||
annotation.cursor.row, annotation.cursor.column + 1
|
||||
)
|
||||
colorScheme: colorScheme
|
||||
snapToStartOfRange: true
|
||||
}
|
||||
@_drawCursor(annotation, colorScheme)
|
||||
|
||||
showAnnotationLabels: (position) ->
|
||||
labelToShow = null
|
||||
for label in @labels or []
|
||||
if label.range.contains(position.row, position.column)
|
||||
labelToShow = label
|
||||
|
||||
@$scope.$apply () =>
|
||||
if !labelToShow?
|
||||
@$scope.annotationLabel.show = false
|
||||
else
|
||||
$ace = $(@editor.renderer.container).find(".ace_scroller")
|
||||
# Move the label into the Ace content area so that offsets and positions are easy to calculate.
|
||||
$ace.append(@element.find(".annotation-label"))
|
||||
|
||||
if labelToShow.snapToStartOfRange
|
||||
coords = @editor.renderer.textToScreenCoordinates(labelToShow.range.start.row, labelToShow.range.start.column)
|
||||
else
|
||||
coords = @editor.renderer.textToScreenCoordinates(position.row, position.column)
|
||||
|
||||
offset = $ace.offset()
|
||||
height = $ace.height()
|
||||
coords.pageX = coords.pageX - offset.left
|
||||
coords.pageY = coords.pageY - offset.top
|
||||
|
||||
if coords.pageY > 100
|
||||
console.log "middle of page", height - coords.pageY
|
||||
top = "auto"
|
||||
bottom = height - coords.pageY
|
||||
else
|
||||
console.log "top of page", coords.pageY
|
||||
top = coords.pageY + @editor.renderer.lineHeight
|
||||
bottom = "auto"
|
||||
|
||||
left = coords.pageX
|
||||
|
||||
console.log "TOP BOTTOM", top, bottom
|
||||
|
||||
|
||||
@$scope.annotationLabel = {
|
||||
show: true
|
||||
left: left
|
||||
bottom: bottom
|
||||
top: top
|
||||
backgroundColor: labelToShow.colorScheme.labelBackgroundColor
|
||||
text: labelToShow.text
|
||||
}
|
||||
|
||||
_clearMarkers: () ->
|
||||
for marker_id in @markerIds
|
||||
@editor.getSession().removeMarker(marker_id)
|
||||
@markerIds = []
|
||||
|
||||
for annotation in @$scope.annotations or []
|
||||
do (annotation) =>
|
||||
console.log "DRAWING ANNOTATION", annotation
|
||||
@markerIds.push @editor.getSession().addMarker new Range(
|
||||
annotation.cursor.row, annotation.cursor.column,
|
||||
annotation.cursor.row, annotation.cursor.column + 1
|
||||
), "remote-cursor", (html, range, left, top, config) ->
|
||||
div = """
|
||||
<div
|
||||
class='remote-cursor custom ace_start'
|
||||
style='height: #{config.lineHeight}px; top:#{top}px; left:#{left}px;'
|
||||
>
|
||||
<div class="nubbin" style="bottom: #{config.lineHeight - 2}px"></div>
|
||||
<div class="name" style="display: none; bottom: #{config.lineHeight - 2}px">#{$('<div/>').text(annotation.text).html()}</div>
|
||||
</div>
|
||||
"""
|
||||
html.push div
|
||||
, true
|
||||
_clearLabels: () ->
|
||||
@labels = []
|
||||
|
||||
_drawCursor: (annotation, colorScheme) ->
|
||||
@markerIds.push @editor.getSession().addMarker new Range(
|
||||
annotation.cursor.row, annotation.cursor.column,
|
||||
annotation.cursor.row, annotation.cursor.column + 1
|
||||
), "remote-cursor", (html, range, left, top, config) ->
|
||||
div = """
|
||||
<div
|
||||
class='remote-cursor custom ace_start'
|
||||
style='height: #{config.lineHeight}px; top:#{top}px; left:#{left}px; border-color: #{colorScheme.cursor};'
|
||||
>
|
||||
<div class="nubbin" style="bottom: #{config.lineHeight}px; background-color: #{colorScheme.cursor};"></div>
|
||||
</div>
|
||||
"""
|
||||
html.push div
|
||||
, true
|
||||
|
||||
_getColorScheme: (hue) ->
|
||||
if @_isDarkTheme()
|
||||
return {
|
||||
cursor: "hsl(#{hue}, 100%, 50%)"
|
||||
labelBackgroundColor: "hsl(#{hue}, 100%, 50%)"
|
||||
}
|
||||
else
|
||||
return {
|
||||
cursor: "hsl(#{hue}, 100%, 50%)"
|
||||
labelBackgroundColor: "hsl(#{hue}, 100%, 50%)"
|
||||
}
|
||||
|
||||
_isDarkTheme: () ->
|
||||
rgb = @element.find(".ace_editor").css("background-color");
|
||||
[m, r, g, b] = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/)
|
||||
r = parseInt(r, 10)
|
||||
g = parseInt(g, 10)
|
||||
b = parseInt(b, 10)
|
||||
return r + g + b < 3 * 128
|
|
@ -39,10 +39,10 @@ define [
|
|||
|
||||
editor = Ace.edit(element.find(".ace-editor-body")[0])
|
||||
|
||||
autoCompleteManager = new AutoCompleteManager(scope, editor)
|
||||
spellCheckManager = new SpellCheckManager(scope, editor, element)
|
||||
undoManager = new UndoManager(scope, editor)
|
||||
annotationsManagaer = new AnnotationsManager(scope, editor)
|
||||
autoCompleteManager = new AutoCompleteManager(scope, editor, element)
|
||||
spellCheckManager = new SpellCheckManager(scope, editor, element)
|
||||
undoManager = new UndoManager(scope, editor, element)
|
||||
annotationsManagaer = new AnnotationsManager(scope, editor, element)
|
||||
|
||||
# Prevert Ctrl|Cmd-S from triggering save dialog
|
||||
editor.commands.addCommand
|
||||
|
@ -91,6 +91,7 @@ define [
|
|||
session.setMode(new LatexMode())
|
||||
|
||||
autoCompleteManager.bindToSession(session)
|
||||
annotationsManagaer.redrawAnnotations()
|
||||
|
||||
doc = session.getDocument()
|
||||
doc.on "change", () ->
|
||||
|
@ -138,6 +139,20 @@ define [
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="annotation-label"
|
||||
ng-show="annotationLabel.show"
|
||||
ng-style="{
|
||||
position: 'absolute',
|
||||
left: annotationLabel.left,
|
||||
right: annotationLabel.right,
|
||||
bottom: annotationLabel.bottom,
|
||||
top: annotationLabel.top,
|
||||
'background-color': annotationLabel.backgroundColor
|
||||
}"
|
||||
>
|
||||
{{ annotationLabel.text }}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
define [], () ->
|
||||
define [
|
||||
"../../../libs/md5"
|
||||
], () ->
|
||||
class OnlineUsersManager
|
||||
constructor: (@ide, @$scope) ->
|
||||
@$scope.onlineUsers = {}
|
||||
|
@ -17,6 +19,7 @@ define [], () ->
|
|||
@updateCursorHighlights()
|
||||
|
||||
@ide.socket.on "clientTracking.clientDisconnected", (client_id) =>
|
||||
console.log "CLIENT DISCONNECTED", client_id
|
||||
@$scope.$apply () =>
|
||||
delete @$scope.onlineUsers[client_id]
|
||||
@updateCursorHighlights()
|
||||
|
@ -26,12 +29,14 @@ define [], () ->
|
|||
@$scope.onlineUserCursorAnnotations = {}
|
||||
for client_id, client of @$scope.onlineUsers
|
||||
doc_id = client.doc_id
|
||||
continue if !doc_id?
|
||||
@$scope.onlineUserCursorAnnotations[doc_id] ||= []
|
||||
@$scope.onlineUserCursorAnnotations[doc_id].push {
|
||||
text: client.name
|
||||
cursor:
|
||||
row: client.row
|
||||
column: client.column
|
||||
hue: @getHueForUserId(client.user_id)
|
||||
}
|
||||
|
||||
UPDATE_INTERVAL: 500
|
||||
|
@ -52,5 +57,20 @@ define [], () ->
|
|||
, @UPDATE_INTERVAL
|
||||
else
|
||||
console.log "NOT UPDATING"
|
||||
|
||||
|
||||
OWN_HUE: 200 # We will always appear as this color to ourselves
|
||||
ANONYMOUS_HUE: 100
|
||||
getHueForUserId: (user_id) ->
|
||||
if !user_id?
|
||||
return @ANONYMOUS_HUE
|
||||
|
||||
if window.user.id == user_id
|
||||
return @OWN_HUE
|
||||
|
||||
hash = CryptoJS.MD5(user_id)
|
||||
hue = parseInt(hash.toString().slice(0,8), 16) % 320
|
||||
# Avoid 20 degrees either side of the personal hue
|
||||
if hue > @OWNER_HUE - 20
|
||||
hue = hue + 40
|
||||
return hue
|
||||
|
||||
|
|
|
@ -193,28 +193,25 @@
|
|||
background-repeat: repeat-x;
|
||||
background-position: bottom left;
|
||||
}
|
||||
@cursor-color: rgb(14, 158, 0);
|
||||
.remote-cursor {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
border-left: 2px solid @cursor-color;
|
||||
.name {
|
||||
font-size: 0.8em;
|
||||
background-color: @cursor-color;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px 3px 3px 0;
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
}
|
||||
border-left: 2px solid transparent;
|
||||
.nubbin {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background-color: @cursor-color;
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
left: -2px;
|
||||
}
|
||||
}
|
||||
.annotation-label {
|
||||
padding: (@line-height-computed / 4) (@line-height-computed / 2);
|
||||
font-size: 0.8rem;
|
||||
z-index: 100;
|
||||
font-family: @font-family-sans-serif;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-layout-resizer {
|
||||
|
|
|
@ -246,15 +246,16 @@ describe "EditorController", ->
|
|||
row: @row = 42
|
||||
column: @column = 37
|
||||
}
|
||||
@clientParams = {
|
||||
project_id: @project_id
|
||||
first_name: @first_name = "Douglas"
|
||||
last_name: @last_name = "Adams"
|
||||
}
|
||||
@client.get = (param, callback) => callback null, @clientParams[param]
|
||||
|
||||
describe "with a logged in user", ->
|
||||
beforeEach ->
|
||||
@clientParams = {
|
||||
project_id: @project_id
|
||||
first_name: @first_name = "Douglas"
|
||||
last_name: @last_name = "Adams"
|
||||
email: @email = "joe@example.com"
|
||||
user_id: @user_id = "user-id-123"
|
||||
}
|
||||
@client.get = (param, callback) => callback null, @clientParams[param]
|
||||
@EditorController.updateClientPosition @client, @update
|
||||
|
||||
it "should send the update to the project room with the user's name", ->
|
||||
|
@ -265,13 +266,17 @@ describe "EditorController", ->
|
|||
name: "#{@first_name} #{@last_name}"
|
||||
row: @row
|
||||
column: @column
|
||||
email: @email
|
||||
user_id: @user_id
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
describe "with an anonymous user", ->
|
||||
beforeEach ->
|
||||
@clientParams.first_name = null
|
||||
@clientParams.last_name = null
|
||||
@clientParams = {
|
||||
project_id: @project_id
|
||||
}
|
||||
@client.get = (param, callback) => callback null, @clientParams[param]
|
||||
@EditorController.updateClientPosition @client, @update
|
||||
|
||||
it "should send the update to the project room with an anonymous name", ->
|
||||
|
|
Loading…
Add table
Reference in a new issue