Merge pull request #1168 from sharelatex/cmg-range-prototype

Rich text track changes pt. 1 view changes

GitOrigin-RevId: a50ba1491a46525894b32b87a8c05be0af90084d
This commit is contained in:
Chrystal Maria Griffiths 2019-01-30 13:50:04 +00:00 committed by sharelatex
parent dc60d6b630
commit d9692b7f91
15 changed files with 1023 additions and 955 deletions

View file

@ -22,7 +22,8 @@
"$": true,
"angular": true,
// Injected in layout.pug
"user_id": true
"user_id": true,
"ace": true
},
"settings": {
// Tell eslint-plugin-react to detect which version of React we are using

View file

@ -387,7 +387,7 @@ module.exports = ProjectController =
editorThemes: THEME_LIST
maxDocLength: Settings.max_doc_length
useV2History: !!project.overleaf?.history?.display
richTextEnabled: Features.hasFeature('rich-text')
richTextTrackChangesEnabled: req.query?.rttc == 'true'
showTestControls: req.query?.tc == 'true' || user.isAdmin
brandVariation: brandVariation
allowedImageNames: Settings.allowedImageNames || []

View file

@ -24,9 +24,6 @@ module.exports = Features =
return !Settings.overleaf?
when 'affiliations'
return Settings?.apis?.v1?.url?
when 'rich-text'
isEnabled = true # Switch to false to disable
Settings.overleaf? and isEnabled
when 'redirect-sl'
return Settings.redirectToV2?
when 'force-import-to-v2'

View file

@ -134,7 +134,7 @@ block requirejs
window.maxDocLength = #{maxDocLength};
window.trackChangesState = data.trackChangesState;
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.richTextEnabled = #{richTextEnabled}
window.richTextTrackChangesEnabled = #{richTextTrackChangesEnabled}
window.gitBridgePublicBaseUrl = '#{gitBridgePublicBaseUrl}'
window.requirejs = {
"paths" : {

View file

@ -43,7 +43,6 @@ div.full-size(
ace-editor="editor",
ng-if="!editor.showRichText",
ng-show="!!editor.sharejs_doc && !editor.opening",
style=richTextEnabled ? "top: 32px" : "",
theme="settings.editorTheme",
keybindings="settings.mode",
font-size="settings.fontSize",
@ -69,7 +68,6 @@ div.full-size(
syntax-validation="settings.syntaxValidation",
review-panel="reviewPanel",
events-bridge="reviewPanelEventsBridge"
track-changes-enabled="project.features.trackChangesVisible",
track-changes= "editor.trackChanges",
doc-id="editor.open_doc_id"
renderer-data="reviewPanel.rendererData"

View file

@ -1,4 +1,4 @@
#review-panel(style=richTextEnabled ? "top: 32px" : "")
#review-panel
.rp-in-editor-widgets
a.rp-track-changes-indicator(
href

View file

@ -83,9 +83,6 @@ define([
}
showRichText() {
if (!window.richTextEnabled) {
return false
}
return (
this.localStorage(`editor.mode.${this.$scope.project_id}`) ===
'rich-text'

View file

@ -11,9 +11,9 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define([], function() {
let AceShareJsCodec
return (AceShareJsCodec = {
aceRangeToShareJs(range, lines) {
let EditorShareJsCodec
return (EditorShareJsCodec = {
rangeToShareJs(range, lines) {
let offset = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
@ -23,8 +23,8 @@ define([], function() {
return offset
},
aceChangeToShareJs(delta, lines) {
const offset = AceShareJsCodec.aceRangeToShareJs(delta.start, lines)
changeToShareJs(delta, lines) {
const offset = EditorShareJsCodec.rangeToShareJs(delta.start, lines)
const text = delta.lines.join('\n')
switch (delta.action) {
@ -37,7 +37,7 @@ define([], function() {
}
},
shareJsOffsetToAcePosition(offset, lines) {
shareJsOffsetToRowColumn(offset, lines) {
let row = 0
for (row = 0; row < lines.length; row++) {
const line = lines[row]

View file

@ -1,18 +1,7 @@
/* global _ */
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-undef,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
max-len
*/
define([
'base',
@ -28,6 +17,7 @@ define([
'ide/editor/directives/aceEditor/cursor-position/CursorPositionManager',
'ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter',
'ide/editor/directives/aceEditor/track-changes/TrackChangesManager',
'ide/editor/directives/aceEditor/track-changes/TrackChangesAdapter',
'ide/editor/directives/aceEditor/metadata/MetadataManager',
'ide/metadata/services/metadata',
'ide/graphics/services/graphics',
@ -47,6 +37,7 @@ define([
CursorPositionManager,
CursorPositionAdapter,
TrackChangesManager,
TrackChangesAdapter,
MetadataManager
) {
let syntaxValidationEnabled
@ -119,7 +110,6 @@ define([
reviewPanel: '=',
eventsBridge: '=',
trackChanges: '=',
trackChangesEnabled: '=',
docId: '=',
rendererData: '=',
lineHeight: '=',
@ -177,6 +167,7 @@ define([
)
}
/* eslint-disable no-unused-vars */
const undoManager = new UndoManager(scope, editor, element)
const highlightsManager = new HighlightsManager(scope, editor, element)
const cursorPositionManager = new CursorPositionManager(
@ -187,8 +178,10 @@ define([
const trackChangesManager = new TrackChangesManager(
scope,
editor,
element
element,
new TrackChangesAdapter(editor)
)
const metadataManager = new MetadataManager(
scope,
editor,
@ -205,6 +198,8 @@ define([
files
)
/* eslint-enable no-unused-vars */
scope.$watch('onSave', function(callback) {
if (callback != null) {
Vim.defineEx('write', 'w', callback)
@ -387,10 +382,6 @@ define([
cursorPosition.row,
cursorPosition.column
)
const screenPos = editor.renderer.textToScreenCoordinates(
sessionPos.row,
sessionPos.column
)
return (
sessionPos.row * editor.renderer.lineHeight - session.getScrollTop()
)
@ -460,12 +451,21 @@ define([
)
scope.$watch('fontFamily', function(value) {
const monospaceFamilies = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
if (value != null) {
switch (value) {
case 'monaco':
return editor.setOption(
'fontFamily',
'"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace'
monospaceFamilies.join(', ')
)
case 'lucida':
return editor.setOption(
@ -598,7 +598,9 @@ define([
if (!spellCheckManager) return
spellCheckManager.init()
editor.on('changeSession', onSessionChangeForSpellCheck)
onSessionChangeForSpellCheck({ session: editor.getSession() }) // Force initial setup
onSessionChangeForSpellCheck({
session: editor.getSession()
}) // Force initial setup
return editor.on('nativecontextmenu', spellCheckManager.onContextMenu)
}
@ -611,6 +613,36 @@ define([
)
}
const initTrackChanges = function() {
trackChangesManager.rangesTracker = scope.sharejsDoc.ranges
// Force onChangeSession in order to set up highlights etc.
trackChangesManager.onChangeSession()
if (!trackChangesManager) return
editor.on('changeSelection', trackChangesManager.onChangeSelection)
// Selection also moves with updates elsewhere in the document
editor.on('change', trackChangesManager.onChangeSelection)
editor.on('changeSession', trackChangesManager.onChangeSession)
editor.on('cut', trackChangesManager.onCut)
editor.on('paste', trackChangesManager.onPaste)
editor.renderer.on('resize', trackChangesManager.onResize)
}
const tearDownTrackChanges = function() {
if (!trackChangesManager) return
this.trackChangesManager.tearDown()
editor.off('changeSelection', trackChangesManager.onChangeSelection)
editor.off('change', trackChangesManager.onChangeSelection)
editor.off('changeSession', trackChangesManager.onChangeSession)
editor.off('cut', trackChangesManager.onCut)
editor.off('paste', trackChangesManager.onPaste)
editor.renderer.off('resize', trackChangesManager.onResize)
}
const onSessionChangeForCursorPosition = function(e) {
if (e.oldSession != null) {
e.oldSession.selection.off(
@ -629,7 +661,10 @@ define([
const initCursorPosition = function() {
editor.on('changeSession', onSessionChangeForCursorPosition)
onSessionChangeForCursorPosition({ session: editor.getSession() }) // Force initial setup
// Force initial setup
onSessionChangeForCursorPosition({ session: editor.getSession() })
return $(window).on('unload', onUnloadForCursorPosition)
}
@ -690,8 +725,10 @@ define([
session.setOption('useWorker', scope.syntaxValidation)
}
// set to readonly until document change handlers are attached
editor.setReadOnly(true)
// now attach session to editor
editor.setReadOnly(true) // set to readonly until document change handlers are attached
editor.setSession(session)
const doc = session.getDocument()
@ -700,10 +737,13 @@ define([
editor.initing = true
sharejs_doc.attachToAce(editor)
editor.initing = false
// now ready to edit document
editor.setReadOnly(scope.readOnly) // respect the readOnly setting, normally false
// respect the readOnly setting, normally false
editor.setReadOnly(scope.readOnly)
triggerEditorInitEvent()
initSpellCheck()
initTrackChanges()
resetScrollMargins()
@ -753,6 +793,7 @@ define([
var detachFromAce = function(sharejs_doc) {
tearDownSpellCheck()
tearDownTrackChanges()
sharejs_doc.detachFromAce()
sharejs_doc.off('remoteop.recordRemote')

View file

@ -10,7 +10,7 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(['ide/editor/AceShareJsCodec'], function(AceShareJsCodec) {
define(['ide/editor/EditorShareJsCodec'], function(EditorShareJsCodec) {
let CursorPositionAdapter
return (CursorPositionAdapter = class CursorPositionAdapter {
constructor(editor) {
@ -50,7 +50,10 @@ define(['ide/editor/AceShareJsCodec'], function(AceShareJsCodec) {
.getSession()
.getDocument()
.getAllLines()
const position = AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
const position = EditorShareJsCodec.shareJsOffsetToRowColumn(
offset,
lines
)
return this.gotoLine(position.row + 1, position.column)
}
})

View file

@ -0,0 +1,253 @@
/* eslint-disable
camelcase
*/
define(['ace/ace', 'ide/editor/EditorShareJsCodec'], function(
_ignore,
EditorShareJsCodec
) {
const { Range } = ace.require('ace/range')
class TrackChangesAdapter {
constructor(editor) {
this.editor = editor
this.changeIdToMarkerIdMap = {}
}
tearDown() {
this.changeIdToMarkerIdMap = {}
}
clearAnnotations() {
const session = this.editor.getSession()
for (let change_id in this.changeIdToMarkerIdMap) {
const markers = this.changeIdToMarkerIdMap[change_id]
for (let marker_name in markers) {
const marker_id = markers[marker_name]
session.removeMarker(marker_id)
}
}
this.changeIdToMarkerIdMap = {}
}
onInsertAdded(change) {
const start = this.shareJsOffsetToRowColumn(change.op.p)
const end = this.shareJsOffsetToRowColumn(
change.op.p + change.op.i.length
)
const session = this.editor.getSession()
const background_range = new Range(
start.row,
start.column,
end.row,
end.column
)
const background_marker_id = session.addMarker(
background_range,
'track-changes-marker track-changes-added-marker',
'text'
)
const callout_marker_id = this.createCalloutMarker(
start,
'track-changes-added-marker-callout'
)
this.changeIdToMarkerIdMap[change.id] = {
background_marker_id,
callout_marker_id
}
}
onDeleteAdded(change) {
const position = this.shareJsOffsetToRowColumn(change.op.p)
const session = this.editor.getSession()
const markerLayer = this.editor.renderer.$markerBack
const klass = 'track-changes-marker track-changes-deleted-marker'
const background_range = this.makeZeroWidthRange(position)
const background_marker_id = session.addMarker(
background_range,
klass,
(html, range, left, top, config) =>
markerLayer.drawSingleLineMarker(
html,
range,
`${klass} ace_start`,
config,
0,
''
)
)
const callout_marker_id = this.createCalloutMarker(
position,
'track-changes-deleted-marker-callout'
)
this.changeIdToMarkerIdMap[change.id] = {
background_marker_id,
callout_marker_id
}
}
onInsertRemoved(change) {
const {
background_marker_id,
callout_marker_id
} = this.changeIdToMarkerIdMap[change.id]
delete this.changeIdToMarkerIdMap[change.id]
const session = this.editor.getSession()
session.removeMarker(background_marker_id)
session.removeMarker(callout_marker_id)
}
onDeleteRemoved(change) {
const {
background_marker_id,
callout_marker_id
} = this.changeIdToMarkerIdMap[change.id]
delete this.changeIdToMarkerIdMap[change.id]
const session = this.editor.getSession()
session.removeMarker(background_marker_id)
session.removeMarker(callout_marker_id)
}
onChangeMoved(change) {
let end
const start = this.shareJsOffsetToRowColumn(change.op.p)
if (change.op.i != null) {
end = this.shareJsOffsetToRowColumn(change.op.p + change.op.i.length)
} else {
end = start
}
this.updateMarker(change.id, start, end)
}
onCommentAdded(comment) {
if (this.changeIdToMarkerIdMap[comment.id] == null) {
// Only create new markers if they don't already exist
const start = this.shareJsOffsetToRowColumn(comment.op.p)
const end = this.shareJsOffsetToRowColumn(
comment.op.p + comment.op.c.length
)
const session = this.editor.getSession()
const background_range = new Range(
start.row,
start.column,
end.row,
end.column
)
const background_marker_id = session.addMarker(
background_range,
'track-changes-marker track-changes-comment-marker',
'text'
)
const callout_marker_id = this.createCalloutMarker(
start,
'track-changes-comment-marker-callout'
)
this.changeIdToMarkerIdMap[comment.id] = {
background_marker_id,
callout_marker_id
}
}
}
onCommentMoved(comment) {
const start = this.shareJsOffsetToRowColumn(comment.op.p)
const end = this.shareJsOffsetToRowColumn(
comment.op.p + comment.op.c.length
)
this.updateMarker(comment.id, start, end)
}
onCommentRemoved(comment) {
if (this.changeIdToMarkerIdMap[comment.id] != null) {
// Resolved comments may not have marker ids
const {
background_marker_id,
callout_marker_id
} = this.changeIdToMarkerIdMap[comment.id]
delete this.changeIdToMarkerIdMap[comment.id]
const session = this.editor.getSession()
session.removeMarker(background_marker_id)
session.removeMarker(callout_marker_id)
}
}
updateMarker(change_id, start, end) {
if (this.changeIdToMarkerIdMap[change_id] == null) {
return
}
const session = this.editor.getSession()
const markers = session.getMarkers()
const {
background_marker_id,
callout_marker_id
} = this.changeIdToMarkerIdMap[change_id]
if (
background_marker_id != null &&
markers[background_marker_id] != null
) {
const background_marker = markers[background_marker_id]
background_marker.range.start = start
background_marker.range.end = end
}
if (callout_marker_id != null && markers[callout_marker_id] != null) {
const callout_marker = markers[callout_marker_id]
callout_marker.range.start = start
callout_marker.range.end = start
}
}
shareJsOffsetToRowColumn(offset) {
const lines = this.editor
.getSession()
.getDocument()
.getAllLines()
return EditorShareJsCodec.shareJsOffsetToRowColumn(offset, lines)
}
createCalloutMarker(position, klass) {
const session = this.editor.getSession()
const callout_range = this.makeZeroWidthRange(position)
const markerLayer = this.editor.renderer.$markerBack
return session.addMarker(
callout_range,
klass,
(html, range, left, top, config) =>
markerLayer.drawSingleLineMarker(
html,
range,
`track-changes-marker-callout ${klass} ace_start`,
config,
0,
'width: auto; right: 0;'
)
)
}
makeZeroWidthRange(position) {
const ace_range = new Range(
position.row,
position.column,
position.row,
position.column
)
// Our delete marker is zero characters wide, but Ace doesn't draw ranges
// that are empty. So we monkey patch the range to tell Ace it's not empty
// We do want to claim to be empty if we're off screen after clipping rows
// though. This is the code we need to trick:
// var range = marker.range.clipRows(config.firstRow, config.lastRow);
// if (range.isEmpty()) continue;
ace_range.clipRows = function(first_row, last_row) {
this.isEmpty = function() {
return first_row > this.end.row || last_row < this.start.row
}
return this
}
return ace_range
}
}
return TrackChangesAdapter
})

View file

@ -522,8 +522,12 @@ define([
$scope.$on('editor:track-changes:changed', function() {
const doc_id = $scope.editor.open_doc_id
updateEntries(doc_id)
$scope.$broadcast('review-panel:recalculate-screen-positions')
return $scope.$broadcast('review-panel:layout')
// For now, not worrying about entry panels for rich text
if (!$scope.editor.showRichText) {
$scope.$broadcast('review-panel:recalculate-screen-positions')
return $scope.$broadcast('review-panel:layout')
}
})
$scope.$on('editor:track-changes:visibility_changed', () =>

View file

@ -83,6 +83,7 @@
#editor, #editor-rich-text {
.full-size;
top: 32px;
}
#editor-rich-text {

View file

@ -114,7 +114,7 @@
}
position: absolute;
top: 0px;
top: 32px;
bottom: 0px;
right: 0px;
background-color: @rp-bg-blue;
@ -919,7 +919,27 @@
}
.track-changes-added-marker {
background-color: @rp-green-on-dark;
}
}
}
}
.cm-editor-wrapper {
.track-changes-marker {
border-radius: 0;
}
.track-changes-added-marker {
// Uses rgba so not to affect the opacity of the text - doesn't layer like ace
background-color: rgba(44, 142, 48, 0.3);
color: black;
}
.track-changes-deleted-marker-callout {
border-bottom: 1px dashed #c5060b;
border-left: 1px dotted #c5060b;
width: 100%;
height: 20px;
margin-top: -20px;
}
}